开发视频插件
本文展示如何使用声网 SDK 提供的 API 开发一个视频插件。
开发视频插件需要使用以下接口:
- IExtensionVideoFilter:实现视频数据的处理能力,包括接收、处理和返回处理完的视频帧。
- IExtensionProvider:将视频数据的处理能力封装为插件。
前提条件
开发前,请确保你的开发环境满足以下要求:
- Android Studio 3.0 或以上版本。
- Android API 级别 16 或以上。
准备开发环境
参考如下步骤将声网云市场的 API 添加到你的项目中:
- 下载视频 SDK v4.x,然后解压下载的 SDK 包。
- 将 SDK 包中的 /rtc/sdk/low_level_api/include路径下的头文件拷贝至你的项目文件夹中。
开发视频插件
实现插件
视频插件通过 IExtensionVideoFilter 接口实现。该接口位于 NGIAgoraMediaNode.h 文件中。IExtensionVideoFilter 接口类包含如下方法:
- getProcessMode
- start
- stop
- getVideoFormatWanted
- adaptVideoFrame
- pendVideoFrame
- deliverVideoFrame
- setProperty
- getProperty
getProcessMode
设置 SDK 与视频插件的交互模式。SDK 在加载插件时,会首先调用该方法向插件发送回调。收到该回调后,你需要在返回值中指定自己需要的交互模式。
virtual void getProcessMode(ProcessMode& mode, bool& independent_thread) = 0;
| 参数 | 描述 | 
|---|---|
| mode | SDK 和插件互相传递视频数据的模式,支持设为以下值: 
 | 
| independent_thread | 是否为插件创建独立的线程: 
 | 
你可以根据 YUV 运算的复杂度和是否使用 OpenGL 设置 mode 和 independent_thread 参数:
- 如果视频插件内部使用较复杂的 YUV 运算处理数据,推荐将 mode和independent_thread分别设为Async和false;如果视频插件内部使用简单的 YUV 运算处理数据,推荐将mode和independent_thread分别设为Sync和false。
- 如果视频插件内部使用了 OpenGL 处理数据,推荐将 mode和independent_thread分别设为Sync和true。
start
virtual int start(agora::agora_refptr<Control> control) = 0;
创建视频插件实例 IExtensionVideoFilter 后,会触发该回调。你可以在该回调中初始化 OpenGL。
SDK 会在该方法中传递一个 Control 对象,方便插件和 SDK 的后续交互。你需要保留收到的 Control 对象,并根据场景需要实现该对象中的相关方法:
class Control : public RefCountInterface {
    public:
    /**
    * 在异步模式下(mode 设为 Async),插件需要调用该方法向 SDK 返回处理后的视频帧。
    * 使用该方法前,请确保 SDK 已经通过 pendVideoFrame 提交了待处理的视频帧。
    */
    virtual ProcessResult deliverVideoFrame(agora::agora_refptr<IVideoFrame> frame) = 0;
    /**
    * 插件需要开辟新的内存空间时,可以调用该方法创建一个新的 IVideoFrame 对象。
    * 例如:美颜插件如果需要同时保存原图和美化后的图片,可以调用该方法优化内存管理。你也可以自行管理内存。
    */
    virtual agora::agora_refptr<IVideoFrameMemoryPool> getMemoryPool() = 0;
    /**
    * 插件可以调用该方法向 SDK 报告事件,SDK 会将该事件通知给 App。
    */
    virtual int postEvent(const char* key, const char* value) = 0;
    /**
    * 插件可以调用该方法向 SDK 打印日志。
    */
    virtual void printLog(commons::LOG_LEVEL level, const char* format, ...) = 0;
    /**
    * 当插件内部出错且不可恢复时,可以调用该方法让 SDK 停止向插件发送视频帧。
    * SDK 也会同步将该方法中设置的错误及具体报错信息同步给 App。
    */
    virtual void disableMe(int error, const char* msg) = 0;
};
stop
virtual int stop() = 0;
销毁视频插件实例 IExtensionVideoFilter 后,会触发该回调。你可以在该回调中释放 OpenGL。
getVideoFormatWanted
设置待处理视频数据的类型和格式。SDK 每向插件提供一帧待处理的视频数据,都会先调用该方法向插件发送回调。收到该回调后,你需要在返回值中指定这一帧的数据类型和格式。你可以给不同的视频帧指定不同的数据类型和格式。
virtual void getVideoFormatWanted(VideoFrameData::Type& type, RawPixelBuffer::Format& format) = 0;
| 参数 | 描述 | 
|---|---|
| type | 视频数据的类型,目前仅支持设为 RawPixels,代表原始数据。 | 
| format | 视频数据的格式,支持设为以下值: 
 | 
