`

XBMC源代码简析 5:视频播放器(dvdplayer)-解复用器(以ffmpeg为例)

    博客分类:
  • XBMC
 
阅读更多

XBMC分析系列文章:

 

XBMC源代码分析 1:整体结构以及编译方法

XBMC源代码分析 2:Addons(皮肤Skin)

XBMC源代码分析 3:核心部分(core)-综述

XBMC源代码分析 4:视频播放器(dvdplayer)-解码器(以ffmpeg为例)

本文我们分析XBMC中视频播放器(dvdplayer)中的解复用器部分。由于解复用器种类很多,不可能一一分析,因此以ffmpeg解复用器为例进行分析。

XBMC解复用器部分文件目录如下图所示:

在这里我们看一下解复用器中的FFMPEG解复用器。对应DVDDemuxFFmpeg.h和DVDDemuxFFmpeg.cpp

之前的分析类文章在解复用器这方面已经做过详细的分析了。在此就不多叙述了,代码很清晰。重点的地方已经标上了注释。

DVDDemuxFFmpeg.h源代码如下所示:

 

/*
 * 雷霄骅
 * leixiaohua1020@126.com
 * 中国传媒大学/数字电视技术
 *
 */
#include "DVDDemux.h"
#include "DllAvFormat.h"
#include "DllAvCodec.h"
#include "DllAvUtil.h"

#include "threads/CriticalSection.h"
#include "threads/SystemClock.h"

#include <map>

class CDVDDemuxFFmpeg;
class CURL;

class CDemuxStreamVideoFFmpeg
  : public CDemuxStreamVideo
{
  CDVDDemuxFFmpeg *m_parent;
  AVStream*        m_stream;
public:
  CDemuxStreamVideoFFmpeg(CDVDDemuxFFmpeg *parent, AVStream* stream)
    : m_parent(parent)
    , m_stream(stream)
  {}
  virtual void GetStreamInfo(std::string& strInfo);
};


class CDemuxStreamAudioFFmpeg
  : public CDemuxStreamAudio
{
  CDVDDemuxFFmpeg *m_parent;
  AVStream*        m_stream;
public:
  CDemuxStreamAudioFFmpeg(CDVDDemuxFFmpeg *parent, AVStream* stream)
    : m_parent(parent)
    , m_stream(stream)
  {}
  std::string m_description;

  virtual void GetStreamInfo(std::string& strInfo);
  virtual void GetStreamName(std::string& strInfo);
};

class CDemuxStreamSubtitleFFmpeg
  : public CDemuxStreamSubtitle
{
  CDVDDemuxFFmpeg *m_parent;
  AVStream*        m_stream;
public:
  CDemuxStreamSubtitleFFmpeg(CDVDDemuxFFmpeg *parent, AVStream* stream)
    : m_parent(parent)
    , m_stream(stream)
  {}
  std::string m_description;

  virtual void GetStreamInfo(std::string& strInfo);
  virtual void GetStreamName(std::string& strInfo);

};

#define FFMPEG_FILE_BUFFER_SIZE   32768 // default reading size for ffmpeg
#define FFMPEG_DVDNAV_BUFFER_SIZE 2048  // for dvd's
//FFMPEG解复用
class CDVDDemuxFFmpeg : public CDVDDemux
{
public:
  CDVDDemuxFFmpeg();
  virtual ~CDVDDemuxFFmpeg();
  //打开一个流
  bool Open(CDVDInputStream* pInput);
  void Dispose();//关闭
  void Reset();//复位
  void Flush();
  void Abort();
  void SetSpeed(int iSpeed);
  virtual std::string GetFileName();

  DemuxPacket* Read();

  bool SeekTime(int time, bool backwords = false, double* startpts = NULL);
  bool SeekByte(int64_t pos);
  int GetStreamLength();
  CDemuxStream* GetStream(int iStreamId);
  int GetNrOfStreams();

  bool SeekChapter(int chapter, double* startpts = NULL);
  int GetChapterCount();
  int GetChapter();
  void GetChapterName(std::string& strChapterName);
  virtual void GetStreamCodecName(int iStreamId, CStdString &strName);

  bool Aborted();

  AVFormatContext* m_pFormatContext;
  CDVDInputStream* m_pInput;

protected:
  friend class CDemuxStreamAudioFFmpeg;
  friend class CDemuxStreamVideoFFmpeg;
  friend class CDemuxStreamSubtitleFFmpeg;

  int ReadFrame(AVPacket *packet);
  CDemuxStream* AddStream(int iId);
  void AddStream(int iId, CDemuxStream* stream);
  CDemuxStream* GetStreamInternal(int iStreamId);
  void CreateStreams(unsigned int program = UINT_MAX);
  void DisposeStreams();

  AVDictionary *GetFFMpegOptionsFromURL(const CURL &url);
  double ConvertTimestamp(int64_t pts, int den, int num);
  void UpdateCurrentPTS();
  bool IsProgramChange();

  CCriticalSection m_critSection;
  std::map<int, CDemuxStream*> m_streams;
  std::vector<std::map<int, CDemuxStream*>::iterator> m_stream_index;

  AVIOContext* m_ioContext;
  //各种封装的Dll
  DllAvFormat m_dllAvFormat;
  DllAvCodec  m_dllAvCodec;
  DllAvUtil   m_dllAvUtil;

  double   m_iCurrentPts; // used for stream length estimation
  bool     m_bMatroska;
  bool     m_bAVI;
  int      m_speed;
  unsigned m_program;
  XbmcThreads::EndTime  m_timeout;

  // Due to limitations of ffmpeg, we only can detect a program change
  // with a packet. This struct saves the packet for the next read and
  // signals STREAMCHANGE to player
  struct
  {
    AVPacket pkt;       // packet ffmpeg returned
    int      result;    // result from av_read_packet
  }m_pkt;
};

 

 

该类中以下几个函数包含了解复用器的几个功能。

bool Open(CDVDInputStream* pInput);//打开
void Dispose();//关闭
void Reset();//复位
void Flush();

我们查看一下这几个函数的源代码。

Open()

 

//打开一个流
bool CDVDDemuxFFmpeg::Open(CDVDInputStream* pInput)
{
  AVInputFormat* iformat = NULL;
  std::string strFile;
  m_iCurrentPts = DVD_NOPTS_VALUE;
  m_speed = DVD_PLAYSPEED_NORMAL;
  m_program = UINT_MAX;
  const AVIOInterruptCB int_cb = { interrupt_cb, this };

  if (!pInput) return false;

  if (!m_dllAvUtil.Load() || !m_dllAvCodec.Load() || !m_dllAvFormat.Load())  {
    CLog::Log(LOGERROR,"CDVDDemuxFFmpeg::Open - failed to load ffmpeg libraries");
    return false;
  }
  //注册解复用器
  // register codecs
  m_dllAvFormat.av_register_all();

  m_pInput = pInput;
  strFile = m_pInput->GetFileName();

  bool streaminfo = true; /* set to true if we want to look for streams before playback*/

  if( m_pInput->GetContent().length() > 0 )
  {
    std::string content = m_pInput->GetContent();

    /* check if we can get a hint from content */
    if     ( content.compare("video/x-vobsub") == 0 )
      iformat = m_dllAvFormat.av_find_input_format("mpeg");
    else if( content.compare("video/x-dvd-mpeg") == 0 )
      iformat = m_dllAvFormat.av_find_input_format("mpeg");
    else if( content.compare("video/x-mpegts") == 0 )
      iformat = m_dllAvFormat.av_find_input_format("mpegts");
    else if( content.compare("multipart/x-mixed-replace") == 0 )
      iformat = m_dllAvFormat.av_find_input_format("mjpeg");
  }

  // open the demuxer
  m_pFormatContext  = m_dllAvFormat.avformat_alloc_context();
  m_pFormatContext->interrupt_callback = int_cb;

  // try to abort after 30 seconds
  m_timeout.Set(30000);

  if( m_pInput->IsStreamType(DVDSTREAM_TYPE_FFMPEG) )
  {
    // special stream type that makes avformat handle file opening
    // allows internal ffmpeg protocols to be used
    CURL url = m_pInput->GetURL();
    CStdString protocol = url.GetProtocol();

    AVDictionary *options = GetFFMpegOptionsFromURL(url);

    int result=-1;
    if (protocol.Equals("mms"))
    {
      // try mmsh, then mmst
      url.SetProtocol("mmsh");
      url.SetProtocolOptions("");
	  //真正地打开
      result = m_dllAvFormat.avformat_open_input(&m_pFormatContext, url.Get().c_str(), iformat, &options);
      if (result < 0)
      {
        url.SetProtocol("mmst");
        strFile = url.Get();
      } 
    }
	//真正地打开
    if (result < 0 && m_dllAvFormat.avformat_open_input(&m_pFormatContext, strFile.c_str(), iformat, &options) < 0 )
    {
      CLog::Log(LOGDEBUG, "Error, could not open file %s", CURL::GetRedacted(strFile).c_str());
      Dispose();
      m_dllAvUtil.av_dict_free(&options);
      return false;
    }
    m_dllAvUtil.av_dict_free(&options);
  }
  else
  {
    unsigned char* buffer = (unsigned char*)m_dllAvUtil.av_malloc(FFMPEG_FILE_BUFFER_SIZE);
    m_ioContext = m_dllAvFormat.avio_alloc_context(buffer, FFMPEG_FILE_BUFFER_SIZE, 0, this, dvd_file_read, NULL, dvd_file_seek);
    m_ioContext->max_packet_size = m_pInput->GetBlockSize();
    if(m_ioContext->max_packet_size)
      m_ioContext->max_packet_size *= FFMPEG_FILE_BUFFER_SIZE / m_ioContext->max_packet_size;

    if(m_pInput->Seek(0, SEEK_POSSIBLE) == 0)
      m_ioContext->seekable = 0;

    if( iformat == NULL )
    {
      // let ffmpeg decide which demuxer we have to open

      bool trySPDIFonly = (m_pInput->GetContent() == "audio/x-spdif-compressed");

      if (!trySPDIFonly)
        m_dllAvFormat.av_probe_input_buffer(m_ioContext, &iformat, strFile.c_str(), NULL, 0, 0);

      // Use the more low-level code in case we have been built against an old
      // FFmpeg without the above av_probe_input_buffer(), or in case we only
      // want to probe for spdif (DTS or IEC 61937) compressed audio
      // specifically, or in case the file is a wav which may contain DTS or
      // IEC 61937 (e.g. ac3-in-wav) and we want to check for those formats.
      if (trySPDIFonly || (iformat && strcmp(iformat->name, "wav") == 0))
      {
        AVProbeData pd;
        uint8_t probe_buffer[FFMPEG_FILE_BUFFER_SIZE + AVPROBE_PADDING_SIZE];

        // init probe data
        pd.buf = probe_buffer;
        pd.filename = strFile.c_str();

        // read data using avformat's buffers
        pd.buf_size = m_dllAvFormat.avio_read(m_ioContext, pd.buf, m_ioContext->max_packet_size ? m_ioContext->max_packet_size : m_ioContext->buffer_size);
        if (pd.buf_size <= 0)
        {
          CLog::Log(LOGERROR, "%s - error reading from input stream, %s", __FUNCTION__, CURL::GetRedacted(strFile).c_str());
          return false;
        }
        memset(pd.buf+pd.buf_size, 0, AVPROBE_PADDING_SIZE);

        // restore position again
        m_dllAvFormat.avio_seek(m_ioContext , 0, SEEK_SET);

        // the advancedsetting is for allowing the user to force outputting the
        // 44.1 kHz DTS wav file as PCM, so that an A/V receiver can decode
        // it (this is temporary until we handle 44.1 kHz passthrough properly)
        if (trySPDIFonly || (iformat && strcmp(iformat->name, "wav") == 0 && !g_advancedSettings.m_dvdplayerIgnoreDTSinWAV))
        {
          // check for spdif and dts
          // This is used with wav files and audio CDs that may contain
          // a DTS or AC3 track padded for S/PDIF playback. If neither of those
          // is present, we assume it is PCM audio.
          // AC3 is always wrapped in iec61937 (ffmpeg "spdif"), while DTS
          // may be just padded.
          AVInputFormat *iformat2;
          iformat2 = m_dllAvFormat.av_find_input_format("spdif");

          if (iformat2 && iformat2->read_probe(&pd) > AVPROBE_SCORE_MAX / 4)
          {
            iformat = iformat2;
          }
          else
          {
            // not spdif or no spdif demuxer, try dts
            iformat2 = m_dllAvFormat.av_find_input_format("dts");

            if (iformat2 && iformat2->read_probe(&pd) > AVPROBE_SCORE_MAX / 4)
            {
              iformat = iformat2;
            }
            else if (trySPDIFonly)
            {
              // not dts either, return false in case we were explicitely
              // requested to only check for S/PDIF padded compressed audio
              CLog::Log(LOGDEBUG, "%s - not spdif or dts file, fallbacking", __FUNCTION__);
              return false;
            }
          }
        }
      }

      if(!iformat)
      {
        std::string content = m_pInput->GetContent();

        /* check if we can get a hint from content */
        if( content.compare("audio/aacp") == 0 )
          iformat = m_dllAvFormat.av_find_input_format("aac");
        else if( content.compare("audio/aac") == 0 )
          iformat = m_dllAvFormat.av_find_input_format("aac");
        else if( content.compare("video/flv") == 0 )
          iformat = m_dllAvFormat.av_find_input_format("flv");
        else if( content.compare("video/x-flv") == 0 )
          iformat = m_dllAvFormat.av_find_input_format("flv");
      }

      if (!iformat)
      {
        CLog::Log(LOGERROR, "%s - error probing input format, %s", __FUNCTION__, CURL::GetRedacted(strFile).c_str());
        return false;
      }
      else
      {
        if (iformat->name)
          CLog::Log(LOGDEBUG, "%s - probing detected format [%s]", __FUNCTION__, iformat->name);
        else
          CLog::Log(LOGDEBUG, "%s - probing detected unnamed format", __FUNCTION__);
      }
    }


    m_pFormatContext->pb = m_ioContext;

    if (m_dllAvFormat.avformat_open_input(&m_pFormatContext, strFile.c_str(), iformat, NULL) < 0)
    {
      CLog::Log(LOGERROR, "%s - Error, could not open file %s", __FUNCTION__, CURL::GetRedacted(strFile).c_str());
      Dispose();
      return false;
    }
  }

  // Avoid detecting framerate if advancedsettings.xml says so
  if (g_advancedSettings.m_videoFpsDetect == 0) 
      m_pFormatContext->fps_probe_size = 0;
  
  // analyse very short to speed up mjpeg playback start
  if (iformat && (strcmp(iformat->name, "mjpeg") == 0) && m_ioContext->seekable == 0)
    m_pFormatContext->max_analyze_duration = 500000;

  // we need to know if this is matroska or avi later
  m_bMatroska = strncmp(m_pFormatContext->iformat->name, "matroska", 8) == 0;	// for "matroska.webm"
  m_bAVI = strcmp(m_pFormatContext->iformat->name, "avi") == 0;

  if (streaminfo)
  {
    /* too speed up dvd switches, only analyse very short */
    if(m_pInput->IsStreamType(DVDSTREAM_TYPE_DVD))
      m_pFormatContext->max_analyze_duration = 500000;


    CLog::Log(LOGDEBUG, "%s - avformat_find_stream_info starting", __FUNCTION__);
    int iErr = m_dllAvFormat.avformat_find_stream_info(m_pFormatContext, NULL);
    if (iErr < 0)
    {
      CLog::Log(LOGWARNING,"could not find codec parameters for %s", CURL::GetRedacted(strFile).c_str());
      if (m_pInput->IsStreamType(DVDSTREAM_TYPE_DVD)
      ||  m_pInput->IsStreamType(DVDSTREAM_TYPE_BLURAY)
      || (m_pFormatContext->nb_streams == 1 && m_pFormatContext->streams[0]->codec->codec_id == AV_CODEC_ID_AC3))
      {
        // special case, our codecs can still handle it.
      }
      else
      {
        Dispose();
        return false;
      }
    }
    CLog::Log(LOGDEBUG, "%s - av_find_stream_info finished", __FUNCTION__);
  }
  // reset any timeout
  m_timeout.SetInfinite();

  // if format can be nonblocking, let's use that
  m_pFormatContext->flags |= AVFMT_FLAG_NONBLOCK;

  // print some extra information
  m_dllAvFormat.av_dump_format(m_pFormatContext, 0, strFile.c_str(), 0);

  UpdateCurrentPTS();

  CreateStreams();

  return true;
}

 

 

Dispose()

 

//关闭
void CDVDDemuxFFmpeg::Dispose()
{
  m_pkt.result = -1;
  m_dllAvCodec.av_free_packet(&m_pkt.pkt);

  if (m_pFormatContext)
  {
    if (m_ioContext && m_pFormatContext->pb && m_pFormatContext->pb != m_ioContext)
    {
      CLog::Log(LOGWARNING, "CDVDDemuxFFmpeg::Dispose - demuxer changed our byte context behind our back, possible memleak");
      m_ioContext = m_pFormatContext->pb;
    }
    m_dllAvFormat.avformat_close_input(&m_pFormatContext);
  }

  if(m_ioContext)
  {
    m_dllAvUtil.av_free(m_ioContext->buffer);
    m_dllAvUtil.av_free(m_ioContext);
  }

  m_ioContext = NULL;
  m_pFormatContext = NULL;
  m_speed = DVD_PLAYSPEED_NORMAL;

  DisposeStreams();

  m_pInput = NULL;

  m_dllAvFormat.Unload();
  m_dllAvCodec.Unload();
  m_dllAvUtil.Unload();
}

 

 

Reset()

 

//复位
void CDVDDemuxFFmpeg::Reset()
{
  CDVDInputStream* pInputStream = m_pInput;
  Dispose();
  Open(pInputStream);
}

 

 

Flush()

 

void CDVDDemuxFFmpeg::Flush()
{
  // naughty usage of an internal ffmpeg function
  if (m_pFormatContext)
    m_dllAvFormat.av_read_frame_flush(m_pFormatContext);

  m_iCurrentPts = DVD_NOPTS_VALUE;

  m_pkt.result = -1;
  m_dllAvCodec.av_free_packet(&m_pkt.pkt);
}

 

 

 

分享到:
评论

相关推荐

    script.bluray.com:用于访问 blu-ray.com 功能的 Kodi (XBMC) 插件

    script.bluray.com用于访问 blu-ray.com 功能的 Kodi (XBMC) 插件它目前仅在我的可用。 存储库的安装应通过 Kodi System::Settings::Add-ons::Install from zip 文件完成插件的安装应通过 Kodi System::Settings::...

    repository.xbmc-addons-chinese-1.2.1.zip

    5. 导航到刚才添加的源,选择“repository.xbmc-addons-chinese-1.2.1.zip”进行安装。 6. 安装完成后,会在“我的添加-ons”中出现新的“仓库”条目,用户可以从这里浏览和安装中文插件。 这个压缩包的使用对于...

    aac-rtmp-red5

    4. 更新仓库并安装Red5:`sudo apt-get update && sudo apt-get install red5` 安装完成后,你需要配置Red5服务器,包括设置端口、应用域等。默认情况下,Red5监听1935端口,你可以根据需求修改配置文件。 接下来...

    repository.xbmc-addons-chinese-2.0.0.zip

    标题中的"repository.xbmc-addons-chinese-2.0.0.zip"是一个针对KODI媒体中心的中文插件库的压缩包,版本号为2.0.0。这个压缩包包含了多款专为中国用户设计的KODI插件,旨在提升KODI在中文环境下的用户体验和功能...

    ru:seppius-xbmc-repo(叉子)

    【标题】"ru:seppius-xbmc-repo(叉子)" 指的是一种基于XBMC或Kodi的俄罗斯开发者论坛扩展资源库的克隆版本。XBMC(Xbox Media Center)是一个开源的媒体中心软件,后来更名为Kodi,它允许用户组织和播放各种多媒体...

    kodi中文插件最新2022 repository.xbmc-addons-chinese-2.0.0

    kodi中文插件最新2022 repository.xbmc-addons-chinese-2.0.0

    ffmpeg-2.8.5-Jarvis-rc1.tar.gz

    综上所述,"ffmpeg-2.8.5-Jarvis-rc1.tar.gz" 是为xbmc/kodi开发提供的一个关键的多媒体处理库,包含了一组特定版本的FFmpeg组件,方便开发者快速集成到他们的项目中,以实现多媒体内容的流畅播放。

    repository.xbmc-addons-chinese-2.0.1.zip亲测可用kodi中文插件库下载

    标题中的“repository.xbmc-addons-chinese-2.0.1.zip”是一个针对Kodi的中文插件库的压缩包,版本号为2.0.1。Kodi是一款开源的媒体中心软件,允许用户在各种设备上管理和播放多媒体内容,如视频、音乐和图片。这个...

    xbmc 移植到ANDROID 方法

    XBMC(Xbox Media Center)是一款开源媒体播放器应用,最初为Xbox游戏机开发,后来被移植到了多个操作系统上,包括Android。将XBMC移植到Android平台上需要进行一系列准备工作,包括设置Android开发环境、获取源代码...

    xbmc-forum:XBMC论坛的源代码-Forum source code

    "xbmc-forum:XBMC论坛的源代码"这个标题表明我们讨论的是XBMC官方论坛的源代码,这意味着我们可以深入理解其背后的开发过程、社区互动机制以及论坛的构建方式。 源代码通常对开发者来说是非常宝贵的资源,因为它...

    XBMC-Trailer-Downloader:抓取 hd-trailers.net 并下载用于 XBMC 和 Cinema Experience 脚本的预告片

    XBMC-Trailer-Downloader 抓取 hd-trailers.net 并下载用于 XBMC 和 Cinema Experience 脚本的预告片。 安装:pip install -r requirements.txt 可以以 480/720/1080 的分辨率下载预告片。 可以删除已下载且超过...

    QtAV:基于Qt和FFmpeg(https:github.comwang-binavbuild)的跨平台多媒体框架。 高性能。 用户和开发人员友好。 支持Android,iOS,Windows应用商店和台式机。基于Qt和FFmpeg的跨平台高级音视频播放框架

    它可以帮助您以比以往更少的精力编写播放器。 QtAV已添加到FFmpeg项目页面 QtAV是根据LGPL v2.1条款获得许可的免费软件。 播放器示例已根据GPL v3许可。 如果您使用QtAV或其组成库,则必须遵守相关许可条款。 ...

    XBMC-OBD2:XBMC OBD2 插件-开源

    XBMC-OBD2 是一款基于XBMC(Xbox Media Center,现称为Kodi)平台的开源插件,专为汽车爱好者设计,它允许用户通过ELM327兼容的OBD2诊断适配器获取车辆的实时数据。下面将详细阐述这款插件的功能、工作原理以及如何...

    xbmc-bilibili-main.zip

    在这个xbmc-bilibili-main插件中,Python被用来编写与Bilibili API进行交互的代码,实现视频搜索、播放、评论等功能。 安装此插件的步骤一般包括以下几步: 1. 下载"xbmc-bilibili-main.zip"文件到本地。 2. 在Kodi...

    repository.kodi-chinese-addons:repository.kodi-chinese-addons xbmckodi用于中文插件的存储库

    这意味着用户将获得最新的插件和更新,开发者可以在这里找到最新的源代码以进行维护和开发。 通过这个仓库,Kodi用户可以轻松地安装和管理各种中文插件,比如: 1. **中文界面**:提供完全汉化的Kodi界面,让操作...

    ubuntu下的kodi(XBMC)编译

    标题中的“ubuntu下的kodi(XBMC)编译”指的是在Ubuntu操作系统环境下,对Kodi(以前称为XBMC,Xbox Media Center)媒体中心软件进行源代码编译的过程。Kodi是一款开源的多媒体中心应用,它能播放各种音频、视频格式...

    xbmc-12.1-Frodo-armeabi-v7a.apk

    XBMC最初为Xbox而开发,现在可以运行在Linux、OSX、Windows系统。 2003年,一些兴趣相投的程序员创建了这个项目。XBMC是一个非盈利的项目,由遍布世界各地的自愿者开发维护。超过50名软件开发人员为XBMC作出贡献,...

    Ubuntu 11.04 安装后要做的20件事情.txt

    - **背景**:Medibuntu 是一个额外的软件源,提供了一些不能包含在官方 Ubuntu 发行版中的软件,如多媒体编解码器、专有驱动等。 - **操作步骤**: - 安装 Medibuntu 仓库: ```bash sudo apt-get install ...

    kodi(xbmc)中文字幕插件

    在“repository.xbmc-addons-chinese”这个压缩包中,包含了一些为中国用户定制的Kodi插件,尤其是与中文字幕相关的插件。 安装插件的过程如下: 1. **打开Kodi**:启动你的Kodi应用程序,进入主界面。 2. **进入...

    Kodi-XBMC-plugin-boilerplate:目录结构,包含操作Kodi(XBMC)附加组件所需的所有资源

    解压缩并将文件夹“ plugin.media-type.plugin-name ”重命名为您的插件名称。 在您喜欢的Editor / IDE中将文件夹添加到项目中,然后开始编码! 插件目录结构 去做 贡献 叉吧! 创建功能分支: git checkout -b ...

Global site tag (gtag.js) - Google Analytics