视频外部采集

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

1 常用场景

当开发者业务中出现以下情况时,我们推荐使用 SDK 的外部采集功能:

  1. 普通摄像头的采集无法满足需求。例如,包含了大量的原有业务。
  2. 直播过程中,开发者需要使用摄像头完成的额外功能和 SDK 的默认逻辑有冲突,导致摄像头无法正常使用。例如,直播到一半,需要录制短视频。
  3. 直播非摄像头数据。例如视频播放、屏幕分享、游戏直播等。
  4. 开发者可以使用外部采集来做自定义美颜等视频预处理。

2 功能简介

考虑到设备的独占问题,SDK 的视频外部采集采用的是面向对象的设计,帮助用户把原有采集代码封装成外部采集设备。

开发者通过实现 ZegoVideoCaptureFactoryZegoVideoCaptureDevice 协议,可以把外部采集的数据传给 SDK 进行编码推流:

  1. ZegoVideoCaptureFactory 是外部采集的入口,定义了创建、销毁 ZegoVideoCaptureDevice 的接口,向 SDK 提供管理 ZegoVideoCaptureDevice 生命周期的能力。需要调用 setVideoCaptureFactory 的地方必须实现该接口。
  2. ZegoVideoCaptureDevice 定义基本的组件能力,包括 allocateAndStartstopAndDeAllocatestartCapturestopCapturezego_supportBufferType,方便 SDK 在直播流程中进行交互。

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

3 实现步骤

外部采集的接口调用流程如下图所示:

完整示例demo展示了如何通过外部采集工厂和外部采集设备来使用外部采集功能并进行推流、拉流的操作,demo里包含了采用摄像头、图片、屏幕作为外部采集源的示例。

示例中, ZGExternalVideoCaptureBaseSource 负责获取采集的视频数据 , ZGExternalVideoCaptureManager 实现了 ZegoVideoCaptureFactoryZegoVideoCaptureDevice 协议,负责向 SDK 提供 ZGExternalVideoCaptureSource 采集到的视频数据,当没有对应数据源时,Manager 便不会向 SDK 提供数据,但是并不会被销毁。这样的优点是可以自由切换 Source 而不会导致推流中断。

3.1 外部采集工厂实现

在 SDK 回调 zego_create 时,创建并返回 ZegoVideoCaptureDevice 实例。 下述代码展示了如何创建外部采集工厂(工厂自身实现了 ZegoVideoCaptureDevice ,所以返回自己)。

@implementation ZGExternalVideoCaptureManager

/**
  SDK 会在 App 预览 / 推流 / 拉流时初始化采集工厂 。(如果已经初始化过,则不会多次重复调用)
*/
- (nonnull id<ZegoVideoCaptureDevice>)zego_create:(nonnull NSString*)deviceId {
    return self;
}

/**
  SDK 会在 App 预览/推流 / 拉流全部结束后销毁采集工厂。
*/
- (void)zego_destroy:(nonnull id<ZegoVideoCaptureDevice>)device {
    [self zego_stopAndDeAllocate];
}

@end

请注意:

  1. 大部分情况下,ZegoVideoCaptureFactory 会缓存 ZegoVideoCaptureDevice实例,开发者需避免创建新的实例,造成争抢独占设备(例如摄像头)。
  2. 开发者必须保证 ZegoVideoCaptureDevice 在采集工厂 zego_createzego_destroy 之间是可用的,请勿直接销毁对象。

3.2 外部采集设备实现

ZGExternalVideoCaptureManager 自身实现了 ZegoVideoCaptureDevice 协议,具体实现如下。

@interface ZGExternalVideoCaptureManager () <ZegoVideoCaptureDevice>

@property (strong, nonatomic, nullable) ZGExternalVideoCaptureBaseSource *source;
@property (strong, nonatomic) id<ZegoVideoCaptureClientDelegate> client;

@property (assign, nonatomic) BOOL isPreview;
@property (assign, nonatomic) BOOL isPublish;

@end

@implementation ZGExternalVideoCaptureManager

- (void)setSourceType:(ZGExternalVideoCaptureSourceType)sourceType {
    ...

    self.source = [self sourceForType:self.sourceType];
    // 通过代理接收Source采集的视频帧数据
    self.source.receiver = self;

    ...
}

- (void)startCapture {
    [self.source start];
}

- (void)stopCapture {
    [self.source stop];
}

- (BOOL)isCapture {
    return self.isPublish || self.isPreview;
}

/** ------ 下方为ZegoVideoCaptureDevice协议实现 ------ */

