自定义视频前处理

下载pdf
更新时间:2020-01-02 19:05

1 功能简介

考虑到滤镜的性能问题和美颜厂商的多样性,SDK 的自定义视频前处理功能采用面向对象设计,结合线程模型,帮助用户把外部处理视频数据的代码封装成可替换的滤镜组件。

自定义视频前处理一般在以下情况下使用: 当 SDK 自带的美颜无法满足需求,例如需要做挂件、贴纸,或者美颜效果无法达到预期时。

但是对于比较复杂的场景,例如想要用摄像头画面做图层混合,建议开发者使用视频外部采集功能实现,这样性能优化的空间会更大。

请注意:

1.SDK 会在适当的时机创建和销毁 ZegoVideoFilter,开发者无需担心生命周期不一致的问题。

2.开发者在外部调用 SDK 的切换摄像头的方法时,SDK 内部会调用 zego_stopAndDeAllocate 等方法以实现摄像头的切换,如果客户使用需要初始化多次的美颜资源(比如美颜资源和线程相关或者有上下文的),并在 zego_stopAndDeAllocate 中做了释放美颜资源的操作,在有切换摄像头的需求下,需要在 zego_allocateAndStart 中重新初始化美颜资源,否则会导致摄像头切换后画面卡顿;像贴纸类只需要初始化一次的美颜资源,建议放到构造滤镜时进行初始化(ZegoVideoFilterFactoryzego_create

2 下载示例 Demo

示例 Demo 源码下载地址:互动视频示例 Demo_iOS

码云下载地址(国内推荐)
阿里云下载地址(国内推荐)
github下载地址

互动视频示例 Demo 包含了基础专题和进阶专题,展示了如何使用 SDK API,开发者可参考其用法来实现自己的业务。互动视频示例 Demo 涵盖了 SDK 的大部分功能,开发者也可在示例专题 Demo 中查找 ZEGO SDK 更多的进阶功能,测试其功能和性能,以实现特定的需求。

自定义视频前处理模块源码请查看 /src/Topics/ExternalVideoFilter 以及 /src/LiveRoomPlayground-iOS/ExternalVideoFilterUI 目录下的源文件,该模块分为 VM 层和 UI 层,展示了使用 ZEGO SDK 实现自定义的视频前处理所需调用的关键 API 接口。

注意: 该示例专题 Demo 的自定义视频前处理模块集成了 FaceUnity 美颜,并且实现了 iOS 支持的四种滤镜类型的示例;若需要体验美颜效果,需要先从相芯科技申请美颜的鉴权证书,然后替换掉 /src/Topics/ExternalVideoFilter/FaceUnity-SDK-iOS 目录下的 authpack.h 文件。

相芯科技申请美颜鉴权证书的指引网址:http://www.faceunity.com/docs_develop/#/markdown/integrate/flow_io

3 支持的滤镜类型

为了实现传输不同数据模型,适配不同线程模型,同时避免实现多余接口,SDK 采用伪 COM 的设计方式。

开发者需要在 ZGVideoFilterFactoryDemo 子类中显式指定一种数据传递类型,SDK 目前支持的类型有:

滤镜类型 说明
ZegoVideoBufferTypeAsyncPixelBuffer 异步滤镜(异步传递 BGRA32 的 CVPixelBufferRef)
ZegoVideoBufferTypeSyncPixelBuffer 同步滤镜(同步传递 BGRA32 的 CVPixelBufferRef)
ZegoVideoBufferTypeAsyncI420PixelBuffer 异步 I420 滤镜(异步传递 I420 的 CVPixelBufferRef)
ZegoVideoBufferTypeAsyncNV12PixelBuffer 异步 NV12 滤镜(异步传递 NV12 的 CVPixelBufferRef)

SDK 会根据数据类型,实例化不同类型的 client,在调用 zego_allocateAndStart 时传给外部滤镜。

下面会按照不同的滤镜类型,分别介绍用法。

请注意:

如果外部滤镜没有明显的性能问题,使用同步滤镜可以减少数据拷贝的次数。 但如果比较耗时,请使用异步的方式传递数据,以保证低端机型可以流畅运行。

请开发者结合业务特点,选择合适的滤镜类型。

4 实现步骤

下面将以 ZegoVideoBufferTypeAsyncPixelBuffer (异步传递 BGRA32 图像数据)滤镜类型为例演示外部滤镜的用法,其余滤镜类型的外部滤镜实现示例请参考互动视频示例 Demo。

注意:以下步骤的示例代码和互动视频示例 Demo 的源码不一致,互动视频示例 Demo 中包含了 FaceUnity 美颜相关的调用。

实现 ZegoVideoBufferTypeAsyncPixelBuffer 滤镜类型的时序图如下:

4.1 创建外部滤镜

ZegoVideoFilter 定义最基本的组件功能,包括 zego_allocateAndStartzego_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 进行转型:

  1. ZegoVideoBufferTypeAsyncPixelBuffer: SDK 按照 ZegoVideoBufferPool 调用外部滤镜。
  2. ZegoVideoBufferTypeSyncPixelBuffer: SDK 按照 ZegoVideoFilterDelegate 调用外部滤镜。
  3. ZegoVideoBufferTypeAsyncI420PixelBuffer:SDK 按照 ZegoVideoBufferPool 调用外部滤镜,与 Async 型滤镜只是图像颜色空间有所区别。
  4. 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 请求外部滤镜返回 CVPixelBufferRef,拷贝原始数据

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;
    }
}

