The Agora SDK uses default audio and video modules for capturing and rendering in real-time communications.
However, these default modules might not meet your development requirements, such as in the following scenarios:
This article describes how to use the Agora Native SDK to customize the video source and renderer.
Agora provides open-source demo projects on GitHub that implement the custom video source and renderer function. You can download the projects to try them out or view the source code:
The following diagram shows how the video data is transferred when you customize the video source or video renderer:
Video frames captured by the SDK or a custom video source, or received from a remote user, can be rendered by either the SDK or a custom video renderer.
push
mode: In this mode, you can call the pushExternalVideoFrame
method to push the captured video frames to the SDK.mediaIO
mode: In this mode, the SDK takes control of the capturing process with an AgoraVideoSourceProtocol
class, and then it stores the captured video frames in an AgoraVideoFrameConsumer
class. To send the captured frames to the SDK, you can call consumePixelBuffer
or consumeRawData
in the AgoraVideoFrameConsumer
class.AgoraVideoSinkProtocol
class to control the rendering process, and the setLocalVideoRenderer
and setRemoteVideoRenderer
methods to display the video of the local and remote view.push
mode differs from the mediaIO
mode in the following aspects:mediaIO
mode can.push
to SDK capture is not supported. To switch the video source directly, you must use the custom video capture by mediaIO
. See How can I switch from custom video capture to SDK capture.The Agora SDK provides the setExternalVideoSource
and pushExternalVideoFrame
methods to customize the video source. The API call sequence is as follows:
1. Enable the custom video source
Before joining a channel, call setExternalVideoSource
to enable the custom video source. Once you enable it, you cannot use the methods of the SDK to capture video frames.
// Swift
// Calls setExternalVideoSource to notify the SDK that the app uses the custom video source
agoraKit.setExternalVideoSource(true, useTexture: true, pushMode: true)
2. Implement the custom video source
Once the custom video source is enabled, you need to implement video capturing using APIs from outside the SDK. In the sample project, we define a class called AgoraCameraSourcePush
class that captures video frames using the native methods of the system.
// 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
}
}
// Initializes the custom video source
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")
// Displays the captured video frames on the view
let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
videoView.insertCaptureVideoPreviewLayer(previewLayer: previewLayer)
}
deinit {
captureSession.stopRunning()
}
// Starts capturing video frames
func startCapture(ofCamera camera: Camera) {
guard let currentOutput = currentOutput else {
return
}
// Sets the camera as the capturing device
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()
}
}
// Stops capturing video frames
func stopCapture() {
currentOutput?.setSampleBufferDelegate(nil, queue: nil)
captureQueue.async { [weak self] in
self?.captureSession.stopRunning()
}
}
// Switches the camera
func switchCamera() {
stopCapture()
currentCamera = currentCamera.next()
startCapture(ofCamera: currentCamera)
}
}
We also define a class called AgoraCameraSourcePushDelegate
to receive the captured video frames.
// Swift
protocol AgoraCameraSourcePushDelegate {
func myVideoCapture(_ capture: AgoraCameraSourcePush, didOutputSampleBuffer pixelBuffer: CVPixelBuffer, rotation: Int, timeStamp: CMTime)
}
3. Implement the custom video renderer
The Agora SDK does not support rendering video frames captured in the push mode. Therefore, you need to implement a custom video renderer using methods from outside the SDK. In the sample project, we define a class called CustomVideoSourcePreview
using the native AVCaptureVideoPreviewLayer
class.
// Swift
// Initializes localVideo
var localVideo = CustomVideoSourcePreview(frame: CGRect.zero)
// Defines the CustomVideoSourcePreview class
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. Start capturing and rendering video frames
In the sample project, we create a customCamera
instance using the AgoraCameraSourcePush
class, and then we call startCapture
to start the capturing and rendering process.
// Swift
// Initializes the AgoraCameraSourcePush class and sets the camera as the capturing device
customCamera = AgoraCameraSourcePush(delegate: self, videoView:localVideo)
// Calls startCapture of the AgoraCameraSourcePush class to start capturing video frames
customCamera?.startCapture(ofCamera: .defaultCamera())
5. Push the captured video frames to the SDK
Call the pushExternalVideoFrame
method to push the captured video frames to the 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)
// Pushes the captured video frames to the SDK
agoraKit?.pushExternalVideoFrame(videoFrame)
}
}
The Agora SDK provides the following classes in the mediaIO
interface:
AgoraVideoSourceProtocol
: Triggers callbacks that control video capturing.AgoraVideoSinkProtocol
: Triggers callbacks that control video rendering.AgoraVideoFrameConsumer
: Stores the captured video frames. In scenarios involving a custom video source, the AgoraVideoFrameConsumer
class sends the captured video frames to the SDK. In scenarios involving a custom video renderer, the AgoraVideoFrameConsumer
class sends the captured video frames to the renderer.This section introduces how to implement a custom video source and renderer with the mediaIO
interface.
The API call sequence for implementing the custom video source is as follows:
1. Create a customized video source instance
Create a customized video source instance with the AgoraVideoSourceProtocol
class, and set the following callback logics:
bufferType
, specify the format of the captured video frames in the return value.shouldInitialize
, initialize the custom video source, for example, by turning on the video capture device. The SDK maintains the AgoraVideoFrameConsumer instance contained in the callback.shouldStart
, send video frames to the SDK using consumePixelBuffer
or consumeRawData
. You can modify the video frame parameters in AgoraVideoFrameConsumer
according to your app scenario.shouldStop
, stop sending video frames to the SDK.shouldDispose
, release the custom video source, for example, by turning off the video capture device.In the sample project, we implement a class called AgoraCameraSourceMediaIO
based on AgoraVideoSourceProtocol
, and we manage the video-capturing process with the callbacks contained in AgoraVideoSourceProtocol
.
// Swift
// Uses AgoraVideoFrameConsumer to send and store captured video frames
var consumer: AgoraVideoFrameConsumer?
// Pushes the captured frames to the SDK
consumer?.consumePixelBuffer(pixelBuffer, withTimestamp: time, rotation: rotation)
extension AgoraCameraSourceMediaIO: AgoraVideoSourceProtocol {
// Initializes the custom video source after receiving shouldInitialize
func shouldInitialize() -> Bool {
return initialize()
}
// Starts the custom video source after receiving shouldStart
func shouldStart() {
startCapture()
}
// Stops the custom video source after receiving shouldStop
func shouldStop() {
stopCapture()
}
// Releases the AgoraVideoConsumer instance after receiving shouldDispose
func shouldDispose() {
dispose()
}
// Specifies the format of the captured video frames as pixel buffer after receiving bufferType
func bufferType() -> AgoraVideoBufferType {
return .pixelBuffer
}
// Sets the content hint of the captured video frames as default
func contentHint() -> AgoraVideoContentHint {
return .none
}
// Sets the capture type as camera after receiving captureType
func captureType() -> AgoraVideoCaptureType {
return .camera
}
}
2. Implement the custom video source
Refer to the following code samples when implementing the custom video source.
// Swift
private extension AgoraCameraSourceMediaIO {
// Initializes capturing video frames
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
}
// Starts capturing video frames
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()
}
}
// Stops capturing video frames
func stopCapture() {
currentOutput?.setSampleBufferDelegate(nil, queue: nil)
captureQueue?.async { [weak self] in
self?.captureSession?.stopRunning()
}
}
func dispose() {
captureQueue = nil
captureSession = nil
}
}
3. Call setVideoSource to set the custom video source
Call setVideoSource
of the Agora SDK. Once setVideoSource
is implemented, the shouldStart
callback triggers the customized video capturing and calls consumePixelBuffer
to send the captured frames to the SDK. After the SDK receives the captured video frames, you can call startPreview
or setupLocalVideo
to render the video frames.
// Swift
// Assigns AgoraCameraSourceMediaIO to customCamera
fileprivate let customCamera = AgoraCameraSourceMediaIO()
// Calls setVideoSource and passes customCamera to the SDK
agoraKit.setVideoSource(customCamera)
The API call sequence for implementing the custom video renderer is as follows:
1. Implement the custom video renderer
Implement AgoraVideoSinkProtocol
and AgoraVideoFrameConsumer
, and set the following callback logics:
bufferType
and pixelFormat
, specify the format of rendered video frames in the return value.shouldInitialize
, shouldStart
, shouldStop
, and shouldDispose
callbacks.renderPixelBuffer
or renderRawData
according to the video-frames format that you set in bufferType
or pixelFormat
.In the sample project, we implement a class called AgoraMetalRender
based on AgoraVideoSinkProtocol
, and we control the video-rendering process with the callbacks in AgoraVideoSinkProtocol
.
// Swift
extension AgoraMetalRender: AgoraVideoSinkProtocol {
// Initializes the custom video renderer after receiving shouldInitialize
func shouldInitialize() -> Bool {
initializeRenderPipelineState()
return true
}
// Starts rendering video frames after receiving shouldStart
func shouldStart() {
#if os(iOS) && (!arch(i386) undefined (!arch(i386) undefined (!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. Call setLocalVideoRenderer to set the custom video renderer
Call setLocalVideoRenderer
of the SDK to display the video in the local view.
// Swift
// Sets the local video renderer
if let customRender = localVideo.videoView {
agoraKit.setLocalVideoRenderer(customRender)
}
renderPixelBuffer
or renderRawData
may not be 0. This is probably due to the settings of video capturing, and you need to process the rotation information yourself.