实时音视频传输过程中,声网 SDK 通常会启动默认的音视频模块进行采集和渲染。在以下场景中,你可能会发现默认的视频模块无法满足开发需求:
本文介绍如何使用声网提供的方法实现自定义的视频采集与渲染。
声网在 GitHub 上提供了一个开源的 API-Example 示例项目,包含了实现自定义视频采集和渲染功能的示例:
下图分别展示了自定义采集与自定义渲染视频场景下的数据流转过程。
无论是 SDK 采集,还是自定义采集的视频数据,或是 SDK 接收到的远端视频数据,都可以直接发送给 SDK 进行渲染,或使用自定义渲染。其中:
Push
方式和 mediaIO
方式将采集到的视频帧发送回 SDK。Push
方式下,你可以直接使用 SDK 的 pushExternalVideoFrame
方法将采集到的视频帧推送给 SDK;mediaIO
方式下,SDK 通过 AgoraVideoSourceProtocol
协议来控制整个采集过程,并使用一个 AgoraVideoFrameConsumer
类来存储采集到的视频帧。你可以调用 consumePixelBuffer
或 consumeRawData
将采集到的视频数据发送给 SDK。mediaIO
接口还支持通过 AgoraVideoSinkProtocol
协议来控制整个渲染过程,你可以调用 setLocalVideoRenderer
来显示自渲染后的视频数据。Push
方式和 mediaIO
方式实现视频自采集有以下区别:Push
方式自采集的视频数据,不能直接使用 SDK 渲染;而 mediaIO
方式采集的视频数据可以。Push
方式自定义的视频源不支持在频道内切换到 SDK 采集。你必须使用 mediaIO
方式实现视频源的直接切换。详见如何从视频自采集切换到 SDK 采集。声网提供了 setExternalVideoSource
和 pushExternalVideoFrame
方法来实现自定义视频采集。API 调用时序如下图所示:
1. 开启自定义视频采集
在加入频道前调用 setExternalVideoSource
开启自定义视频采集。一旦开启后,你将无法使用 SDK 提供的 API 进行采集。
// Swift
// 调用声网的 setExternalVideoSource 方法,告知 SDK 使用自采集的视频数据。
agoraKit.setExternalVideoSource(true, useTexture: true, pushMode: true)
2. 自行实现自采集模块
启用自定义视频采集后,你需要自行管理视频的采集。因此我们要自行实现这几个功能。在示例项目中,我们定义了一个 AgoraCameraSourcePush
类,并通过系统方法来管理视频采集。
// Swift
class AgoraCameraSourcePush: NSObject {
fileprivate var delegate: AgoraCameraSourcePushDelegate?
private var videoView: CustomVideoSourcePreview
private var currentCamera = Camera.defaultCamera()
private let captureSession: AVCaptureSession
private let captureQueue: DispatchQueue
private var currentOutput: AVCaptureVideoDataOutput? {
if let outputs = self.captureSession.outputs as? [AVCaptureVideoDataOutput] {
return outputs.first
} else {
return nil
}
}
// 初始化视频采集
init(delegate: AgoraCameraSourcePushDelegate?, videoView: CustomVideoSourcePreview) {
self.delegate = delegate
self.videoView = videoView
captureSession = AVCaptureSession()
captureSession.usesApplicationAudioSession = false
let captureOutput = AVCaptureVideoDataOutput()
captureOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange]
if captureSession.canAddOutput(captureOutput) {
captureSession.addOutput(captureOutput)
}
captureQueue = DispatchQueue(label: "MyCaptureQueue")
// 将采集到的画面在图层上进行渲染
let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
videoView.insertCaptureVideoPreviewLayer(previewLayer: previewLayer)
}
deinit {
captureSession.stopRunning()
}
// 开始采集
func startCapture(ofCamera camera: Camera) {
guard let currentOutput = currentOutput else {
return
}
// 设置采集设备为当前的摄像头
currentCamera = camera
currentOutput.setSampleBufferDelegate(self, queue: captureQueue)
captureQueue.async { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.changeCaptureDevice(toIndex: camera.rawValue, ofSession: strongSelf.captureSession)
strongSelf.captureSession.beginConfiguration()
if strongSelf.captureSession.canSetSessionPreset(AVCaptureSession.Preset.vga640x480) {
strongSelf.captureSession.sessionPreset = AVCaptureSession.Preset.vga640x480
}
strongSelf.captureSession.commitConfiguration()
strongSelf.captureSession.startRunning()
}
}
// 停止采集
func stopCapture() {
currentOutput?.setSampleBufferDelegate(nil, queue: nil)
captureQueue.async { [weak self] in
self?.captureSession.stopRunning()
}
}
// 切换采集的摄像头
func switchCamera() {
stopCapture()
currentCamera = currentCamera.next()
startCapture(ofCamera: currentCamera)
}
}
再定义一个 AgoraCameraSourcePushDelegate
类来接收采集到的视频数据。
// Swift
protocol AgoraCameraSourcePushDelegate {
func myVideoCapture(_ capture: AgoraCameraSourcePush, didOutputSampleBuffer pixelBuffer: CVPixelBuffer, rotation: Int, timeStamp: CMTime)
}
3. 自行定义自渲染模块
由于 SDK 不支持渲染使用 Push
方式采集到的视频数据,因此你需要自行实现。在示例项目中,我们基于系统原生的 AVCaptureVideoPreviewLayer
类定义了一个 CustomVideoSourcePreview
类。
// Swift
// 初始化 localVideo
var localVideo = CustomVideoSourcePreview(frame: CGRect.zero)
// 定义 CustomVideoSourcePreview 类
class CustomVideoSourcePreview : UIView {
private var previewLayer: AVCaptureVideoPreviewLayer?
func insertCaptureVideoPreviewLayer(previewLayer: AVCaptureVideoPreviewLayer) {
self.previewLayer?.removeFromSuperlayer()
previewLayer.frame = bounds
layer.insertSublayer(previewLayer, below: layer.sublayers?.first)
self.previewLayer = previewLayer
}
override func layoutSublayers(of layer: CALayer) {
super.layoutSublayers(of: layer)
previewLayer?.frame = bounds
}
}
4. 开始自定义的视频采集
在示例代码中,我们基于自定义的 AgoraCameraSourcePush
类创建了一个 customCamera
对象,并调用 startCapture
方法开始视频采集。
// Swift
// 实例化一个 AgoraCameraSourcePush 类,将设备的摄像头指定为采集设备。
customCamera = AgoraCameraSourcePush(delegate: self, videoView:localVideo)
// 调用 AgoraCameraSourcePush 类中的 startCapture,开始视频采集
customCamera?.startCapture(ofCamera: .defaultCamera())
5. 将接收到的视频数据推送给 SDK
调用 SDK 的 pushExternalVideoFrame
方法将采集到的视频数据推送给 SDK。
// Swift
extension CustomVideoSourcePushMain:AgoraCameraSourcePushDelegate
{
func myVideoCapture(_ capture: AgoraCameraSourcePush, didOutputSampleBuffer pixelBuffer: CVPixelBuffer, rotation: Int, timeStamp: CMTime) {
let videoFrame = AgoraVideoFrame()
videoFrame.format = 12
videoFrame.textureBuf = pixelBuffer
videoFrame.time = timeStamp
videoFrame.rotation = Int32(rotation)
// 获取到视频数据后,调用方法将数据推送给 SDK
agoraKit?.pushExternalVideoFrame(videoFrame)
}
}
SDK 通过 mediaIO
接口提供:
AgoraVideoSourceProtocal
协议,通过回调方式控制自定义视频的采集过程。AgoraVideoSinkProtocal
协议,通过回调方式控制自定义视频的渲染过程。AgoraVideoFrameConsumer
类,存储采集到的视频数据。在自定义视频采集场景中,该类负责将采集到的视频数据发送给 SDK;在自定义视频渲染场景中,该类负责将获取的视频数据发送给自渲染模块。下文分别展示如何通过 mediaIO
接口实现视频自采集与自渲染。
使用 mediaIO
接口自定义视频采集的 API 调用时序如下图所示:
1. 实现一个自定义的 Video source 类
遵守 AgoraVideoSourceProtocol
协议,实现一个自定义的视频采集的类。并在回调中设置以下逻辑:
bufferType
回调后,在该回调的返回值中指定想要采集的视频数据格式。shouldInitialize
回调后,初始化自定义的视频源,如打开视频采集设备。SDK 会保存该回调中的 AgoraVideoFrameConsumer
实例。shouldStart
回调后,通过 consumePixelBuffer
或 consumeRawData
向 SDK 发送视频数据。为满足实际使用需求,你可以在将视频帧发送回 SDK 前,修改 AgoraVideoFrameConsumer
中的视频帧参数,如 rotation
,SDK 会按照传递的 rotation
信息旋转画面。shouldStop
回调后,停止 AgoraVideoFrameConsumer
向 SDK 发送视频数据。shouldDispose
回调后,释放自定义的视频源,如关闭视频采集设备。在示例项目中,我们定义了一个 AgoraCameraSourceMediaIO
类,并在接收到协议中的回调时,调用自定义的方法,控制视频采集的过程。
// Swift
// 通过 AgoraVideoFrameConsumer 对象发送和接收自定义的视频数据
var consumer: AgoraVideoFrameConsumer?
// 将接收到的视频数据推送给 SDK
consumer?.consumePixelBuffer(pixelBuffer, withTimestamp: time, rotation: rotation)
extension AgoraCameraSourceMediaIO: AgoraVideoSourceProtocol {
// 收到 shouldInitialize 回调后初始化视频采集
func shouldInitialize() -> Bool {
return initialize()
}
// 收到 shouldStart 回调后开始采集
func shouldStart() {
startCapture()
}
// 收到 shouldStop 回调后停止采集
func shouldStop() {
stopCapture()
}
// 收到 shouldDispose 回调后,释放 Consumer 对象
func shouldDispose() {
dispose()
}
// 收到 bufferType 回调后设置采集的视频数据格式为 pixel buffer
func bufferType() -> AgoraVideoBufferType {
return .pixelBuffer
}
// 收到 contentHint 回调后设置采集的视频内容为默认
func contentHint() -> AgoraVideoContentHint {
return .none
}
// 收到 captureType 后设置采集的视频类型为摄像头采集
func captureType() -> AgoraVideoCaptureType {
return .camera
}
}
2. 自行实现自采集模块
如下示例代码展示了如何调用系统原生方法实现自定义的视频采集。
// Swift
private extension AgoraCameraSourceMediaIO {
// 初始化视频采集
func initialize() -> Bool {
let captureSession = AVCaptureSession()
captureSession.usesApplicationAudioSession = false
let captureOutput = AVCaptureVideoDataOutput()
captureOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange]
if captureSession.canAddOutput(captureOutput) {
captureSession.addOutput(captureOutput)
}
self.captureSession = captureSession
captureQueue = DispatchQueue(label: "Agora-Custom-Video-Capture-Queue")
return true
}
// 开始采集
func startCapture() {
guard let currentOutput = currentOutput, let captureQueue = captureQueue else {
return
}
currentOutput.setSampleBufferDelegate(self, queue: captureQueue)
captureQueue.async { [weak self] in
guard let strongSelf = self, let captureSession = strongSelf.captureSession else {
return
}
strongSelf.changeCaptureDevice(toPosition: strongSelf.position, ofSession: captureSession)
captureSession.beginConfiguration()
if captureSession.canSetSessionPreset(.vga640x480) {
captureSession.sessionPreset = .vga640x480
}
captureSession.commitConfiguration()
captureSession.startRunning()
}
}
// 停止采集
func stopCapture() {
currentOutput?.setSampleBufferDelegate(nil, queue: nil)
captureQueue?.async { [weak self] in
self?.captureSession?.stopRunning()
}
}
func dispose() {
captureQueue = nil
captureSession = nil
}
}
3. 调用 setVideoSource 设置自定义的视频源
调用 SDK 的 setVideoSource
方法设置自定义的视频源。执行 setVideoSource
后,shouldStart
回调会触发视频自采集,并调用 consumePixelBuffer
将采集到的视频数据发送给 SDK。SDK 接收到自定义采集的视频数据后,可以直接调用 startPreview
或 setupLocalVideo
进行渲染。
// Swift
// 将 AgoraCameraSourceMediaIO 作为自定义的 video source 赋值给 customCamera
fileprivate let customCamera = AgoraCameraSourceMediaIO()
// 调用 SDK 的 setVideoSource 方法,然后将 customCamera 作为该方法的参数,设置给 AgoraRtcEngineKit
agoraKit.setVideoSource(customCamera)
使用 mediaIO 接口自定义视频渲染的 API 调用时序如下图所示:
1. 实现一个自定义的 video render 类
实现 AgoraVideoSinkProtocol
协议和 AgoraVideoFrameConsumer
类,并在回调中设置以下逻辑:
bufferType
和 pixelFormat
回调后,根据需要返回的视频数据类型选择对应的回调。shouldInitialize
、shouldStart
、shouldStop
、shouldDispose
回调,控制视频数据的渲染过程。bufferType
和 pixelFormat
中设置的返回值,触发对应的 renderPixelBuffer
或 renderRawData
回调,渲染接收到视频帧。在示例项目中,我们基于 AgoraVideoSinkProtocol
协议实现一个 AgoraMetalRender
类,并在收到协议包含的回调时,设置想要渲染的数据类型、并控制整个渲染过程。
// Swift
extension AgoraMetalRender: AgoraVideoSinkProtocol {
// 收到 shouldInitialize 回调后初始化视频渲染
func shouldInitialize() -> Bool {
initializeRenderPipelineState()
return true
}
// 收到 shouldStart 回调后开始渲染
func shouldStart() {
#if os(iOS) && (!arch(i386) && !arch(x86_64))
metalView.delegate = self
#endif
}
// 收到 shouldStop 回调后停止渲染
func shouldStop() {
#if os(iOS) && (!arch(i386) && !arch(x86_64))
metalView.delegate = nil
#endif
}
func shouldDispose() {
textures = nil
}
// 收到 bufferType 后设置渲染的数据类型为 pixel buffer
func bufferType() -> AgoraVideoBufferType {
return .pixelBuffer
}
// 收到 pixelFormat 后设置 pixel 格式为 NV12
func pixelFormat() -> AgoraVideoPixelFormat {
return .NV12
}
// 调用 renderPixelBuffer 方法渲染视频数据,并定义详细的渲染方法
func renderPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: AgoraVideoRotation) {
#if os(iOS) || (os(macOS)) && (!arch(i386) && !arch(x86_64))
guard CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) == kCVReturnSuccess else {
return
}
defer {
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
}
let isPlanar = CVPixelBufferIsPlanar(pixelBuffer)
let width = isPlanar ? CVPixelBufferGetWidthOfPlane(pixelBuffer, 0) : CVPixelBufferGetWidth(pixelBuffer)
let height = isPlanar ? CVPixelBufferGetHeightOfPlane(pixelBuffer, 0) : CVPixelBufferGetHeight(pixelBuffer)
let size = CGSize(width: width, height: height)
let mirror = mirrorDataSource?.renderViewShouldMirror(renderView: self) ?? false
if let renderedCoordinates = rotation.renderedCoordinates(mirror: mirror,
videoSize: size,
viewSize: viewSize) {
let byteLength = 4 * MemoryLayout.size(ofValue: renderedCoordinates[0])
vertexBuffer = device?.makeBuffer(bytes: renderedCoordinates, length: byteLength, options: [])
}
if let yTexture = texture(pixelBuffer: pixelBuffer, textureCache: textureCache, planeIndex: 0, pixelFormat: .r8Unorm),
let uvTexture = texture(pixelBuffer: pixelBuffer, textureCache: textureCache, planeIndex: 1, pixelFormat: .rg8Unorm) {
self.textures = [yTexture, uvTexture]
}
#endif
}
}
2. 调用 setLocalVideoRenderer 设置自定义的渲染源
调用 setLocalVideoRenderer
,设置自定义的本地视频渲染器,将视频数据渲染到视图上。
// Swift
// 设置本地视频渲染
if let customRender = localVideo.videoView {
agoraKit.setLocalVideoRenderer(customRender)
}
自定义视频采集和渲染场景中,需要开发者具有采集或渲染视频的能力:
自定义视频渲染场景中,当 renderPixelBuffer
或 renderRawData
报告 rotation
不为 0 时,自渲染视频会呈一定角度。该角度可能由 SDK 采集或自采集的设置引起,你需要能根据实际使用需求处理自渲染的视频角度。
在 iOS 上,使用 MediaIO 自采集 BGRA 格式的 CVPixelBuffer 时,如果视频渲染是通过自渲染而非 SDK 渲染,请确保你的视频渲染器适配了 BGRA 格式,否则可能出现黑屏闪屏等问题。