外部滤镜处理数据,结束后返回给 SDK

  1. 当 SDK 拷贝完数据给外部滤镜后,会调用 queueInputBuffer:timestamp: 方法通知外部滤镜。
  2. 外部滤镜处理完数据后,拷贝给 SDK 的流程和 SDK 调用外部滤镜的步骤类似:
    • App 调用 dequeueInputBuffer:height:stride: 向 SDK 请求 CVPixelBufferRef 作为拷贝目标
    • 执行拷贝
    • App 调用 queueInputBuffer:timestamp: 通知 SDK 拷贝完毕

外部滤镜应当按照约定的数据传递类型,切换线程,异步处理。

请注意,此处的演示代码没有做任何操作,只是在另一个线程进行数据拷贝。开发者应该按照各自业务需求,差异化实现该方法,也可参考互动视频示例 Demo 中使用 FaceUnity 的滤镜示例

// 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.hZGVideoFilterAsyncDemo.m 找到,具体细节不再赘述。

4.2 创建外部滤镜工厂

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

请注意:

  1. 大部分情况下,ZegoVideoFilterFactory 会缓存原有 ZegoVideoFilter 实例,开发者需避免创建新的实例。
  2. 开发者必须保证 ZegoVideoFiltercreatedestroy 之间是可用的,请勿直接销毁对象。

4.3 设置外部滤镜工厂

开发者需要使用外部滤镜功能时,请在使用前调用 setVideoFilterFactory:channelIndex: 设置外部滤镜工厂对象(此例中的对象为步骤 4.2 中所创建的 ZGVideoFilterFactoryDemo)。

请注意,如果用户释放了工厂对象,不再需要它时,请调用本接口将其设置为空。

ZGExternalVideoFilterDemo.h

@interface ZGExternalVideoFilterDemo : NSObject
+ (instancetype)shared;

/**
 初始化外部滤镜工厂对象

 @param type 视频缓冲区类型(Async, Sync, I420)
 @discussion 创建外部滤镜工厂对象后,先释放 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

4.4 初始化 SDK

请参考文档:快速开始-初始化

注意:必须在初始化 SDK 前设置外部滤镜工厂。

4.5 登录房间

请参考文档:快速开始-登录房间

4.6 推流

请参考文档:快速开始-推流

若对图像数据进行了前处理并将前处理后的数据回传给了 SDK,拉流时将是带美颜的图像。

5 API 参考列表

方法 描述
+ 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 退出房间

6 相关文档

7 Q&A

Q1: 提示缺少 FaceUnity 证书?

  答:若需要体验美颜效果,需要先从相芯科技申请美颜的鉴权证书,然后替换掉 /src/Topics/ExternalVideoFilter/FaceUnity-SDK-iOS 目录下的 authpack.h 文件。

Q2: 如何访问 CVPixelBufferRef 持有的图像数据?

  答:请参考 ZGImageUtilscopyPixelBufferFrom:to: 方法,然后对照苹果官方头文件。

Q3: ZegoVideoFilterFactory 的子类什么时候释放?

  答:我们推荐把工厂的实例保存为单例,仅作为 SDK 管理外部滤镜生命周期的通道,开发者可以为工厂子类添加 settergetter,一起管理滤镜的生命周期。