开发视频插件
本文展示如何使用声网 SDK 提供的 API 开发一个视频插件。
开发视频插件需要使用以下接口:
IExtensionVideoFilter
:实现视频数据的处理能力,包括接收、处理和返回处理完的视频帧。IExtensionProvider
:将视频数据的处理能力封装为插件。
前提条件
开发前,请确保你的开发环境满足以下要求:
- Xcode 9.0 或以上版本。
- iOS 9.0 或以上版本的设备。
准备开发环境
参考如下步骤将声网云市场的 API 添加到你的项目中:
- 下载视频 SDK v4.x,然后解压下载的 SDK 包。
- 打开 Xcode,进入 TARGETS > Project Name > General > Frameworks, Libraries, and Embedded Content 菜单,点击 + > Add Other... > Add Files,将安装包中的
/libs/AgoraRtcKit.framework
文件添加至你的项目中。确保 Embed 属性设置为 Do Not Embed。
开发视频插件
实现插件
视频插件通过 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 开发者调用 setExtensionPropertyWithVendor
时,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) {}
}
}
打包视频插件
完成插件开发后,你需要对其进行注册、打包,并将最终的 .framework
或 .xcframework
文件,连同一个包含了插件名称、服务商名称和 Filter 名称的文件提交给声网进行验证。
插件通过宏 REGISTER_AGORA_EXTENSION_PROVIDER
进行注册,该宏位于 AgoraExtensionProviderEntry.h
文件中。
你需要在插件的入口使用这个宏。SDK 在加载插件时,该宏会自动向 SDK 注册你的插件。注意填入 PROVIDER_NAME
时不要填写标点符号。示例:
REGISTER_AGORA_EXTENSION_PROVIDER(ByteDance, agora::extension::ExtensionProvider);
示例项目
声网提供一个 iOS 插件开发的示例项目 SimpleFilter 供你参考。