考虑到滤镜的性能问题和美颜厂商的多样性,SDK 的自定义视频前处理功能采用面向对象设计,结合线程模型,帮助用户把外部处理视频数据的代码封装成可替换的滤镜组件。
自定义视频前处理一般在以下情况下使用: 当 SDK 自带的美颜无法满足需求,例如需要做挂件、贴纸,或者美颜效果无法达到预期时。
但是对于比较复杂的场景,例如想要用摄像头画面做图层混合,建议开发者使用视频外部采集功能实现,这样性能优化的空间会更大。
请注意:
1.SDK 会在适当的时机创建和销毁
ZegoVideoFilter
,开发者无需担心生命周期不一致的问题。2.开发者在外部调用 SDK 的切换摄像头的方法时,SDK 内部会调用
zego_stopAndDeAllocate
等方法以实现摄像头的切换,如果客户使用需要初始化多次的美颜资源(比如美颜资源和线程相关或者有上下文的),并在zego_stopAndDeAllocate
中做了释放美颜资源的操作,在有切换摄像头的需求下,需要在zego_allocateAndStart
中重新初始化美颜资源,否则会导致摄像头切换后画面卡顿;像贴纸类只需要初始化一次的美颜资源,建议放到构造滤镜时进行初始化(ZegoVideoFilterFactory
的zego_create
)
为了实现传输不同数据模型,适配不同线程模型,同时避免实现多余接口,SDK 采用伪 COM 的设计方式。
开发者需要在 ZGVideoFilterFactoryDemo
子类中显式指定一种数据传递类型,SDK 目前支持的类型有:
滤镜类型 | 说明 |
---|---|
ZegoVideoBufferTypeAsyncPixelBuffer |
异步滤镜(异步传递 BGRA32 的 CVPixelBufferRef) |
ZegoVideoBufferTypeSyncPixelBuffer |
同步滤镜(同步传递 BGRA32 的 CVPixelBufferRef) |
ZegoVideoBufferTypeAsyncI420PixelBuffer |
异步 I420 滤镜(异步传递 I420 的 CVPixelBufferRef) |
ZegoVideoBufferTypeAsyncNV12PixelBuffer |
异步 NV12 滤镜(异步传递 NV12 的 CVPixelBufferRef) |
SDK 会根据数据类型,实例化不同类型的 client,在调用 zego_allocateAndStart
时传给外部滤镜。
下面会按照不同的滤镜类型,分别介绍用法。
请注意:
如果外部滤镜没有明显的性能问题,使用同步滤镜可以减少数据拷贝的次数。 但如果比较耗时,请使用异步的方式传递数据,以保证低端机型可以流畅运行。
请开发者结合业务特点,选择合适的滤镜类型。
相关功能的 Demo 源码,请联系 ZEGO 技术支持获取。
下面将以 ZegoVideoBufferTypeAsyncPixelBuffer
(异步传递 BGRA32 图像数据)滤镜类型为例演示外部滤镜的用法。
实现 ZegoVideoBufferTypeAsyncPixelBuffer
滤镜类型的时序图如下:
ZegoVideoFilter
定义最基本的组件功能,包括 zego_allocateAndStart
、zego_stopAndDeAllocate
,方便 SDK 在直播流程中进行交互。
ZGVideoFilterAsyncDemo
的类定义如下:
// 注意异步滤镜设备需要实现 ZegoVideoFilter、ZegoVideoBufferPool 协议
ZGVideoFilterAsyncDemo.h
@interface ZGVideoFilterAsyncDemo : NSObject<ZegoVideoFilter, ZegoVideoBufferPool>
@end
ZGVideoFilterAsyncDemo.m
@interface ZGVideoFilterAsyncDemo ()
@property (atomic) int pendingCount; // 未处理帧数
@end
@implementation ZGVideoFilterAsyncDemo {
id<ZegoVideoFilterClient> client_;
id<ZegoVideoBufferPool> buffer_pool_;
dispatch_queue_t queue_;
int width_;
int height_;
int stride_;
CVPixelBufferPoolRef pool_;
int buffer_count_;
}
外部滤镜需要根据 supportBufferType 的类型,对 client 进行转型:
- ZegoVideoBufferTypeAsyncPixelBuffer: SDK 按照 ZegoVideoBufferPool 调用外部滤镜。
- ZegoVideoBufferTypeSyncPixelBuffer: SDK 按照 ZegoVideoFilterDelegate 调用外部滤镜。
- ZegoVideoBufferTypeAsyncI420PixelBuffer:SDK 按照 ZegoVideoBufferPool 调用外部滤镜,与 Async 型滤镜只是图像颜色空间有所区别。
- ZegoVideoBufferTypeAsyncNV12PixelBuffer:SDK 按照 ZegoVideoBufferPool 调用外部滤镜,与 Async 型滤镜只是图像颜色空间有所区别。
正常来说,如果 SDK 是异步调用外部滤镜,外部滤镜完成前处理后,也按照同样的步骤回调 SDK。
在本例中,开发者需要显式在 supportBufferType
中指定当前使用的滤镜类型为异步滤镜:
- (ZegoVideoBufferType)supportBufferType {
// 返回滤镜的类型,此滤镜为:异步 BGRA32 滤镜
return ZegoVideoBufferTypeAsyncPixelBuffer;
}
开发者初始化资源在 zego_allocateAndStart
中进行。
开发者在 zego_allocateAndStart
中获取到 client(SDK 内部实现的、同样实现 ZegoVideoFilterClient 协议的对象),用于通知 SDK 处理结果。
SDK 会在 App 第一次预览/推流/拉流时调用 zego_allocateAndStart
。除非 App 中途调用过 zego_stopAndDeAllocate
,否则 SDK 不会再调用 zego_allocateAndStart
。
- (void)zego_allocateAndStart:(id<ZegoVideoFilterClient>) client {
client_ = client;
if ([client_ conformsToProtocol:@protocol(ZegoVideoBufferPool)]) {
buffer_pool_ = (id<ZegoVideoBufferPool>)client;
}
width_ = 0;
height_ = 0;
stride_ = 0;
pool_ = nil;
buffer_count_ = 4;
self.pendingCount = 0;
queue_ = dispatch_queue_create("video.filter", nil);
}
请注意,client 必须保存为强引用对象,在
zego_stopAndDeAllocate
被调用前必须一直被保存,SDK 不负责管理 client 的生命周期
开发者释放资源在 zego_stopAndDeAllocate
中进行。
建议同步停止滤镜任务后再清理 client 对象,保证 SDK 调用 zego_stopAndDeAllocate
后,没有残留的异步任务导致野指针 crash。
- (void)zego_stopAndDeAllocate {
if (queue_) {
dispatch_sync(queue_, ^ {
});
queue_ = nil;
}
if (pool_) {
[ZGImageUtils destroyPixelBufferPool:&pool_];
pool_ = nil;
}
if (client_) {
[client_ destroy];
client_ = nil;
buffer_pool_ = nil;
}
}
请注意,开发者必须在
zego_stopAndDeAllocate
方法中调用 client 的destroy
方法,否则会造成内存泄漏。
SDK 先调用 ZegoVideoBufferPool 的 dequeueInputBuffer:height:stride:
方法,通知外部滤镜当前采集图像的宽高,并请求外部滤镜返回一个 CVPixelBufferRef 用于拷贝内存数据。
// SDK 回调。从 App 获取 CVPixelBufferRef 对象,用于保存视频帧数据
- (CVPixelBufferRef)dequeueInputBuffer:(int)width height:(int)height stride:(int)stride {
// * 按需创建 CVPixelBufferPool
if (width_ != width || height_ != height || stride_ != stride) {
if (pool_) {
[ZGImageUtils destroyPixelBufferPool:&pool_];
}
if ([ZGImageUtils create32BGRAPixelBufferPool:&pool_ width:width height:height]) {
width_ = width;
height_ = height;
stride_ = stride;
} else {
return nil;
}
}
// * 如果处理不及时,未处理帧超过了 pool 的大小,则丢弃该帧
if (self.pendingCount >= buffer_count_) {
return nil;
}
CVPixelBufferRef pixel_buffer = nil;
CVReturn ret = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pool_, &pixel_buffer);
if (ret != kCVReturnSuccess) {
return nil;
} else {
self.pendingCount = self.pendingCount + 1;
// * 返回一个可以用于存储采集到的图像的 CVPixelBuffer 实例
return pixel_buffer;
}
}
queueInputBuffer:timestamp:
方法通知外部滤镜。dequeueInputBuffer:height:stride:
向 SDK 请求 CVPixelBufferRef
作为拷贝目标queueInputBuffer:timestamp:
通知 SDK 拷贝完毕外部滤镜应当按照约定的数据传递类型,切换线程,异步处理。
请注意,此处的演示代码没有做任何操作,只是在另一个线程进行数据拷贝。开发者应该按照各自业务需求,差异化实现该方法。
// SDK 回调。App 在此接口中获取 SDK 采集到的视频帧数据,并进行处理
- (void)queueInputBuffer:(CVPixelBufferRef)pixel_buffer timestamp:(unsigned long long)timestamp_100n {
// * 采集到的图像数据通过这个传进来,这个点需要异步处理
dispatch_async(queue_, ^ {
int imageWidth = (int)CVPixelBufferGetWidth(pixel_buffer);
int imageHeight = (int)CVPixelBufferGetHeight(pixel_buffer);
int imageStride = (int)CVPixelBufferGetBytesPerRowOfPlane(pixel_buffer, 0);
CVPixelBufferRef dst = [buffer_pool_ dequeueInputBuffer:imageWidth height:imageHeight stride:imageStride];
if (!dst) {
return;
}
// 自定义前处理:此处使用 FaceUnity 作为外部滤镜
CVPixelBufferRef output = [[FUManager shareManager] renderItemsToPixelBuffer:pixel_buffer];
if ([ZGImageUtils copyPixelBufferFrom:output to:dst]) {
// * 把从 buffer pool 中得到的 CVPixelBuffer 实例传进来
[buffer_pool_ queueInputBuffer:dst timestamp:timestamp_100n];
}
self.pendingCount = self.pendingCount - 1;
CVPixelBufferRelease(pixel_buffer);
});
}
上述步骤带 FaceUnity 美颜滤镜的示例代码可以在
/src/Topics/ExternalVideoFilter/VideoFilter
下的ZGVideoFilterAsyncDemo.h
和ZGVideoFilterAsyncDemo.m
找到,具体细节不再赘述。
ZegoVideoFilterFactory
是外部滤镜的入口,定义了创建、销毁 ZegoVideoFilter
接口,向 SDK 提供管理 ZegoVideoFilter
生命周期的能力。需要调用 setVideoFilterFactory:channelIndex:
的地方必须实现该接口。
下述代码演示了如何创建外部滤镜工厂。工厂保存了 ZegoVideoFilter
的实例,不会反复创建。
ZGVideoFilterFactoryDemo.h
@interface ZGVideoFilterFactoryDemo : NSObject<ZegoVideoFilterFactory>
@property (nonatomic, assign) ZegoVideoBufferType bufferType;
@end
ZGVideoFilterFactoryDemo.m
@implementation ZGVideoFilterFactoryDemo {
id<ZegoVideoFilter> g_filter_;
}
// 创建外部滤镜实例
- (id<ZegoVideoFilter>)zego_create {
if (g_filter_ == nil) {
// 此处的 bufferType 对应四种滤镜类型,以创建不同的外部滤镜实例
switch (self.bufferType) {
case ZegoVideoBufferTypeAsyncPixelBuffer:
g_filter_ = [[ZGVideoFilterAsyncDemo alloc] init];
break;
case ZegoVideoBufferTypeSyncPixelBuffer:
g_filter_ = [[ZGVideoFilterSyncDemo alloc] init];
break;
case ZegoVideoBufferTypeAsyncI420PixelBuffer:
g_filter_ = [[ZGVideoFilterI420Demo alloc] init];
break;
case ZegoVideoBufferTypeAsyncNV12PixelBuffer:
g_filter_ = [[ZGVideoFilterNV12Demo alloc] init];
break;
default:
break;
}
}
return g_filter_;
}
// 销毁外部滤镜实例
- (void)zego_destroy:(id<ZegoVideoFilter>)filter {
if (g_filter_ == filter) {
g_filter_ = nil;
}
}
@end
请注意:
- 大部分情况下,
ZegoVideoFilterFactory
会缓存原有ZegoVideoFilter
实例,开发者需避免创建新的实例。- 开发者必须保证
ZegoVideoFilter
在create
和destroy
之间是可用的,请勿直接销毁对象。
开发者需要使用外部滤镜功能时,请在使用前调用 setVideoFilterFactory:channelIndex:
设置外部滤镜工厂对象(此例中的对象为步骤 3.2 中所创建的 ZGVideoFilterFactoryDemo
)。
请注意,如果用户释放了工厂对象,不再需要它时,请调用本接口将其设置为空。
ZGExternalVideoFilterDemo.h
@interface ZGExternalVideoFilterDemo : NSObject
+ (instancetype)shared;
/**
初始化外部滤镜工厂对象
@param type 视频缓冲区类型(Async, Sync, I420)
@discussion `2020-05-12` 之前版本,由于外部采集工厂对象需要在 initSDK 之前设置,所以创建外部滤镜工厂对象后,先释放 ZegoLiveRoomSDK 确保 setVideoFilterFactory:channelIndex: 的调用在 initSDK 前
*/
- (void)initFilterFactoryType:(ZegoVideoBufferType)type;
/**
释放外部滤镜工厂对象
*/
- (void)releaseFilterFactory;
@end
ZGExternalVideoFilterDemo.m
@interface ZGExternalVideoFilterDemo ()
@property (nonatomic, strong) ZGVideoFilterFactoryDemo *g_filterFactory;
@end
@implementation ZGExternalVideoFilterDemo
+ (instancetype)shared {
static id instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
- (void)initFilterFactoryType:(ZegoVideoBufferType)type {
if (self.g_filterFactory == nil) {
self.g_filterFactory = [[ZGVideoFilterFactoryDemo alloc] init];
self.g_filterFactory.bufferType = type;
}
[ZGApiManager releaseApi];
[ZegoExternalVideoFilter setVideoFilterFactory:self.g_filterFactory channelIndex:ZEGOAPI_CHN_MAIN];
}
- (void)releaseFilterFactory {
self.g_filterFactory = nil;
[ZegoExternalVideoFilter setVideoFilterFactory:nil channelIndex:ZEGOAPI_CHN_MAIN];
}
@end
请参考文档:快速开始-初始化
2020-05-12
及之后版本的 SDK,必须在引擎启动之前设置外部滤镜工厂。即 setVideoFilterFactory
需要在 startPreview、startPublishing、startPlaying 之前调用才有效。2020-05-12
之前版本的 SDK,必须在初始化 SDK 前设置外部滤镜工厂。即 setVideoFilterFactory
需要在 “initSDK” 之前调用才有效。引擎启动的时机有:1、登录房间;2、未登录房间启动预览功能;3、未登录房间使用媒体播放器;4、未登录房间使用音效播放器。 引擎关闭的时机有:1、退出房间;2、未登录房间并且预览、媒体播放器和引擎播放器都停止工作。
请参考文档:快速开始-登录房间
请参考文档:快速开始-推流
若对图像数据进行了前处理并将前处理后的数据回传给了 SDK,拉流时将是带美颜的图像。
方法 | 描述 |
---|---|
setVideoFilterFactory:channelIndex: | 设置外部滤镜工厂 |
zego_create | 创建外部滤镜实例 |
zego_destroy: | 销毁外部滤镜实例 |
zego_allocateAndStart: | 初始化外部滤镜使用的资源 |
zego_stopAndDeAllocate | 停止并释放外部滤镜占用的资源 |
supportBufferType | 支持的 Buffer 类型 |
dequeueInputBuffer:height:stride: | SDK 获取 CVPixelBufferRef 对象 |
queueInputBuffer:timestamp: | 异步处理视频帧数据 |
destroy | 销毁外部滤镜客户端 |
onProcess:withTimeStatmp: | 外部滤镜同步回调 |
Common Methods | |
initWithAppID:appSignature:completionBlock: | 初始化 SDK |
setUserID:userName: | 设置用户 ID 及 用户名 |
loginRoom:role:withCompletionBlock: | 登录房间 |
startPreview | 启动本地预览 |
startPublishing:title:flag: | 开始推流 |
stopPublishing | 停止推流 |
logoutRoom | 退出房间 |
Q1:提示缺少 FaceUnity 证书?
答:若需要体验美颜效果,需要先从相芯科技申请美颜的鉴权证书,然后替换掉 /src/Topics/ExternalVideoFilter/FaceUnity-SDK-iOS
目录下的 authpack.h
文件。
Q2:如何访问
CVPixelBufferRef
持有的图像数据?
答:请参考 ZGImageUtils
的 copyPixelBufferFrom:to:
方法,然后对照苹果官方头文件。
Q3:
ZegoVideoFilterFactory
的子类什么时候释放?
答:我们推荐把工厂的实例保存为单例,仅作为 SDK 管理外部滤镜生命周期的通道,开发者可以为工厂子类添加 setter
和 getter
,一起管理滤镜的生命周期。
联系我们
文档反馈