实时音视频传输过程中,声网 SDK 通常会启动默认的音视频模块进行采集和渲染。在以下场景中,你可能会发现默认的音视频模块无法满足开发需求:
基于此,声网 RTC Native SDK 支持使用自定义的音视频源或渲染器,实现相关场景。本文介绍如何实现自定义视频采集和渲染。具体实现包括 Push 和 mediaIO 两种自定义视频采集方式:
setExternalVideoSource
指定 SDK 之外的视频源。你需要使用自采集模块驱动采集设备对视频进行采集,采集的视频帧通过 pushVideoFrame
发送给 SDK 。你可以使用自渲染模块对采集的视频进行渲染。 setVideoSource
指定视频源,通过调用 consumeRawVideoFrame
读取自采集模块的视频帧并将视频帧发送给 SDK。你可以使用自渲染模块对采集的视频进行渲染。我们在 GitHub 上提供以下开源的示例项目。
你可以前往下载,或查看其中的源代码。
开始自定义采集和渲染前,请确保你已在项目中实现基本的通话或者直播功能,详见一对一通话或互动直播。
参考如下步骤,在你的项目中实现自定义采集和渲染功能:
joinChannel
前通过调用 setExternalVideoSource
指定自定义视频源。pushVideoFrame
发送给 SDK 进行后续操作。参考下图时序在你的项目中自定义视频采集。
下图展示了 Push 方式的自采集与自渲染情况下的视频数据流转:
setExternalVideoSource
指定视频源。pushVideoFrame
向 SDK 发送采集的视频帧。示例代码同时实现了自采集和自渲染功能。
setExternalVideoSource
指定 SDK 之外的视频源。// 指定自定义视频源
BOOL CAgoraCaptureVideoDlg::EnableExtendVideoCapture(BOOL bEnable)
{
// 创建使用 IMediaEngine 类为 template 的 AutoPtr 实例
agora::util::AutoPtr<agora::media::IMediaEngine> mediaEngine;
// 你可以在 SDK 的 AgoraBase.h 中参考 AutoPtr 类的实现
// AutoPtr 实例调用 queryInterface 方法,通过 IID 获取 IMediaEngine 实例的指针。
// AutoPtr 实例会通过箭头操作符访问 IMediaEngine 实例的指针并通过 IMediaEngine 实例调用 setExternalVideoSource。
// m_rtcEngine 是 IRtcEngine 对象的实例
mediaEngine.queryInterface(m_rtcEngine, agora::AGORA_IID_MEDIA_ENGINE);
int nRet = 0;
if (mediaEngine.get() == NULL)
return FALSE;
if (bEnable) {
// 指定自定义视频源
nRet = mediaEngine->setExternalVideoSource(true, false);
}
else {
// 取消自定义视频源
nRet = mediaEngine->setExternalVideoSource(false, false);
}
return nRet == 0 ? TRUE : FALSE;
}
ExternalVideoFrame
实例设置视频帧属性和 setVideoEncoderConfiguration
设置视频编码属性,并开始视频自采集。// 设置本地视频的编码属性并开始视频自采集
void CAgoraCaptureVideoDlg::EnableCaputre(BOOL bEnable)
{
if (bEnable == (BOOL)!m_extenalCaptureVideo)return;
// 从 UI 获取采集设备类型
int nIndex = m_cmbVideoType.GetCurSel();
if (bEnable)
{
// 根据 UI 的采集设备类型选择视频采集设备。m_agVideoCaptureDevice 是自行实现的自采集模块中的实例。
m_agVideoCaptureDevice.SelectMediaCap(nIndex == -1 ? 0 : nIndex);
// 创建 VIDEOINFOHEADER 实例(此处使用了 MFC 中 Windows Media Device Manager SDK 中的类)
VIDEOINFOHEADER videoInfo;
// 创建 VideoEncoderConfiguration 实例。
VideoEncoderConfiguration config;
// 创建视频采集过滤器
m_agVideoCaptureDevice.CreateCaptureFilter();
// 获取选择的视频采集设备
m_agVideoCaptureDevice.GetCurrentVideoCap(&videoInfo);
// 下列参数由 videoInfo 中的 bmiHeader 参数得到。bmiHeader 是 Win32 _BITMAPINFOHEADER 结构,包含视频图像 bitmap 的颜色和维度信息。
// 设置 setVideoEncoderConfiguration 的 config 参数
config.dimensions.width = videoInfo.bmiHeader.biWidth;
config.dimensions.height = videoInfo.bmiHeader.biHeight;
// m_videoFrame 是 IVideoFrame 中 ExternalVideoFrame 结构体的实例,下面设置外部视频帧属性
// 下列参数由 videoInfo 中的 bmiHeader 参数得到。bmiHeader 是 Win32 _BITMAPINFOHEADER 结构,包含视频图像 bitmap 的颜色和维度信息。
m_videoFrame.stride = videoInfo.bmiHeader.biWidth;
m_videoFrame.height = videoInfo.bmiHeader.biHeight;
// 下列参数需要开发者自行设定
m_videoFrame.rotation = 0;
m_videoFrame.cropBottom = 0;
m_videoFrame.cropLeft = 0;
m_videoFrame.cropRight = 0;
m_videoFrame.cropTop = 0;
m_videoFrame.format = agora::media::ExternalVideoFrame::VIDEO_PIXEL_I420;
m_videoFrame.type = agora::media::ExternalVideoFrame::VIDEO_BUFFER_TYPE::VIDEO_BUFFER_RAW_DATA;
// 设置 fps,10000000 除以每帧平均展示时间(单位为 100 纳秒)
m_fps = (int)(10000000ll / videoInfo.AvgTimePerFrame);
// 设置视频编码属性
m_rtcEngine->setVideoEncoderConfiguration(config);
// 初始化渲染模块
m_d3dRender.Init(m_localVideoWnd.GetSafeHwnd(),
videoInfo.bmiHeader.biWidth, videoInfo.bmiHeader.biHeight, true);
// 开始视频自采集
m_agVideoCaptureDevice.Start();
}
else {
// 停止视频采集
m_agVideoCaptureDevice.Stop();
// 移除视频采集过滤器
m_agVideoCaptureDevice.RemoveCaptureFilter();
if (m_rtcEngine)
{
m_rtcEngine->stopPreview();
m_d3dRender.Close();
}
}
}
PushVideoFrameThread
线程。线程读取自采集模块采集的视频帧,开启自渲染模块,并执行 pushVideoFrame
推送外部视频帧。// 此线程通过本地用户加入频道时触发
void CAgoraCaptureVideoDlg::PushVideoFrameThread(CAgoraCaptureVideoDlg * self)
{
// 创建使用 IMediaEngine 类为 template 的 AutoPtr 实例
agora::util::AutoPtr<agora::media::IMediaEngine> mediaEngine;
// AutoPtr 实例调用 queryInterface 方法,通过 IID 获取 IMediaEngine 实例的指针。AutoPtr 实例会通过箭头操作符访问 IMediaEngine 实例的指针并通过 IMediaEngine 实例调用 setExternalVideoSource。
mediaEngine.queryInterface(self->m_rtcEngine, agora::AGORA_IID_MEDIA_ENGINE);
// 开始本地预览
self -> m_rtcEngine->startPreview();
// 如果在 UI 开启了自采集并且本地用户已加入频道
while (self->m_extenalCaptureVideo && self->m_joinChannel)
{
// 如果视频帧是 YUV 标准格式 4:2:0,执行以下操作:读取 buffer,设置时间戳;使用自渲染模块(d3d)渲染视频帧;执行 pushAudioFrame 将视频帧推送到 SDK
if (self->m_videoFrame.format == agora::media::ExternalVideoFrame::VIDEO_PIXEL_I420) {
int bufSize = self->m_videoFrame.stride * self->m_videoFrame.height * 3 / 2;
int timestamp = GetTickCount();
// 如果有可读取的自采集视频帧,设置时间戳为当前时间
if (CAgVideoBuffer::GetInstance()->readBuffer(self->m_buffer, bufSize, timestamp)) {
self->m_videoFrame.timestamp = timestamp;
}
else
{
// 如果没有可读取的自采集视频帧,就休眠 1 毫秒并略过下面的步骤,继续执行循环
Sleep(1);
continue;
}
// 将读取的 buffer 赋值给 m_videoFrame 实例(ExternalVideoFrame)的 buffer 成员变量(指针),用于 pushVideoFrame 推送。readBuffer 属于自采集模块定义的功能。
self->m_videoFrame.buffer = self->m_buffer;
// 使用自渲染模块渲染读取的 buffer 并发送到本地预览窗口
self->m_d3dRender.Render((char*)self->m_buffer);
// 调用 pushVideoFrame 将采集的视频帧推送至 SDK
mediaEngine->pushVideoFrame(&self->m_videoFrame);
// 休眠,时长为每个视频帧出现的毫秒数
Sleep(1000 / self->m_fps);
}
else {
return;
}
}
}
开始自定义采集前,请确保你已在项目中实现基本的通话或者直播功能,详见一对一通话或互动直播。
参考如下步骤,在你的项目中实现自定义视频源功能:
实现 IVideoSource
类和 IVideoFrameConsumer
类。在回调中设置以下逻辑:
onInitialize
回调时保存 IVideoFrameConsumer
实例。onStart
回调时,调用 consumeRawVideoFrame
,读取自采集模块的视频帧并将视频帧发送给 SDK。onStop
回调时,停止 IVideoFrameConsumer
向 SDK 发送视频帧。调用 setVideoSource
设置自定义的视频源。
开发者自行实现视频采集功能。
(可选)开发者自行实现视频渲染功能。
启动自采集模块。在 onStart
回调中调用 consumeRawVideoFrame
读取自采集数据并将视频帧发送给 SDK。
下图展示了 API 调用时序:
下图展示了 mediaIO 方式的自采集情况下视频帧的流转:
setVideoSource
指定视频源。consumeRawVideoFrame
发送到 SDK。示例代码仅实现了自采集功能。
IVideoSource
类和 IVideoFrameConsumer
类。在对象中创建 ThreadRun
线程,用于消费外部视频源。设置线程在 onStart
回调触发时启动,调用 consumeRawVideoFrame
方法读取自采集模块的视频帧并将视频帧发送给 SDK。class CAgoraVideoSource :public IVideoSource {
// 重写原先的 onInitialize 回调
virtual bool onInitialize(IVideoFrameConsumer *consumer) override
{
// 互斥锁,保证进程不陷入死锁
std::lock_guard<std::mutex> m(m_mutex);
// 从回调处获取 IVideoFrameConsumer
m_videoConsumer = consumer;
OutputDebugString(_T("onInitialize\n"));
return true;
}
// 重写原先的 onDispose 回调
virtual void onDispose() override
{
OutputDebugString(_T("onDispose\n"));
Stop();
}
// 重写原先的 onStart 回调
virtual bool onStart() override
{
OutputDebugString(_T("onStart\n"));
// 开启 ThreadRun 线程
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadRun, this, 0, NULL);
return true;
}
// ThreadRun 线程,读取自采集模块的视频帧并将视频帧发送给 SDK
static void ThreadRun(CAgoraVideoSource* self)
{
// 只要没有执行 onStop,就继续执行循环
while (!self->m_isExit)
{
// 设置 bufSize 参数,获取当前 timestamp,读取 buffer
int bufSize = self->m_width * self->m_height * 3 / 2;
int timestamp = GetTickCount();
// 如果读不到 buffer,则休眠 1 毫秒并略过下面的步骤并继续执行循环
if (!CAgVideoBuffer::GetInstance()->readBuffer(self->m_buffer, bufSize, timestamp)) {
Sleep(1);
continue;
}
// 如果已经执行 onInitialize,就执行下列逻辑
if (self->m_videoConsumer)
{
// 开启互斥锁
self->m_mutex.lock();
// 消费原始视频帧。getBufferType 回调中指定的视频帧类型和在 consumeRawVideoFrame 方法中指定的视频帧类型必须一致。
self->m_videoConsumer->consumeRawVideoFrame(self->m_buffer, ExternalVideoFrame::VIDEO_PIXEL_I420,
self->m_width, self->m_height, self->m_rotation, timestamp);
// 关闭互斥锁
self->m_mutex.unlock();
// 休眠,时长为每个视频帧的显示时长
Sleep(1000 / self->m_fps);
}
}
}
// 重写原先的 onStop 方法
virtual void onStop() override
{
OutputDebugString(_T("onStop\n"));
Stop();
}
// 重写获取 buffer 类型方法。强制返回 VIDEO_PIXEL_I420。getBufferType 回调中指定的视频帧类型和在 consumeRawVideoFrame 方法中指定的视频帧类型必须一致。
virtual agora::media::ExternalVideoFrame::VIDEO_PIXEL_FORMAT getBufferType() override
{
return ExternalVideoFrame::VIDEO_PIXEL_I420;
}
// 重写,强制返回 VIDEO_CAPTURE_CAMERA。
virtual VIDEO_CAPTURE_TYPE getVideoCaptureType() override
{
return VIDEO_CAPTURE_CAMERA;
}
// 重载,强制返回 CONTENT_HINT_DETAILS。
virtual VideoContentHint getVideoContentHint() override
{
return CONTENT_HINT_DETAILS;
}
public:
// 构造方法
CAgoraVideoSource()
{
m_buffer = new BYTE[1920 * 1080 * 4 * 4];
}
// 析构方法
~CAgoraVideoSource()
{
delete m_buffer;
}
// 停止 ThreadRun 线程
void Stop()
{
std::lock_guard<std::mutex> m(m_mutex);
m_isExit = true;
m_videoConsumer = nullptr;
}
// 设置相关参数
void SetParameters(bool isExit, int width, int height, int rotation,int fps)
{
// 互斥锁
std::lock_guard<std::mutex> m(m_mutex);
// 是否已退出
m_isExit = isExit;
// 宽,高,旋转,fps
m_width = width;
m_height = height;
m_rotation = rotation;
m_fps = fps;
}
private:
// IVideoFrameConsumer 对象
IVideoFrameConsumer * m_videoConsumer;
bool m_isExit;
BYTE * m_buffer;
int m_width;
int m_height;
int m_rotation;
int m_fps;
std::mutex m_mutex;
};
// 创建外部视频源对象
CAgoraVideoSource m_videoSouce;
// 创建、初始化视频采集对象
m_agVideoCaptureDevice.Create();
setVideoSource
设置自定义的视频源。执行 setVideoSource
之后,onStart
回调会触发 ThreadRun
线程,线程会调用 consumeRawVideoFrame
从自采集模块读取视频帧并将视频帧发送给 SDK。如果线程收不到视频帧则会继续等待。// 设置自定义的视频源。
BOOL CAgoraMediaIOVideoCaptureDlg::EnableExtendVideoCapture(BOOL bEnable)
{
bool bRet = true;
if (mediaEngine.get() == NULL)
return FALSE;
if (bEnable) {
// 设置自定义视频源
bRet = m_rtcEngine->setVideoSource(&m_videoSouce);
}
return bRet ? TRUE : FALSE;
}
// 开始视频自采集
void CAgoraMediaIOVideoCaptureDlg::EnableCaputre(BOOL bEnable)
{
if (bEnable == (BOOL)!m_extenalCaptureVideo)return;
int nIndex = m_cmbVideoType.GetCurSel();
if (bEnable)
{
// 根据 UI 的采集设备类型选择视频采集设备。m_agVideoCaptureDevice 是自行实现的自采集模块中的实例。
m_agVideoCaptureDevice.SelectMediaCap(nIndex == -1 ? 0 : nIndex);
// 创建 VIDEOINFOHEADER 实例,由 Windows 原生的 Windows Media Device Manager SDK 实现
VIDEOINFOHEADER videoInfo;
// 创建 VideoEncoderConfiguration 实例。
VideoEncoderConfiguration config;
// 创建采集过滤器
m_agVideoCaptureDevice.CreateCaptureFilter();
// 获取选择的视频采集设备
m_agVideoCaptureDevice.GetCurrentVideoCap(&videoInfo);
// 设置 setVideoEncoderConfiguration 的 config 参数
// 下列参数由 videoInfo 中的 bmiHeader 参数得到。bmiHeader 是 Win32 _BITMAPINFOHEADER 结构,包含视频图像 bitmap 的颜色和维度信息。
config.dimensions.width = videoInfo.bmiHeader.biWidth;
config.dimensions.height = videoInfo.bmiHeader.biHeight;
// 设置外部视频参数。SetParameters 方法的实现详见 CAgoraVideoSource。
m_videoSouce.SetParameters(false, videoInfo.bmiHeader.biWidth,
videoInfo.bmiHeader.biHeight, 0, (int)(10000000ll / videoInfo.AvgTimePerFrame));
// 设置视频编码属性
m_rtcEngine->setVideoEncoderConfiguration(config);
// 开始视频自采集
m_agVideoCaptureDevice.Start();
// 开始本地预览
m_rtcEngine->startPreview();
}
else {
m_videoSouce.Stop();
// 停止视频自采集
m_agVideoCaptureDevice.Stop();
// 移除视频采集模块
m_agVideoCaptureDevice.RemoveCaptureFilter();
if (m_rtcEngine)
{
m_rtcEngine->stopPreview();
}
}
}
setRecordingAudioFrameParameters
里的 mode
设置为 RAW_AUDIO_FRAME_OP_MODE_READ_WRITE
才可以读写和操作数据。如果你还想在项目中实现自定义的音频采集和渲染功能,请参考文档自定义音频采集和渲染。