/** 
  开发者初始化采集设备,获取 client 并保存该实例,用于通知 SDK 采集结果。
*/
- (void)zego_allocateAndStart:(nonnull id<ZegoVideoCaptureClientDelegate>)client {
    self.client = client;
    [self.client setFillMode:ZegoVideoFillModeCrop];
}

/**
  开发者销毁采集设备, client 实例在 `zego_stopAndDeAllocate` 被调用前必须一直保存。
*/
- (void)zego_stopAndDeAllocate {
    [self stopCapture];

    [self.client destroy];
    self.client = nil;
}

/**
  SDK 开始预览时,会回调 `zego_startPreview` 通知外部采集设备开始工作。
  开发者需要开始采集数据并传递给 SDK 的 client 对象。
*/
- (int)zego_startPreview {
    self.isPreview = YES;
    [self startCapture];
    return 0;
}

/**
  SDK结束预览时,会回调 `zego_stopPreview` 。
  但此时是否需要结束采集还需要判断推流状态。
*/
- (int)zego_stopPreview {
    self.isPreview = NO;
    if (self.isCapture) {
        [self stopCapture];
    }
    return 0;
}

/**
  SDK开始推流时,会回调 `zego_startCapture` 通知外部采集设备开始工作。
  开发者需要开始采集数据并传递给 SDK 的 client 对象。
*/
- (int)zego_startCapture {
    self.isPublish = YES;
    [self startCapture];
    return 0;
}

/**
  SDK结束推流时,会回调 `zego_stopCapture` 。
  但此时是否需要结束采集还需要判断预览状态。
*/
- (int)zego_stopCapture {
    self.isPublish = NO;
    if (self.isCapture) {
        [self stopCapture];
    }
    return 0;
}

@end

告知SDK当前采集数据的类型

由于 iOS 采集的多样性,SDK 支持多种外部采集数据格式,所以开发者需要告知 SDK 当前采集设备使用何种数据类型。目前 SDK 支持的类型有:

类型定义 类型说明
ZegoVideoCaptureDeviceOutputBufferTypeCVPixelBuffer CVImageBufferRef
ZegoVideoCaptureDeviceOutputBufferTypeGlTexture2D GLuint
ZegoVideoCaptureDeviceOutputBufferTypeEncodedFrame EncodedFrame,编码后的采集数据

SDK默认为 ZegoVideoCaptureDeviceOutputBufferTypeCVPixelBuffer 。如果需要使用其他数据类型,需要实现 ZegoVideoCaptureDevice 的相应方法,然后在获取视频帧数据后调用 client 对应数据类型的接收方法。

/**
 支持的 buffer 类型
 如果不实现,则为 ZegoVideoCaptureDeviceOutputBufferTypeCVPixelBuffer
*/
- (ZegoVideoCaptureDeviceOutputBufferType)zego_supportBufferType {
    return ZegoVideoCaptureDeviceOutputBufferTypeCVPixelBuffer;
}

3.3 ZGExternalVideoCaptureSource实现示例

ZGExternalVideoCaptureSource 负责获取采集的视频数据 ,并将其传给 ZGExternalVideoCaptureDataReceiver ,具体实现可参考代码。

@protocol ZGExternalVideoCaptureSourceDelegate <NSObject>
@property (nonatomic, weak) id<ZGExternalVideoCaptureDataReceiver> receiver;
- (BOOL)start;
- (void)stop;
@end

@interface ZGExternalVideoCaptureCameraSource () <AVCaptureVideoDataOutputSampleBufferDelegate>

@property (strong, nonatomic) AVCaptureDeviceInput *input;
@property (strong, nonatomic) AVCaptureVideoDataOutput *output;
@property (strong, nonatomic) AVCaptureSession *session;

@end


@implementation ZGExternalVideoCaptureCameraSource

- (void)dealloc {
    [self stop];
}

// 开始采集
- (BOOL)start {
    ...

    [self.session startRunning];

    return YES;
}

// 停止采集
- (void)stop {
    ...

    [self.session stopRunning];

    ...
}

#pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    ...

    // 将采集到的视频帧数据及时间戳传给Manager
    [self.receiver capturedData:buffer presentationTimeStamp:timeStamp];
}

@end

3.4 接收Source采集的视频帧数据

ZGExternalVideoCaptureManager 实现 ZGExternalVideoCaptureDataReceiver ,接收视频帧数据,并调用 client 对应数据类型的接收方法传给 SDK。

- (void)capturedData:(CVImageBufferRef)image presentationTimeStamp:(CMTime)time {
    [self.client onIncomingCapturedData:image withPresentationTimeStamp:time];
}

3.5 设置外部采集工厂