adaptVideoFrame
处理视频帧。在同步模式下(mode 设为 Sync),SDK 通过该方法与视频插件互相传递数据。SDK 通过 in 将待处理的视频数据传递给插件,插件通过 out 将处理后的数据返回给 SDK。
virtual ProcessResult adaptVideoFrame(agora::agora_refptr<IVideoFrame> in, agora::agora_refptr<IVideoFrame>& out) {
    return ProcessResult::kBypass;
}
参数
| 参数名 | 描述 | 
|---|---|
| in | 输入参数。待处理的视频帧。 | 
| out | 输出参数。处理后的视频帧。 | 
返回值
处理该视频帧的结果:
- Success: 成功处理该视频帧。
- ByPass: 跳过处理该视频帧,并将它传递到后续链路。
- Drop: 丢弃该视频帧。
pendVideoFrame
提交待处理的视频帧。在异步模式下(mode 设为 Async),SDK 通过该方法向视频插件提交待处理的视频帧。使用该方法后,插件必须通过 Control 类的 deliverVideoFrame 方法返回处理后的视频帧。
virtual ProcessResult pendVideoFrame(agora::agora_refptr<IVideoFrame> frame) {
    return ProcessResult::kBypass;
}
参数
| 参数名 | 描述 | 
|---|---|
| frame | 待处理的视频帧。 | 
返回值
处理该视频帧的结果:
- Success: 成功处理该视频帧。
- ByPass: 跳过处理该视频帧,并将它传递到后续链路。
- Drop: 丢弃该视频帧。
setProperty
设置视频插件属性。App 开发者调用 setExtensionProperty 时,SDK 会调用该方法。你需要返回视频插件的属性。
int ExtensionVideoFilter::setProperty(const char *key, const void *buf, size_t buf_size)
| 参数 | 描述 | 
|---|---|
| key | 插件属性的 key。 | 
| buf | 插件属性 key值对应的 buffer 地址,数据形式为 JSON 字符串。我们推荐使用第三方的nlohmann/json library开源库,帮助实现 C++ 的 struct 和 JSON 字符串之前的序列和反序列化。 | 
| buf_size | 插件属性 buffer 的内存大小。 | 
getProperty
获取视频插件属性。App 开发者调用 getProperty 时,SDK 会调用该方法获取视频插件的属性。
int ExtensionVideoFilter::getProperty(const char *key, void *buf, size_t buf_size)
| 参数 | 描述 | 
|---|---|
| key | 插件属性的 key。 | 
| property | 插件属性指针。 | 
| buf_size | 插件属性 buffer 的内存大小。 | 
示例代码
参考如下示例代码了解如何使用上述方法实现一个视频插件:
#include "ExtensionVideoFilter.h"
#include "../logutils.h"
#include <sstream>
namespace agora {
    namespace extension {
        ExtensionVideoFilter::ExtensionVideoFilter(agora_refptr < ByteDanceProcessor > byteDanceProcessor): threadPool_(1) {
            byteDanceProcessor_ = byteDanceProcessor;
        }
        ExtensionVideoFilter::~ExtensionVideoFilter() {
            byteDanceProcessor_ -> releaseOpenGL();
        }
        // 设置 SDK 与视频插件的交互模式。
        void ExtensionVideoFilter::getProcessMode(ProcessMode & mode, bool & independent_thread) {
            mode = ProcessMode::kSync;
            independent_thread = false;
            mode_ = mode;
        }
        // 设置待处理视频数据的类型和格式。
        void ExtensionVideoFilter::getVideoFormatWanted(rtc::VideoFrameData::Type & type,
            rtc::RawPixelBuffer::Format & format) {
            type = rtc::VideoFrameData::Type::kRawPixels;
            format = rtc::RawPixelBuffer::Format::kI420;
        }
        // 在 start 回调中保留 Control 对象并初始化 OpenGL
        int ExtensionVideoFilter::start(agora::agora_refptr < Control > control) {
            PRINTF_INFO("ExtensionVideoFilter::start");
            if (!byteDanceProcessor_) {
                return -1;
            }
            if (control) {
                control_ = control;
                byteDanceProcessor_ -> setExtensionControl(control);
            }
            if (mode_ == ProcessMode::kAsync) {
                invoker_id = threadPool_.RegisterInvoker("thread_videofilter");
                auto res = threadPool_.PostTaskWithRes(invoker_id, [byteDanceProcessor = byteDanceProcessor_] {
                    return byteDanceProcessor -> initOpenGL();
                });
                isInitOpenGL = res.get();
            } else {
                isInitOpenGL = byteDanceProcessor_ -> initOpenGL();
            }
            return 0;
        }
        // 在 stop 回调中释放 OpenGL
        int ExtensionVideoFilter::stop() {
            PRINTF_INFO("ExtensionVideoFilter::stop");
            if (byteDanceProcessor_) {
                byteDanceProcessor_ -> releaseOpenGL();
                isInitOpenGL = false;
            }
            return 0;
        }
        // 如果 mode 设为 Async,则 SDK 和视频插件通过 pendVideoFrame 和 deliverVideoFrame 传递数据。
        rtc::IExtensionVideoFilter::ProcessResult ExtensionVideoFilter::pendVideoFrame(agora::agora_refptr < rtc::IVideoFrame > frame) {
            if (!frame || !isInitOpenGL) {
                return kBypass;
            }
            bool isAsyncMode = (mode_ == ProcessMode::kAsync);
            if (isAsyncMode && byteDanceProcessor_ && control_ && invoker_id >= 0) {
                threadPool_.PostTask(invoker_id, [videoFrame = frame, byteDanceProcessor = byteDanceProcessor_, control = control_] {
                    rtc::VideoFrameData srcData;
                    videoFrame -> getVideoFrameData(srcData);
                    byteDanceProcessor -> processFrame(srcData);
                    control -> deliverVideoFrame(videoFrame);
                });
                return kSuccess;
            }
            return kBypass;
        }
        // 如果 mode 设为 Sync,则 SDK 和视频插件通过 adaptVideoFrame 传递数据。
        rtc::IExtensionVideoFilter::ProcessResult ExtensionVideoFilter::adaptVideoFrame(agora::agora_refptr < rtc::IVideoFrame > src,
            agora::agora_refptr < rtc::IVideoFrame > & dst) {
            if (!isInitOpenGL) {
                return kBypass;
            }
            bool isSyncMode = (mode_ == ProcessMode::kSync);
            if (isSyncMode && byteDanceProcessor_) {
                rtc::VideoFrameData srcData;
                src -> getVideoFrameData(srcData);
                byteDanceProcessor_ -> processFrame(srcData);
                dst = src;
                return kSuccess;
            }
            return kBypass;
        }
        // 设置视频插件属性。
        int ExtensionVideoFilter::setProperty(const char * key,
            const void * buf, size_t buf_size) {
            PRINTF_INFO("setProperty  %s  %s", key, buf);
            std::string stringParameter((char * ) buf);
            byteDanceProcessor_ -> setParameters(stringParameter);
            return 0;
        }
        // 获取视频插件属性
        int ExtensionVideoFilter::getProperty(const char * key, void * buf, size_t buf_size) {
            return -1;
        }
    }
}
封装插件
封装插件通过 IExtensionProvider 接口类实现。该接口位于 NGIAgoraExtensionProvider.h 文件中。你需要先实现这个接口类,并至少实现如下方法:
IExtensionProvider 是插件动态库的提供者,由全局唯一的名字标识。每一个插件动态库可以提供一个或多个有不同功能的插件,如视频前处理插件库可以提供不同功能的视频插件(Video filter)。每个插件是一个具体的 Extension 对象。
enumerateExtensions
提供所有支持封装的插件的信息。SDK 在加载插件时,会调用该方法向插件发送回调。收到该回调后,你需要通过返回值提供所有支持封装的插件的信息。
virtual void enumerateExtensions(ExtensionMetaInfo* extension_list,
                                 int& extension_count) {
    (void) extension_list;
    extension_count = 0;
}
| 参数 | 描述 | 
|---|---|
| extension_list | 插件的信息,包括插件类型和插件名称。支持的视频插件类型有: 
 插件名由插件服务商指定,需要在  
 | 