开发者需要使用外部采集功能时,请在初始化 SDK 前设置外部采集工厂对象。

setVideoCaptureFactory 必须在 InitSDK 前调用,需要使用外部采集功能时,该工厂对象不能为空。

- (void)setUseExternalVideoCapture:(BOOL)useExternalVideoCapture {}
    if (useExternalVideoCapture) {
        // 初始化Factory实例
        id<ZegoVideoCaptureFactory> factory = [ZGExternalVideoCaptureManager new];
        // 设置开启外部采集
        [ZegoExternalVideoCapture setVideoCaptureFactory:factory channelIndex:ZEGOAPI_CHN_MAIN];
    } else {
        // 将factory置空
        [ZegoExternalVideoCapture setVideoCaptureFactory:nil channelIndex:ZEGOAPI_CHN_MAIN];
    }

    ...

    //初始化SDK
    _api = [[ZegoLiveRoomApi alloc] initWithAppID:appid appSignature:sign];
}

请注意

  1. 如果用户释放了工厂对象,不再需要它时,请调用本接口将其设置为空。
  2. 若客户端使用开关控制是否使用外部采集,在其改变 useVideoCapture 的布尔值之后需要先反初始化 SDK(unInitSDK),再重新执行外部采集的设置。

5 Q&A

  • Q1:如何访问 CVPixelBufferRef 持有的图像内存?

  • A1:请参考 ZegoLiveRoomApicopyPixelBufferFrom:to: 方法,然后对照苹果官方头文件。

  • Q2:如何使用 ZegoVideoCaptureDeviceOutputBufferTypeGlTexture2D 方式传递采集数据?

  • A2:选择 ZegoVideoCaptureDeviceOutputBufferTypeGlTexture2D 方式时,开发者实现 zego_supportBufferType 返回 ZegoVideoCaptureDeviceOutputBufferTypeGlTexture2D , 然后调用 clientonIncomingCapturedData:width:height:withPresentationTimeStamp: 传递数据。

  • Q3:如何使用 ZegoVideoCaptureDeviceOutputBufferTypeEncodedFrame 方式传递编码数据?

  • A3:选择 ZegoVideoCaptureDeviceOutputBufferTypeEncodedFrame 方式时,开发者实现 zego_supportBufferType 返回 ZegoVideoCaptureDeviceOutputBufferTypeEncodedFrame , 可以调用 clientonEncodedFrame:config:height:bKeyframe:withPresentationTimeStamp 传递数据。目前只支持H.264码流,关键帧间隔推荐2秒。

    请注意,每个IDR帧都必须携带SPS和PPS,放在最前面。

  • Q4:ZegoSupportsVideoCapture 的方法需要实现么?

  • A4:对于 ZegoSupportsVideoCapture 所包含的方法,不要求开发者实现,仅仅只是为了方便开发者在自己的采集实现和 SDK 默认的采集实现间切换,避免业务层写两套逻辑代码。

每个方法都有对应的 SDK 接口,调用 SDK 接口时,会透传调用对应的方法。比如调用 SDK 的 startPreview 时,会透传调用 ZegoVideoCaptureDevicestartPreview 方法。

  • Q5:ZegoVideoCaptureFactory 的子类什么时候释放?

  • A5:我们推荐把工厂的实例保存为单例,仅作为 SDK 管理外部采集设备生命周期的通道,开发者可以为工厂子类添加 setter 和 getter,一起管理采集类的生命周期。

  • Q6:使用外部采集,本地预览的画面正常,推出去观众端看到的画面变形了?

  • A6:外部采集进来的图像比例和 SDK 默认设置的分辨率的比例不一致(比如外部采集进来的是 4:3 ,SDK 默认推流分辨率 360*640 是 16:9)。解决方案:

    1. 开发者将外部采集的视频分辨率比例修改为 16:9
    2. 开发者调用 setAvConfig ,将 SDK 的推流分辨率自定义为 4:3(比如480*640)
  • Q7:开启外部采集后,setPreviewViewstartPreview无效?

  • A7:SDK开启外部采集,外部采集的数据,目前需要用户自己渲染。

  • Q8:开启外部采集后,外部采集的帧率和拉流播放帧率不一致?

  • A8:通过设置SDK的帧率 setVideoFPS 和外部采集调用 OnIncomingCapturedData 的频率一致。

  • Q9:SDK 接收视频帧数据方法内部对传入的数据是同步处理还是异步处理?

  • A9:SDK 接收视频帧数据后,会先同步拷贝数据,然后再异步执行编码等操作,所以在将数据传入 SDK 后即可立即释放。