| extension_count | 支持封装的插件的总数量。 | 
其中,插件的信息定义如下:
// 插件类型指插件在音视频传输通道中的位置,分类如下:
enum EXTENSION_TYPE {
    // 已废弃,请改用 AUDIO_RECORDING_LOCAL_PLAYBACK_FILTER 或 AUDIO_POST_PROCESSING_FILTER
    AUDIO_FILTER,
    // 视频前处理插件
    VIDEO_PRE_PROCESSING_FILTER,
    // 视频后处理插件
    VIDEO_POST_PROCESSING_FILTER,
    // 预留参数
    AUDIO_SINK,
    // 预留参数
    VIDEO_SINK,
    // 用于处理本地采集(如耳返采集)的,用于本地播放的音频数据
    AUDIO_RECORDING_LOCAL_PLAYBACK_FILTER = 10000,
    // 经过 3A 算法后的本地音频数据后处理
    AUDIO_POST_PROCESSING_FILTER = 10001,
    // 用于对远端用户的音频数据进行处理
    AUDIO_REMOTE_USER_PLAYBACK_FILTER = 10002,
};
// 插件的信息,包括插件类型和插件名称
struct ExtensionMetaInfo {
    EXTENSION_TYPE type;
    const char* extension_name;
};
如果你在 enumerateExtensions 方法中返回的插件类型为 VIDEO_PRE_PROCESSING_FILTER 或 VIDEO_POST_PROCESSING_FILTER,则在 App 开发者初始化 RtcEngine 并创建 IExtensionVideoProvider 对象后,SDK 会调用 createVideoFilter 方法。
createVideoFilter
创建视频插件。SDK 调用该方法后,你需要返回 IExtensionVideoFilter 实例。
virtual agora_refptr<IExtensionVideoFilter> createVideoFilter(const char* name) {
    return NULL;
}
成功创建 IExtensionVideoFilter 实例后,视频插件会在合适的时机通过 IExtensionFilter 类对输入的视频数据进行处理。
示例代码
参考如下示例代码了解如何使用上述方法封装视频插件:
#include "ExtensionProvider.h"
#include "../logutils.h"
#include "VideoProcessor.h"
#include "plugin_source_code/JniHelper.h"
namespace agora {
    namespace extension {
        ExtensionProvider::ExtensionProvider() {
            PRINTF_INFO("ExtensionProvider create");
            byteDanceProcessor_ = new agora::RefCountedObject < ByteDanceProcessor > ();
            audioProcessor_ = new agora::RefCountedObject < AdjustVolumeAudioProcessor > ();
        }
        ExtensionProvider::~ExtensionProvider() {
            PRINTF_INFO("ExtensionProvider destroy");
            byteDanceProcessor_.reset();
            audioProcessor_.reset();
        }
        // 提供所有支持封装的插件的信息。
        void ExtensionProvider::enumerateExtensions(ExtensionMetaInfo * extension_list,
            int & extension_count) {
            extension_count = 1;
            ExtensionMetaInfo i;
            i.type = EXTENSION_TYPE::VIDEO_PRE_PROCESSING_FILTER;
            i.extension_name = "YourExtensionName";
            extension_list[0] = i;
        }
        // 创建视频插件。
        agora_refptr < agora::rtc::IExtensionVideoFilter > ExtensionProvider::createVideoFilter(const char * name) {
            PRINTF_INFO("ExtensionProvider::createVideoFilter %s", name);
            auto videoFilter = new agora::RefCountedObject < agora::extension::ExtensionVideoFilter > (byteDanceProcessor_);
            return videoFilter;
        }
        void ExtensionProvider::setExtensionControl(rtc::IExtensionControl * control) {}
    }
}
打包视频插件
完成插件开发后,你需要对其进行注册、打包,并将最终的 .aar 或 .so 文件,连同一个包含了插件名称、服务商名称和 Filter 名称的文件提交给声网进行验证。
注册插件
插件通过宏 REGISTER_AGORA_EXTENSION_PROVIDER 进行注册,该宏位于 AgoraExtensionProviderEntry.h 文件中。
你需要在插件的入口使用这个宏。SDK 在加载插件时,该宏会自动向 SDK 注册你的插件。注意填入 PROVIDER_NAME 时不要填写标点符号。示例:
REGISTER_AGORA_EXTENSION_PROVIDER(ByteDance, agora::extension::ExtensionProvider);
在 CMakeLists.txt 文件中,按照下表指定 SDK 包中 libagora-rtc-sdk-jni.so 文件的存储路径:
| 依赖文件 | 存储路径 | 
|---|---|
| 64 位的 libagora-rtc-sdk-jni.so | AgoraWithByteDanceAndroid/agora-bytedance/src/main/agoraLibs/arm64-v8a | 
| 32 位的 libagora-rtc-sdk-jni.so | AgoraWithByteDanceAndroid/agora-bytedance/src/main/agoraLibs/arm64-v7a | 
提供插件信息
新建一个 .java 或 .md 文件,并在其中填入如下信息:
- EXTENSION_NAME:插件包的名称,如- libagora-bytedance.so文件的- EXTENSION_NAME就是- agora-bytedance。
- EXTENSION_VENDOR_NAME:服务商名称,即插件的- Provider对象名称,用于在- .cpp文件中进行注册。
- EXTENSION_FILTER_NAME:- ExtensionProvider.h文件中定义的 Filter 名称。
示例项目
声网提供一个 Android 插件开发的示例项目 agora-simple-filter 供你参考。