From a2827e71c5b69151f23b4a93f4a603d1aa33e480 Mon Sep 17 00:00:00 2001 From: jinyu Date: Tue, 16 May 2023 15:48:18 +0800 Subject: [PATCH 01/11] fix android custom vidou source --- .../ custom_video_source_android_ng.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md b/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md index b4f3cc2dc1b..e12454c8792 100644 --- a/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md +++ b/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md @@ -81,7 +81,7 @@ 参考下图调用时序,在你的 app 中实现自定义视频采集: -![](https://web-cdn.agora.io/docs-files/1683598705647) +![](https://web-cdn.agora.io/docs-files/1684223014176) ### 实现步骤 @@ -94,7 +94,7 @@ int videoTrackId = RtcEngine.createCustomVideoTrack(); ``` -2. 调用 `joinChannel` 加入频道,或调用 `joinChannelEx` 加入多频道, 在每个频道的 `ChannelMediaOptions` 中,将 `customVideoTrackId` 参数设置为步骤 1 中获得的视频轨道 ID,并将 `publishCustomVideoTrack` 设置为 `true`,`publishCameraTrack` 设置为 `false`,即可在多个频道中发布指定的自定义视频轨道。 +2. 调用 `joinChannel` 加入频道,或调用 `joinChannelEx` 加入多频道, 在每个频道的 `ChannelMediaOptions` 中,将 `customVideoTrackId` 参数设置为步骤 1 中获得的视频轨道 ID,并将 `publishCustomVideoTrack` 设置为 `true`,即可在多个频道中发布指定的自定义视频轨道。 ```java // 如需在多个频道发布自定义视频轨道,则需要多次设置 ChannelMediaOptions 并多次调用 joinChannelEx @@ -102,13 +102,11 @@ ChannelMediaOptions option = new ChannelMediaOptions(); option.clientRoleType = Constants.CLIENT_ROLE_BROADCASTER; option.autoSubscribeAudio = true; option.autoSubscribeVideo = true; -// 取消发布摄像头流 -option.publishCameraTrack = false; // 发布自采集视频流 option.publishCustomVideoTrack = true; option.customVideoTrackId = videoTrackId; // 加入主频道 -int res = RtcEngine.joinChannel(accessToken, option, new IRtcEngineEventHandler(){}); +int res = engine.joinChannel(accessToken, channelId, 0, option); // 或加入多频道 int res = RtcEngine.joinChannelEx(accessToken, connection, option, new IRtcEngineEventHandler(){}); ``` From c0a5ef1caac5e789e832ddc5bb6f179922f495aa Mon Sep 17 00:00:00 2001 From: jinyu Date: Thu, 18 May 2023 16:00:42 +0800 Subject: [PATCH 02/11] custom video source windows --- .../ custom_video_source_android_ng.md | 173 +++++++++--------- .../ custom_video_source_windows_ng.md | 169 +++++++++++++++++ 2 files changed, 253 insertions(+), 89 deletions(-) create mode 100644 markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md diff --git a/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md b/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md index e12454c8792..d1027ac6614 100644 --- a/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md +++ b/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md @@ -55,14 +55,10 @@ 下图展示在单频道和多频道中实现自定义视频采集时,视频数据的传输过程: -### 单频道 - 仅在一个频道内发布自采集视频流: ![](https://web-cdn.agora.io/docs-files/1683598621022) -### 多频道 - 在多个频道内发布不同的自采集视频流: ![](https://web-cdn.agora.io/docs-files/1683598671853) @@ -75,26 +71,24 @@ ## 实现自定义视频采集 -参考如下内容,在你的 app 中实现自定义视频采集功能。 - -### API 调用时序 - 参考下图调用时序,在你的 app 中实现自定义视频采集: ![](https://web-cdn.agora.io/docs-files/1684223014176) -### 实现步骤 - 参考如下步骤,在你的 app 中实现自定义视频采集功能: -1. 初始化 `RtcEngine` 后,调用 `createCustomVideoTrack` 创建自定义视频轨道并获得视频轨道 ID。根据场景需要,你可以创建多个自定义视频轨道。 +### 1. 创建自定义视频轨道 + +初始化 `RtcEngine` 后,调用 `createCustomVideoTrack` 创建自定义视频轨道并获得视频轨道 ID。根据场景需要,你可以创建多个自定义视频轨道。 ```java // 如需创建多个自定义视频轨道,可以多次调用 createCustomVideoTrack int videoTrackId = RtcEngine.createCustomVideoTrack(); ``` -2. 调用 `joinChannel` 加入频道,或调用 `joinChannelEx` 加入多频道, 在每个频道的 `ChannelMediaOptions` 中,将 `customVideoTrackId` 参数设置为步骤 1 中获得的视频轨道 ID,并将 `publishCustomVideoTrack` 设置为 `true`,即可在多个频道中发布指定的自定义视频轨道。 +### 2. 加入频道并发布自定义视频轨道 + +调用 `joinChannel` 加入频道,或调用 `joinChannelEx` 加入多频道, 在每个频道的 `ChannelMediaOptions` 中,将 `customVideoTrackId` 参数设置为步骤 1 中获得的视频轨道 ID,并将 `publishCustomVideoTrack` 设置为 `true`,即可在多个频道中发布指定的自定义视频轨道。 ```java // 如需在多个频道发布自定义视频轨道,则需要多次设置 ChannelMediaOptions 并多次调用 joinChannelEx @@ -104,6 +98,7 @@ option.autoSubscribeAudio = true; option.autoSubscribeVideo = true; // 发布自采集视频流 option.publishCustomVideoTrack = true; +// 设置自定义视频轨道 ID option.customVideoTrackId = videoTrackId; // 加入主频道 int res = engine.joinChannel(accessToken, channelId, 0, option); @@ -111,106 +106,104 @@ int res = engine.joinChannel(accessToken, channelId, 0, option); int res = RtcEngine.joinChannelEx(accessToken, connection, option, new IRtcEngineEventHandler(){}); ``` -3. 实现视频采集。声网提供 [VideoFileReader.java](https://github.com/AgoraIO/API-Examples/blob/main/Android/APIExample/app/src/main/java/io/agora/api/example/utils/VideoFileReader.java) 演示从本地文件读取 YUV 格式的视频数据。在实际的生产环境中,声网 SDK 不提供自定义视频处理 API,你需要结合业务需求使用 Android SDK 为你的设备创建自定义视频模块。 +### 3. 实现自采集模块 + +声网提供 [VideoFileReader.java](https://github.com/AgoraIO/API-Examples/blob/main/Android/APIExample/app/src/main/java/io/agora/api/example/utils/VideoFileReader.java) 演示从本地文件读取 YUV 格式的视频数据。在实际的生产环境中,声网 SDK 不提供自定义视频处理 API,你需要结合业务需求使用 Android SDK 为你的设备创建自定义视频模块。 -4. 将采集到的视频帧发送至 SDK 之前,通过设置 `VideoFrame` 集成你的视频模块。你可以参考以下代码,将采集到的 YUV 视频数据转换为不同类型的 `VideoFrame`。为确保音视频同步,声网建议你调用 `getCurrentMonotonicTimeInMs` 获取 SDK 当前的 Monotonic Time 后,将该值传入采集的 `VideoFrame` 的时间戳参数。 +### 4. 通过视频轨道推送视频数据到 SDK - ```java - // 创建不同类型的 VideoFrame - VideoFrame.Buffer frameBuffer; - // 将 YUV 视频数据转换为 NV21 格式 - if ("NV21".equals(selectedItem)) { +将采集到的视频帧发送至 SDK 之前,通过设置 `VideoFrame` 集成你的视频模块。你可以参考以下代码,将采集到的 YUV 视频数据转换为不同类型的 `VideoFrame`。为确保音视频同步,声网建议你调用 `getCurrentMonotonicTimeInMs` 获取 SDK 当前的 Monotonic Time 后,将该值传入采集的 `VideoFrame` 的时间戳参数。 + +```java +// 创建不同类型的 VideoFrame +VideoFrame.Buffer frameBuffer; +// 将 YUV 视频数据转换为 NV21 格式 +if ("NV21".equals(selectedItem)) { + int srcStrideY = width; + int srcHeightY = height; + int srcSizeY = srcStrideY * srcHeightY; + ByteBuffer srcY = ByteBuffer.allocateDirect(srcSizeY); + srcY.put(yuv, 0, srcSizeY); + + int srcStrideU = width / 2; + int srcHeightU = height / 2; + int srcSizeU = srcStrideU * srcHeightU; + ByteBuffer srcU = ByteBuffer.allocateDirect(srcSizeU); + srcU.put(yuv, srcSizeY, srcSizeU); + + int srcStrideV = width / 2; + int srcHeightV = height / 2; + int srcSizeV = srcStrideV * srcHeightV; + ByteBuffer srcV = ByteBuffer.allocateDirect(srcSizeV); + srcV.put(yuv, srcSizeY + srcSizeU, srcSizeV); + + int desSize = srcSizeY + srcSizeU + srcSizeV; + ByteBuffer des = ByteBuffer.allocateDirect(desSize); + YuvHelper.I420ToNV12(srcY, srcStrideY, srcV, srcStrideV, srcU, srcStrideU, des, width, height); + + byte[] nv21 = new byte[desSize]; + des.position(0); + des.get(nv21); + + frameBuffer = new NV21Buffer(nv21, width, height, null); +} + // 将 YUV 视频数据转换为 NV12 格式 + else if ("NV12".equals(selectedItem)) { int srcStrideY = width; int srcHeightY = height; int srcSizeY = srcStrideY * srcHeightY; ByteBuffer srcY = ByteBuffer.allocateDirect(srcSizeY); srcY.put(yuv, 0, srcSizeY); - + int srcStrideU = width / 2; int srcHeightU = height / 2; int srcSizeU = srcStrideU * srcHeightU; ByteBuffer srcU = ByteBuffer.allocateDirect(srcSizeU); srcU.put(yuv, srcSizeY, srcSizeU); - + int srcStrideV = width / 2; int srcHeightV = height / 2; int srcSizeV = srcStrideV * srcHeightV; ByteBuffer srcV = ByteBuffer.allocateDirect(srcSizeV); srcV.put(yuv, srcSizeY + srcSizeU, srcSizeV); - + int desSize = srcSizeY + srcSizeU + srcSizeV; ByteBuffer des = ByteBuffer.allocateDirect(desSize); - YuvHelper.I420ToNV12(srcY, srcStrideY, srcV, srcStrideV, srcU, srcStrideU, des, width, height); - - byte[] nv21 = new byte[desSize]; - des.position(0); - des.get(nv21); - - frameBuffer = new NV21Buffer(nv21, width, height, null); + YuvHelper.I420ToNV12(srcY, srcStrideY, srcU, srcStrideU, srcV, srcStrideV, des, width, height); + + frameBuffer = new NV12Buffer(width, height, width, height, des, null); } - // 将 YUV 视频数据转换为 NV12 格式 - else if ("NV12".equals(selectedItem)) { - int srcStrideY = width; - int srcHeightY = height; - int srcSizeY = srcStrideY * srcHeightY; - ByteBuffer srcY = ByteBuffer.allocateDirect(srcSizeY); - srcY.put(yuv, 0, srcSizeY); - - int srcStrideU = width / 2; - int srcHeightU = height / 2; - int srcSizeU = srcStrideU * srcHeightU; - ByteBuffer srcU = ByteBuffer.allocateDirect(srcSizeU); - srcU.put(yuv, srcSizeY, srcSizeU); - - int srcStrideV = width / 2; - int srcHeightV = height / 2; - int srcSizeV = srcStrideV * srcHeightV; - ByteBuffer srcV = ByteBuffer.allocateDirect(srcSizeV); - srcV.put(yuv, srcSizeY + srcSizeU, srcSizeV); - - int desSize = srcSizeY + srcSizeU + srcSizeV; - ByteBuffer des = ByteBuffer.allocateDirect(desSize); - YuvHelper.I420ToNV12(srcY, srcStrideY, srcU, srcStrideU, srcV, srcStrideV, des, width, height); - - frameBuffer = new NV12Buffer(width, height, width, height, des, null); + // 将 YUV 视频数据转换为 Texture 格式 + else if ("Texture2D".equals(selectedItem)) { + if (textureBufferHelper == null) { + textureBufferHelper = TextureBufferHelper.create("PushExternalVideoYUV", EglBaseProvider.instance().getRootEglBase().getEglBaseContext()); } - // 将 YUV 视频数据转换为 Texture 格式 - else if ("Texture2D".equals(selectedItem)) { - if (textureBufferHelper == null) { - textureBufferHelper = TextureBufferHelper.create("PushExternalVideoYUV", EglBaseProvider.instance().getRootEglBase().getEglBaseContext()); - } - if (yuvFboProgram == null) { - textureBufferHelper.invoke((Callable) () -> { - yuvFboProgram = new YuvFboProgram(); - return null; - }); - } - Integer textureId = textureBufferHelper.invoke(() -> yuvFboProgram.drawYuv(yuv, width, height)); - frameBuffer = textureBufferHelper.wrapTextureBuffer(width, height, VideoFrame.TextureBuffer.Type.RGB, textureId, new Matrix()); + if (yuvFboProgram == null) { + textureBufferHelper.invoke((Callable) () -> { + yuvFboProgram = new YuvFboProgram(); + return null; + }); } - // 将 YUV 视频数据转换为 I420 格式 - else if("I420".equals(selectedItem)) - { - JavaI420Buffer i420Buffer = JavaI420Buffer.allocate(width, height); - i420Buffer.getDataY().put(yuv, 0, i420Buffer.getDataY().limit()); - i420Buffer.getDataU().put(yuv, i420Buffer.getDataY().limit(), i420Buffer.getDataU().limit()); - i420Buffer.getDataV().put(yuv, i420Buffer.getDataY().limit() + i420Buffer.getDataU().limit(), i420Buffer.getDataV().limit()); - frameBuffer = i420Buffer; - } - - // 获取 SDK 当前的 Monotonic Time - long currentMonotonicTimeInMs = engine.getCurrentMonotonicTimeInMs(); - // 创建 VideoFrame,并将 SDK 当前的 Monotonic Time 赋值到 VideoFrame 的时间戳参数 - VideoFrame videoFrame = new VideoFrame(frameBuffer, 0, currentMonotonicTimeInMs); - - // 通过视频轨道推送视频帧到 SDK - int ret = engine.pushExternalVideoFrameEx(videoFrame, videoTrack); - if (ret < 0) { - Log.w(TAG, "pushExternalVideoFrameEx error code=" + ret); + Integer textureId = textureBufferHelper.invoke(() -> yuvFboProgram.drawYuv(yuv, width, height)); + frameBuffer = textureBufferHelper.wrapTextureBuffer(width, height, VideoFrame.TextureBuffer.Type.RGB, textureId, new Matrix()); + } + // 将 YUV 视频数据转换为 I420 格式 + else if("I420".equals(selectedItem)) + { + JavaI420Buffer i420Buffer = JavaI420Buffer.allocate(width, height); + i420Buffer.getDataY().put(yuv, 0, i420Buffer.getDataY().limit()); + i420Buffer.getDataU().put(yuv, i420Buffer.getDataY().limit(), i420Buffer.getDataU().limit()); + i420Buffer.getDataV().put(yuv, i420Buffer.getDataY().limit() + i420Buffer.getDataU().limit(), i420Buffer.getDataV().limit()); + frameBuffer = i420Buffer; } - ``` -5. 调用 `pushExternalVideoFrameEx` 并将 `videoTrackId` 指定为步骤 2 中指定的视频轨道 ID,将视频帧通过视频轨道发送给 SDK。 +// 获取 SDK 当前的 Monotonic Time +long currentMonotonicTimeInMs = engine.getCurrentMonotonicTimeInMs(); +// 创建 VideoFrame,并将 SDK 当前的 Monotonic Time 赋值到 VideoFrame 的时间戳参数 +VideoFrame videoFrame = new VideoFrame(frameBuffer, 0, currentMonotonicTimeInMs); +``` + +调用 `pushExternalVideoFrameEx` 并将 `videoTrackId` 指定为步骤 2 中指定的视频轨道 ID,将视频帧通过视频轨道发送给 SDK。 ```java // 通过视频轨道推送视频帧到 SDK @@ -220,7 +213,9 @@ if (ret < 0) { } ``` -6. 如需停止自定义视频采集,调用 `destroyCustomVideoTrack` 来销毁视频轨道。如需销毁多个视频轨道,可多次调用 `destroyCustomVideoTrack`。 +### 5. 销毁自定义视频轨道 + +如需停止自定义视频采集,调用 `destroyCustomVideoTrack` 来销毁视频轨道。如需销毁多个视频轨道,可多次调用 `destroyCustomVideoTrack`。 ```java engine.destroyCustomVideoTrack(videoTrack); diff --git a/markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md b/markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md new file mode 100644 index 00000000000..1d398814ffe --- /dev/null +++ b/markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md @@ -0,0 +1,169 @@ +# 自定义视频采集 (Windows) + +自定义视频采集是指通过自定义的视频采集源实现对视频流的采集。 + +与 SDK 默认的视频采集方式不同,自定义视频采集支持用户自行控制采集源,实现更加精细的视频属性调整。例如,支持通过高清摄像头、无人机摄像头或其他类型的摄像头实现视频采集,同时支持动态调整视频质量、分辨率和帧率等参数,以适应不同的应用场景和需求。 + +声网推荐优先使用更为稳定、可靠、集成维护难度低的 SDK 视频采集,如果你有特定的视频采集需求或无法使用 SDK 采集,那么自定义视频采集为你提供灵活、可定制的方案。 + + +## 适用场景 + +你可以在多种行业的多种场景下使用到自定义视频采集: + +### 视频特殊处理和增强 + +在某些游戏或虚拟现实应用中,需要对视频流进行实时的特效处理、滤镜处理或其他增强效果。在这种情况下,使用自定义视频采集可以直接获取原始视频流,并进行实时处理,从而实现更加逼真的游戏或虚拟现实效果。 + +### 高精度视频采集 + +在视频监控领域,需要对场景中的细节进行精细的观察和分析,此时使用自定义视频采集可以获得更高的图像质量和更精细的采集控制。 + +### 特定视频源采集 + +在 IoT、直播等行业需要使用特定的摄像头、监控设备或其他非摄像头设备视频源,例如视频捕捉卡、录屏数据等。在这种情况下,使用 SDK 内部采集可能无法满足需求,必须使用自定义视频采集来实现对特定视频源的采集。 + +### 与特定设备或第三方应用无缝对接 + +在智能家居或物联网领域,需要将设备中的视频传输到用户的手机或电脑上进行监控和控制,此时可能需要使用特定的设备或应用程序进行视频采集。在这种情况下,使用自定义视频采集可以方便地将特定设备或应用程序与 RTC SDK 进行对接。 + +### 特定的视频编码格式 + +在特定直播场景中,可能需要使用特定的视频编码格式来满足业务需求,此时使用 SDK 内部采集可能无法满足需求,必须使用自定义视频采集来实现对特定编码格式视频的采集和自定义编码。 + + +## 优势介绍 + +使用自定义视频采集,你可以体验到: + +### 更多类型的视频流 + +自定义视频采集功能可以使用更高质量、更多类型的采集设备和摄像头,从而获得更清晰、更流畅的视频流。这有助于提高用户的观看体验,并使产品更具竞争力。 + +### 更灵活的视频特效 + +自定义视频采集功能可以帮助用户实现更丰富、更个性化的视频特效和过滤器,从而提高用户的体验和应用程序的吸引力。用户可以通过自定义视频采集功能实现各种特效,如美颜、滤镜、动态贴纸等。 + +### 更适应各种场景的需求 + +自定义视频采集功能可以帮助应用程序更好地适应各种场景的需求,如直播、视频会议、在线教育等。用户可以根据不同的场景需求,定制不同的视频采集方案,从而提供适应性更强的应用程序。 + + +## 技术原理 + +声网 SDK 提供自定义视频轨道方式实现视频自采集。你可以创建一个或多个自定义视频轨道,加入频道并在每个频道中发布已创建的视频轨道。你需要使用自采集模块驱动采集设备对视频进行采集,并将采集的视频帧通过视频轨道发送给 SDK。 + +下图展示在单频道和多频道中实现自定义视频采集时,视频数据的传输过程: + +仅在一个频道内发布自采集视频流: + +![](https://web-cdn.agora.io/docs-files/1683598621022) + +在多个频道内发布不同的自采集视频流: + +![](https://web-cdn.agora.io/docs-files/1683598671853) + + +## 前提条件 + +在进行操作之前,请确保你已经在项目中实现了基本的实时音视频功能,详见[实现视频通话](https://docs.agora.io/cn/video-call-4.x/start_call_windows_ng)或[实现视频直播](https://docs.agora.io/cn/live-streaming-premium-4.x/start_live_windows_ng)。 + + +## 实现自定义视频采集 + +参考下图调用时序,在你的 app 中实现自定义视频采集: + +![](https://web-cdn.agora.io/docs-files/1684381970999) + +参考如下步骤,在你的 app 中实现自定义视频采集功能: + +### 1. 创建自定义视频轨道 + +初始化 `IRtcEngine` 后,调用 `createCustomVideoTrack` 创建自定义视频轨道并获得视频轨道 ID。根据场景需要,你可以创建多个自定义视频轨道。 + +```cpp +// 如需创建多个自定义视频轨道,可以多次调用 createCustomVideoTrack +int videoTrackId = m_rtcEngine->createCustomVideoTrack(); +m_trackVideoTrackIds[trackIndex] = videoTrackId; +``` + +### 2. 加入频道并发布自定义视频轨道 + +调用 `joinChannel` 加入频道,或调用 `joinChannelEx` 加入多频道, 在每个频道的 `ChannelMediaOptions` 中,将 `customVideoTrackId` 参数设置为步骤 1 中获得的视频轨道 ID,并将 `publishCustomVideoTrack` 设置为 `true`,即可在频道中发布指定的自定义视频轨道。 + +```cpp +// 如需在多个频道发布自定义视频轨道,则需要多次设置 ChannelMediaOptions 并多次调用 joinChannelEx +ChannelMediaOptions mediaOptions; +mediaOptions.clientRoleType = CLIENT_ROLE_BROADCASTER; +// 发布自采集视频流 +mediaOptions.publishCustomVideoTrack = true; +mediaOptions.autoSubscribeVideo = false; +mediaOptions.autoSubscribeAudio = false; +// 设置自定义视频轨道 ID +mediaOptions.customVideoTrackId = videoTrackId; +// 加入频道 +int res = m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.data(), 0, mediaOptions); +// 或加入多频道 +int ret = m_rtcEngine->joinChannelEx(APP_TOKEN, m_trackConnections[trackIndex], mediaOptions, &m_trackEventHandlers[trackIndex]); +``` + +### 3. 实现自采集模块 + +声网提供 [YUVReader.cpp](https://github.com/AgoraIO/API-Examples/blob/dev/4.2.0/windows/APIExample/APIExample/YUVReader.cpp) 和 [YUVReader.h](https://github.com/AgoraIO/API-Examples/blob/dev/4.2.0/windows/APIExample/APIExample/YUVReader.h) 演示从本地文件读取 YUV 格式的视频数据。在实际的生产环境中,声网 SDK 不提供自定义视频处理 API,你需要结合业务需求为你的采集设备创建自定义视频采集模块。 + +```cpp +// 通过 OnYUVRead 回调读取 YUV 视频数据的宽、高、缓冲区和大小 +void MultiVideoSourceTracksYUVReaderHander::OnYUVRead(int width, int height, unsigned char* buffer, int size) +``` + +### 4. 通过视频轨道推送视频数据到 SDK + +将采集到的视频帧发送至 SDK 之前,你可以参考以下代码,将采集到的 YUV 原始视频数据转换为不同类型的 `videoFrame`。为确保音视频同步,声网建议你调用 `getCurrentMonotonicTimeInMs` 获取 SDK 当前的 Monotonic Time 后,将该值传入采集的 `videoFrame` 的时间戳参数。 + +```cpp +// 设置视频像素格式为 I420 +m_videoFrame.format = agora::media::base::VIDEO_PIXEL_I420; +// 设置视频数据类型为原始数据 +m_videoFrame.type = agora::media::base::ExternalVideoFrame::VIDEO_BUFFER_TYPE::VIDEO_BUFFER_RAW_DATA; +// 将采集到的 YUV 视频数据的宽、高、缓冲区传给 videoFrame +m_videoFrame.height = height; +m_videoFrame.stride = width; +m_videoFrame.buffer = buffer; +// 获取 SDK 当前的 Monotonic Time + +// 将 SDK 当前的 Monotonic Time 赋值到 videoFrame 的时间戳参数 +m_videoFrame.timestamp = ???; +``` + +调用 `pushVideoFrame` 并将 `videoTrackId` 指定为步骤 2 中指定的视频轨道 ID,将视频帧通过视频轨道发送给 SDK。 + +```cpp +m_mediaEngine->pushVideoFrame(&m_videoFrame, m_videoTrackId); +``` + +### 5. 销毁自定义视频轨道 + +如需停止自定义视频采集,调用 `destroyCustomVideoTrack` 来销毁视频轨道。如需销毁多个视频轨道,可多次调用 `destroyCustomVideoTrack`。 + +```cpp +m_rtcEngine->destroyCustomVideoTrack(m_trackVideoTrackIds[trackIndex]); +``` + + +## 参考信息 + +本节介绍本文中使用方法的更多信息以及相关页面的链接。 + +### 注意事项 + +如果采集到的自定义视频格式为 Texture,并且远端用户在本地自定义采集视频中看到闪烁、失真等异常情况时,建议先复制视频数据,再将原始视频数据和复制的视频数据发回至 SDK,从而消除内部数据编码过程中出现的异常情况。 + +### 示例项目 + +声网在 GitHub 上提供了开源的示例项目 [MultiVideoSourceTracks](https://github.com/AgoraIO/API-Examples/tree/main/windows/APIExample/APIExample/Advanced/MultiVideoSourceTracks) 供你参考。 + +### API 参考 + +- [`createCustomVideoTrack`](https://docportal.shengwang.cn/cn/video-call-4.x/API%20Reference/windows_ng/API/toc_video_process.html?platform=Windows#api_irtcengine_createcustomvideotrack) +- [`destroyCustomVideoTrack`](https://docportal.shengwang.cn/cn/video-call-4.x/API%20Reference/windows_ng/API/toc_video_process.html?platform=Windows#api_irtcengine_destroycustomvideotrack) +- [`pushVideoFrame`](https://docportal.shengwang.cn/cn/video-call-4.x/API%20Reference/windows_ng/API/toc_video_process.html?platform=Windows#api_imediaengine_pushvideoframe) \ No newline at end of file From dca926d0f4acf3bc5af4a50493996f078c570de5 Mon Sep 17 00:00:00 2001 From: jinyu Date: Thu, 18 May 2023 16:31:16 +0800 Subject: [PATCH 03/11] updates --- .../custom video source/ custom_video_source_android_ng.md | 2 +- .../custom video source/ custom_video_source_windows_ng.md | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md b/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md index d1027ac6614..91d2e705033 100644 --- a/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md +++ b/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md @@ -103,7 +103,7 @@ option.customVideoTrackId = videoTrackId; // 加入主频道 int res = engine.joinChannel(accessToken, channelId, 0, option); // 或加入多频道 -int res = RtcEngine.joinChannelEx(accessToken, connection, option, new IRtcEngineEventHandler(){}); +int res = engine.joinChannelEx(accessToken, connection, option, new IRtcEngineEventHandler() {}); ``` ### 3. 实现自采集模块 diff --git a/markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md b/markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md index 1d398814ffe..736f9df5a70 100644 --- a/markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md +++ b/markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md @@ -102,7 +102,7 @@ mediaOptions.autoSubscribeAudio = false; // 设置自定义视频轨道 ID mediaOptions.customVideoTrackId = videoTrackId; // 加入频道 -int res = m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.data(), 0, mediaOptions); +int ret = m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.data(), 0, mediaOptions); // 或加入多频道 int ret = m_rtcEngine->joinChannelEx(APP_TOKEN, m_trackConnections[trackIndex], mediaOptions, &m_trackEventHandlers[trackIndex]); ``` @@ -130,7 +130,6 @@ m_videoFrame.height = height; m_videoFrame.stride = width; m_videoFrame.buffer = buffer; // 获取 SDK 当前的 Monotonic Time - // 将 SDK 当前的 Monotonic Time 赋值到 videoFrame 的时间戳参数 m_videoFrame.timestamp = ???; ``` From 0ec37db9451daa3e9e3903b735f34dbfcde4cbfa Mon Sep 17 00:00:00 2001 From: jinyu Date: Thu, 25 May 2023 16:57:45 +0800 Subject: [PATCH 04/11] incorporate xcz's feedback --- .../ custom_video_source_windows_ng.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md b/markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md index 736f9df5a70..655ab27e2fb 100644 --- a/markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md +++ b/markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md @@ -92,6 +92,13 @@ m_trackVideoTrackIds[trackIndex] = videoTrackId; 调用 `joinChannel` 加入频道,或调用 `joinChannelEx` 加入多频道, 在每个频道的 `ChannelMediaOptions` 中,将 `customVideoTrackId` 参数设置为步骤 1 中获得的视频轨道 ID,并将 `publishCustomVideoTrack` 设置为 `true`,即可在频道中发布指定的自定义视频轨道。 ```cpp +int uid = 10001 + trackIndex; +m_trackUids[trackIndex] = uid; +m_trackConnections[trackIndex].channelId = m_strChannel.c_str(); +m_trackConnections[trackIndex].localUid = uid; +m_trackEventHandlers[trackIndex].SetId(trackIndex + 1); +m_trackEventHandlers[trackIndex].SetMsgReceiver(m_hWnd); + // 如需在多个频道发布自定义视频轨道,则需要多次设置 ChannelMediaOptions 并多次调用 joinChannelEx ChannelMediaOptions mediaOptions; mediaOptions.clientRoleType = CLIENT_ROLE_BROADCASTER; @@ -120,6 +127,8 @@ void MultiVideoSourceTracksYUVReaderHander::OnYUVRead(int width, int height, uns 将采集到的视频帧发送至 SDK 之前,你可以参考以下代码,将采集到的 YUV 原始视频数据转换为不同类型的 `videoFrame`。为确保音视频同步,声网建议你调用 `getCurrentMonotonicTimeInMs` 获取 SDK 当前的 Monotonic Time 后,将该值传入采集的 `videoFrame` 的时间戳参数。 +以下代码演示推送 I420 格式的原始视频数据,如需推送其他格式的外部视频帧,请进行对应的设置。声网支持的视频像素格式详见 [VIDEO_PIXEL_FORMAT](https://docportal.shengwang.cn/cn/voice-call-4.x/API%20Reference/windows_ng/API/enum_videopixelformat.html)。 + ```cpp // 设置视频像素格式为 I420 m_videoFrame.format = agora::media::base::VIDEO_PIXEL_I420; @@ -129,9 +138,8 @@ m_videoFrame.type = agora::media::base::ExternalVideoFrame::VIDEO_BUFFER_TYPE::V m_videoFrame.height = height; m_videoFrame.stride = width; m_videoFrame.buffer = buffer; -// 获取 SDK 当前的 Monotonic Time -// 将 SDK 当前的 Monotonic Time 赋值到 videoFrame 的时间戳参数 -m_videoFrame.timestamp = ???; +// 获取 SDK 当前的 Monotonic Time 并赋值给 videoFrame 的时间戳参数 +m_videoFrame.timestamp = m_rtcEngine->getCurrentMonotonicTimeInMs(); ``` 调用 `pushVideoFrame` 并将 `videoTrackId` 指定为步骤 2 中指定的视频轨道 ID,将视频帧通过视频轨道发送给 SDK。 From 35a1a246f920c667efa3f7df8c3b3d969d197e99 Mon Sep 17 00:00:00 2001 From: jinyu Date: Fri, 2 Jun 2023 16:32:21 +0800 Subject: [PATCH 05/11] incorporate review feedback --- .../ custom_video_source_windows_ng.md | 71 +++++++++++++------ 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md b/markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md index 655ab27e2fb..021db192e78 100644 --- a/markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md +++ b/markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md @@ -91,6 +91,23 @@ m_trackVideoTrackIds[trackIndex] = videoTrackId; 调用 `joinChannel` 加入频道,或调用 `joinChannelEx` 加入多频道, 在每个频道的 `ChannelMediaOptions` 中,将 `customVideoTrackId` 参数设置为步骤 1 中获得的视频轨道 ID,并将 `publishCustomVideoTrack` 设置为 `true`,即可在频道中发布指定的自定义视频轨道。 +加入主频道: + +```cpp +ChannelMediaOptions mediaOptions; +mediaOptions.clientRoleType = CLIENT_ROLE_BROADCASTER; +// 发布自采集视频流 +mediaOptions.publishCustomVideoTrack = true; +mediaOptions.autoSubscribeVideo = false; +mediaOptions.autoSubscribeAudio = false; +// 设置自定义视频轨道 ID +mediaOptions.customVideoTrackId = videoTrackId; +// 加入频道 +int ret = m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.data(), 0, mediaOptions); +``` + +加入多频道: + ```cpp int uid = 10001 + trackIndex; m_trackUids[trackIndex] = uid; @@ -108,52 +125,60 @@ mediaOptions.autoSubscribeVideo = false; mediaOptions.autoSubscribeAudio = false; // 设置自定义视频轨道 ID mediaOptions.customVideoTrackId = videoTrackId; -// 加入频道 -int ret = m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.data(), 0, mediaOptions); // 或加入多频道 int ret = m_rtcEngine->joinChannelEx(APP_TOKEN, m_trackConnections[trackIndex], mediaOptions, &m_trackEventHandlers[trackIndex]); ``` ### 3. 实现自采集模块 -声网提供 [YUVReader.cpp](https://github.com/AgoraIO/API-Examples/blob/dev/4.2.0/windows/APIExample/APIExample/YUVReader.cpp) 和 [YUVReader.h](https://github.com/AgoraIO/API-Examples/blob/dev/4.2.0/windows/APIExample/APIExample/YUVReader.h) 演示从本地文件读取 YUV 格式的视频数据。在实际的生产环境中,声网 SDK 不提供自定义视频处理 API,你需要结合业务需求为你的采集设备创建自定义视频采集模块。 +声网提供 [YUVReader.cpp](https://github.com/AgoraIO/API-Examples/blob/main/windows/APIExample/APIExample/YUVReader.cpp) 和 [YUVReader.h](https://github.com/AgoraIO/API-Examples/blob/main/windows/APIExample/APIExample/YUVReader.h) 演示从本地文件读取 YUV 格式的视频数据。在实际的生产环境中,声网 SDK 不提供自定义视频处理 API,你需要结合业务需求为你的采集设备创建自定义视频采集模块。 ```cpp -// 通过 OnYUVRead 回调读取 YUV 视频数据的宽、高、缓冲区和大小 -void MultiVideoSourceTracksYUVReaderHander::OnYUVRead(int width, int height, unsigned char* buffer, int size) +// 通过自定义的 YUVReader 类,在 YUVReader 线程中不断读取 YUV 格式视频数据并将数据传递给 OnYUVRead 回调函数进行后续处理 +m_yuvReaderHandlers[trackIndex].Setup(m_rtcEngine, m_mediaEngine.get(), videoTrackId); +m_yuvReaders[trackIndex].start(std::bind(&MultiVideoSourceTracksYUVReaderHander::OnYUVRead, m_yuvReaderHandlers[trackIndex], std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); ``` ### 4. 通过视频轨道推送视频数据到 SDK -将采集到的视频帧发送至 SDK 之前,你可以参考以下代码,将采集到的 YUV 原始视频数据转换为不同类型的 `videoFrame`。为确保音视频同步,声网建议你调用 `getCurrentMonotonicTimeInMs` 获取 SDK 当前的 Monotonic Time 后,将该值传入采集的 `videoFrame` 的时间戳参数。 +调用 `pushVideoFrame` 将采集到的视频帧通过视频轨道推送至 SDK。其中, `videoTrackId` 要与步骤 2 加入频道时指定视频轨道 ID 一致,`videoFrame` 中可以设置视频帧的像素格式、数据类型和时间戳等参数。 -以下代码演示推送 I420 格式的原始视频数据,如需推送其他格式的外部视频帧,请进行对应的设置。声网支持的视频像素格式详见 [VIDEO_PIXEL_FORMAT](https://docportal.shengwang.cn/cn/voice-call-4.x/API%20Reference/windows_ng/API/enum_videopixelformat.html)。 +
  • 以下代码演示将 YUV 格式转换为 I420 格式的原始视频数据。如需推送其他格式的外部视频帧,详见 VIDEO_PIXEL_FORMAT
  • 为确保音视频同步,声网建议你将 videoFrame 的时间戳参数设置为系统 Monotonic Time。你可以调用 getCurrentMonotonicTimeInMs 获取当前的 Monotonic Time。
```cpp -// 设置视频像素格式为 I420 -m_videoFrame.format = agora::media::base::VIDEO_PIXEL_I420; -// 设置视频数据类型为原始数据 -m_videoFrame.type = agora::media::base::ExternalVideoFrame::VIDEO_BUFFER_TYPE::VIDEO_BUFFER_RAW_DATA; -// 将采集到的 YUV 视频数据的宽、高、缓冲区传给 videoFrame -m_videoFrame.height = height; -m_videoFrame.stride = width; -m_videoFrame.buffer = buffer; -// 获取 SDK 当前的 Monotonic Time 并赋值给 videoFrame 的时间戳参数 -m_videoFrame.timestamp = m_rtcEngine->getCurrentMonotonicTimeInMs(); +void MultiVideoSourceTracksYUVReaderHander::OnYUVRead(int width, int height, unsigned char* buffer, int size) +{ + if (m_mediaEngine == nullptr || m_rtcEngine == nullptr) { + return; + } + // 设置视频像素格式为 I420 + m_videoFrame.format = agora::media::base::VIDEO_PIXEL_I420; + // 设置视频数据类型为原始数据 + m_videoFrame.type = agora::media::base::ExternalVideoFrame::VIDEO_BUFFER_TYPE::VIDEO_BUFFER_RAW_DATA; + // 将采集到的 YUV 视频数据的宽、高、缓冲区传给 videoFrame + m_videoFrame.height = height; + m_videoFrame.stride = width; + m_videoFrame.buffer = buffer; + // 获取 SDK 当前的 Monotonic Time 并赋值给 videoFrame 的时间戳参数 + m_videoFrame.timestamp = m_rtcEngine->getCurrentMonotonicTimeInMs(); + // 推送视频帧至 SDK + m_mediaEngine->pushVideoFrame(&m_videoFrame, m_videoTrackId); +} ``` -调用 `pushVideoFrame` 并将 `videoTrackId` 指定为步骤 2 中指定的视频轨道 ID,将视频帧通过视频轨道发送给 SDK。 - -```cpp -m_mediaEngine->pushVideoFrame(&m_videoFrame, m_videoTrackId); -``` ### 5. 销毁自定义视频轨道 如需停止自定义视频采集,调用 `destroyCustomVideoTrack` 来销毁视频轨道。如需销毁多个视频轨道,可多次调用 `destroyCustomVideoTrack`。 ```cpp +// 停止视频数据的自采集 +m_yuvReaders[trackIndex].stop(); +m_yuvReaderHandlers[trackIndex].Release(); +// 销毁自定义视频轨道 m_rtcEngine->destroyCustomVideoTrack(m_trackVideoTrackIds[trackIndex]); +// 离开频道 +m_rtcEngine->leaveChannelEx(m_trackConnections[trackIndex]); ``` @@ -173,4 +198,6 @@ m_rtcEngine->destroyCustomVideoTrack(m_trackVideoTrackIds[trackIndex]); - [`createCustomVideoTrack`](https://docportal.shengwang.cn/cn/video-call-4.x/API%20Reference/windows_ng/API/toc_video_process.html?platform=Windows#api_irtcengine_createcustomvideotrack) - [`destroyCustomVideoTrack`](https://docportal.shengwang.cn/cn/video-call-4.x/API%20Reference/windows_ng/API/toc_video_process.html?platform=Windows#api_irtcengine_destroycustomvideotrack) +- [getCurrentMonotonicTimeInMs](https://docportal.shengwang.cn/cn/video-call-4.x/API%20Reference/windows_ng/API/toc_video_process.html?platform=Windows#api_irtcengine_getcurrentmonotonictimeinms) +- [joinChannelEx](https://docportal.shengwang.cn/cn/video-call-4.x/API%20Reference/windows_ng/API/toc_multi_channel.html?platform=Windows#api_irtcengineex_joinchannelex) - [`pushVideoFrame`](https://docportal.shengwang.cn/cn/video-call-4.x/API%20Reference/windows_ng/API/toc_video_process.html?platform=Windows#api_imediaengine_pushvideoframe) \ No newline at end of file From 4c6c8924545ab480c6d5e2d2e272d58e814a3561 Mon Sep 17 00:00:00 2001 From: jinyu Date: Fri, 2 Jun 2023 17:40:31 +0800 Subject: [PATCH 06/11] Update custom_video_source_windows_ng.md --- .../custom video source/ custom_video_source_windows_ng.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md b/markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md index 021db192e78..e020c999db3 100644 --- a/markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md +++ b/markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md @@ -167,7 +167,7 @@ void MultiVideoSourceTracksYUVReaderHander::OnYUVRead(int width, int height, uns ``` -### 5. 销毁自定义视频轨道 +### 5. 销毁自定义视频轨道并离开频道 如需停止自定义视频采集,调用 `destroyCustomVideoTrack` 来销毁视频轨道。如需销毁多个视频轨道,可多次调用 `destroyCustomVideoTrack`。 @@ -198,6 +198,6 @@ m_rtcEngine->leaveChannelEx(m_trackConnections[trackIndex]); - [`createCustomVideoTrack`](https://docportal.shengwang.cn/cn/video-call-4.x/API%20Reference/windows_ng/API/toc_video_process.html?platform=Windows#api_irtcengine_createcustomvideotrack) - [`destroyCustomVideoTrack`](https://docportal.shengwang.cn/cn/video-call-4.x/API%20Reference/windows_ng/API/toc_video_process.html?platform=Windows#api_irtcengine_destroycustomvideotrack) -- [getCurrentMonotonicTimeInMs](https://docportal.shengwang.cn/cn/video-call-4.x/API%20Reference/windows_ng/API/toc_video_process.html?platform=Windows#api_irtcengine_getcurrentmonotonictimeinms) -- [joinChannelEx](https://docportal.shengwang.cn/cn/video-call-4.x/API%20Reference/windows_ng/API/toc_multi_channel.html?platform=Windows#api_irtcengineex_joinchannelex) +- [`getCurrentMonotonicTimeInMs`](https://docportal.shengwang.cn/cn/video-call-4.x/API%20Reference/windows_ng/API/toc_video_process.html?platform=Windows#api_irtcengine_getcurrentmonotonictimeinms) +- [`joinChannelEx`](https://docportal.shengwang.cn/cn/video-call-4.x/API%20Reference/windows_ng/API/toc_multi_channel.html?platform=Windows#api_irtcengineex_joinchannelex) - [`pushVideoFrame`](https://docportal.shengwang.cn/cn/video-call-4.x/API%20Reference/windows_ng/API/toc_video_process.html?platform=Windows#api_imediaengine_pushvideoframe) \ No newline at end of file From b07ef44f334e9bf4dc74f39860456392d267e77a Mon Sep 17 00:00:00 2001 From: jinyu Date: Fri, 9 Jun 2023 15:16:47 +0800 Subject: [PATCH 07/11] incorporate review feedback --- .../custom video source/ custom_video_source_windows_ng.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md b/markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md index e020c999db3..395267a7ec2 100644 --- a/markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md +++ b/markdown/RTC 4.x/custom video source/ custom_video_source_windows_ng.md @@ -73,7 +73,7 @@ 参考下图调用时序,在你的 app 中实现自定义视频采集: -![](https://web-cdn.agora.io/docs-files/1684381970999) +![](https://web-cdn.agora.io/docs-files/1686294982670) 参考如下步骤,在你的 app 中实现自定义视频采集功能: @@ -131,7 +131,7 @@ int ret = m_rtcEngine->joinChannelEx(APP_TOKEN, m_trackConnections[trackIndex], ### 3. 实现自采集模块 -声网提供 [YUVReader.cpp](https://github.com/AgoraIO/API-Examples/blob/main/windows/APIExample/APIExample/YUVReader.cpp) 和 [YUVReader.h](https://github.com/AgoraIO/API-Examples/blob/main/windows/APIExample/APIExample/YUVReader.h) 演示从本地文件读取 YUV 格式的视频数据。在实际的生产环境中,声网 SDK 不提供自定义视频处理 API,你需要结合业务需求为你的采集设备创建自定义视频采集模块。 +声网提供 [YUVReader.cpp](https://github.com/AgoraIO/API-Examples/blob/main/windows/APIExample/APIExample/YUVReader.cpp) 和 [YUVReader.h](https://github.com/AgoraIO/API-Examples/blob/main/windows/APIExample/APIExample/YUVReader.h) 演示从本地文件读取 YUV 格式的视频数据。在实际的生产环境中,你需要结合业务需求为你的采集设备创建自定义视频采集模块。 ```cpp // 通过自定义的 YUVReader 类,在 YUVReader 线程中不断读取 YUV 格式视频数据并将数据传递给 OnYUVRead 回调函数进行后续处理 @@ -143,7 +143,7 @@ m_yuvReaders[trackIndex].start(std::bind(&MultiVideoSourceTracksYUVReaderHander: 调用 `pushVideoFrame` 将采集到的视频帧通过视频轨道推送至 SDK。其中, `videoTrackId` 要与步骤 2 加入频道时指定视频轨道 ID 一致,`videoFrame` 中可以设置视频帧的像素格式、数据类型和时间戳等参数。 -
  • 以下代码演示将 YUV 格式转换为 I420 格式的原始视频数据。如需推送其他格式的外部视频帧,详见 VIDEO_PIXEL_FORMAT
  • 为确保音视频同步,声网建议你将 videoFrame 的时间戳参数设置为系统 Monotonic Time。你可以调用 getCurrentMonotonicTimeInMs 获取当前的 Monotonic Time。
+
  • 以下代码演示将 YUV 格式转换为 I420 格式的原始视频数据。声网视频自采集还支持推送其他格式的外部视频帧,详见 VIDEO_PIXEL_FORMAT
  • 为确保音视频同步,声网建议你将 videoFrame 的时间戳参数设置为系统 Monotonic Time。你可以调用 getCurrentMonotonicTimeInMs 获取当前的 Monotonic Time。
```cpp void MultiVideoSourceTracksYUVReaderHander::OnYUVRead(int width, int height, unsigned char* buffer, int size) From 2ea5df07a15f7980a135cd831a3d6f1bcd608412 Mon Sep 17 00:00:00 2001 From: jinyu Date: Fri, 9 Jun 2023 15:32:02 +0800 Subject: [PATCH 08/11] Update custom_video_source_android_ng.md --- .../ custom_video_source_android_ng.md | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md b/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md index 91d2e705033..08ed64c4fe8 100644 --- a/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md +++ b/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md @@ -73,7 +73,7 @@ 参考下图调用时序,在你的 app 中实现自定义视频采集: -![](https://web-cdn.agora.io/docs-files/1684223014176) +![](https://web-cdn.agora.io/docs-files/1686295832877) 参考如下步骤,在你的 app 中实现自定义视频采集功能: @@ -90,8 +90,9 @@ int videoTrackId = RtcEngine.createCustomVideoTrack(); 调用 `joinChannel` 加入频道,或调用 `joinChannelEx` 加入多频道, 在每个频道的 `ChannelMediaOptions` 中,将 `customVideoTrackId` 参数设置为步骤 1 中获得的视频轨道 ID,并将 `publishCustomVideoTrack` 设置为 `true`,即可在多个频道中发布指定的自定义视频轨道。 +加入主频道: + ```java -// 如需在多个频道发布自定义视频轨道,则需要多次设置 ChannelMediaOptions 并多次调用 joinChannelEx ChannelMediaOptions option = new ChannelMediaOptions(); option.clientRoleType = Constants.CLIENT_ROLE_BROADCASTER; option.autoSubscribeAudio = true; @@ -102,18 +103,36 @@ option.publishCustomVideoTrack = true; option.customVideoTrackId = videoTrackId; // 加入主频道 int res = engine.joinChannel(accessToken, channelId, 0, option); -// 或加入多频道 +``` + +加入多频道: + +```java +// 如需在多个频道发布自定义视频轨道,则需要多次设置 ChannelMediaOptions 并多次调用 joinChannelEx +ChannelMediaOptions option = new ChannelMediaOptions(); +option.clientRoleType = Constants.CLIENT_ROLE_BROADCASTER; +option.autoSubscribeAudio = true; +option.autoSubscribeVideo = true; +// 发布自采集视频流 +option.publishCustomVideoTrack = true; +// 设置自定义视频轨道 ID +option.customVideoTrackId = videoTrackId; +// 加入多频道 int res = engine.joinChannelEx(accessToken, connection, option, new IRtcEngineEventHandler() {}); ``` ### 3. 实现自采集模块 -声网提供 [VideoFileReader.java](https://github.com/AgoraIO/API-Examples/blob/main/Android/APIExample/app/src/main/java/io/agora/api/example/utils/VideoFileReader.java) 演示从本地文件读取 YUV 格式的视频数据。在实际的生产环境中,声网 SDK 不提供自定义视频处理 API,你需要结合业务需求使用 Android SDK 为你的设备创建自定义视频模块。 +声网提供 [VideoFileReader.java](https://github.com/AgoraIO/API-Examples/blob/main/Android/APIExample/app/src/main/java/io/agora/api/example/utils/VideoFileReader.java) 演示从本地文件读取 YUV 格式的视频数据。在实际的生产环境中,你需要结合业务需求使用 Android SDK 为你的设备创建自定义视频模块。 ### 4. 通过视频轨道推送视频数据到 SDK 将采集到的视频帧发送至 SDK 之前,通过设置 `VideoFrame` 集成你的视频模块。你可以参考以下代码,将采集到的 YUV 视频数据转换为不同类型的 `VideoFrame`。为确保音视频同步,声网建议你调用 `getCurrentMonotonicTimeInMs` 获取 SDK 当前的 Monotonic Time 后,将该值传入采集的 `VideoFrame` 的时间戳参数。 +调用 `pushExternalVideoFrameEx` 将采集到的视频帧通过视频轨道推送至 SDK。其中, `videoTrackId` 要与步骤 2 加入频道时指定视频轨道 ID 一致,`VideoFrame` 中可以设置视频帧的像素格式、数据类型和时间戳等参数。 + +
  • 以下代码演示将 YUV 格式转换为 NV21、NV12、Texture 和 I420 格式的视频数据。。
  • 为确保音视频同步,声网建议你将 VideoFrame 的时间戳参数设置为系统 Monotonic Time。你可以调用 getCurrentMonotonicTimeInMs 获取当前的 Monotonic Time。
+ ```java // 创建不同类型的 VideoFrame VideoFrame.Buffer frameBuffer; @@ -201,11 +220,7 @@ if ("NV21".equals(selectedItem)) { long currentMonotonicTimeInMs = engine.getCurrentMonotonicTimeInMs(); // 创建 VideoFrame,并将 SDK 当前的 Monotonic Time 赋值到 VideoFrame 的时间戳参数 VideoFrame videoFrame = new VideoFrame(frameBuffer, 0, currentMonotonicTimeInMs); -``` -调用 `pushExternalVideoFrameEx` 并将 `videoTrackId` 指定为步骤 2 中指定的视频轨道 ID,将视频帧通过视频轨道发送给 SDK。 - -```java // 通过视频轨道推送视频帧到 SDK int ret = engine.pushExternalVideoFrameEx(videoFrame, videoTrack); if (ret < 0) { @@ -218,7 +233,10 @@ if (ret < 0) { 如需停止自定义视频采集,调用 `destroyCustomVideoTrack` 来销毁视频轨道。如需销毁多个视频轨道,可多次调用 `destroyCustomVideoTrack`。 ```java +// 销毁自定义视频轨道 engine.destroyCustomVideoTrack(videoTrack); +// 离开频道 +engine.leaveChannelEx(connection); ``` @@ -238,4 +256,6 @@ engine.destroyCustomVideoTrack(videoTrack); - [`createCustomVideoTrack`](https://docs.agora.io/cn/extension_customer/API%20Reference/java_ng/API/toc_video_process.html#api_irtcengine_createcustomvideotrack) - [`destroyCustomVideoTrack`](https://docs.agora.io/cn/extension_customer/API%20Reference/java_ng/API/toc_video_process.html#api_irtcengine_destroycustomvideotrack) +- [`getCurrentMonotonicTimeInMs`](https://docportal.shengwang.cn/cn/video-call-4.x/API%20Reference/java_ng/API/toc_video_process.html#api_irtcengine_getcurrentmonotonictimeinms) +- [`joinChannelEx`](https://docportal.shengwang.cn/cn/video-call-4.x/API%20Reference/java_ng/API/toc_multi_channel.html#api_irtcengineex_joinchannelex) - [`pushExternalVideoFrameEx` [2/2]](https://docs.agora.io/cn/extension_customer/API%20Reference/java_ng/API/toc_multi_channel.html#api_irtcengineex_pushvideoframeex2) \ No newline at end of file From 7ae6d8c3c2a23ee7ec00c8f588570f2f6e056854 Mon Sep 17 00:00:00 2001 From: jinyu Date: Thu, 17 Aug 2023 11:06:05 +0800 Subject: [PATCH 09/11] fix --- .../custom video source/ custom_video_source_android_ng.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md b/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md index 08ed64c4fe8..d925cdc0c48 100644 --- a/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md +++ b/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md @@ -219,7 +219,7 @@ if ("NV21".equals(selectedItem)) { // 获取 SDK 当前的 Monotonic Time long currentMonotonicTimeInMs = engine.getCurrentMonotonicTimeInMs(); // 创建 VideoFrame,并将 SDK 当前的 Monotonic Time 赋值到 VideoFrame 的时间戳参数 -VideoFrame videoFrame = new VideoFrame(frameBuffer, 0, currentMonotonicTimeInMs); +VideoFrame videoFrame = new VideoFrame(frameBuffer, 0, currentMonotonicTimeInMs * 1000000); // 通过视频轨道推送视频帧到 SDK int ret = engine.pushExternalVideoFrameEx(videoFrame, videoTrack); From 45279221a26a105433c06b7c41bdcd68885396d7 Mon Sep 17 00:00:00 2001 From: jinyu Date: Thu, 14 Sep 2023 16:17:41 +0800 Subject: [PATCH 10/11] Update custom_video_source_android_ng.md --- .../ custom_video_source_android_ng.md | 168 +++++++++--------- 1 file changed, 80 insertions(+), 88 deletions(-) diff --git a/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md b/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md index d925cdc0c48..dd08289d057 100644 --- a/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md +++ b/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md @@ -127,104 +127,96 @@ int res = engine.joinChannelEx(accessToken, connection, option, new IRtcEngineEv ### 4. 通过视频轨道推送视频数据到 SDK -将采集到的视频帧发送至 SDK 之前,通过设置 `VideoFrame` 集成你的视频模块。你可以参考以下代码,将采集到的 YUV 视频数据转换为不同类型的 `VideoFrame`。为确保音视频同步,声网建议你调用 `getCurrentMonotonicTimeInMs` 获取 SDK 当前的 Monotonic Time 后,将该值传入采集的 `VideoFrame` 的时间戳参数。 +将采集到的视频帧发送至 SDK 之前,通过设置 `VideoFrame` 集成你的视频模块。你可以参考以下代码,推送不同类型的自采集视频数据。为确保音视频同步,声网建议你调用 `getCurrentMonotonicTimeInMs` 获取 SDK 当前的 Monotonic Time 后,将该值传入采集的 `VideoFrame` 的时间戳参数。 调用 `pushExternalVideoFrameEx` 将采集到的视频帧通过视频轨道推送至 SDK。其中, `videoTrackId` 要与步骤 2 加入频道时指定视频轨道 ID 一致,`VideoFrame` 中可以设置视频帧的像素格式、数据类型和时间戳等参数。 -
  • 以下代码演示将 YUV 格式转换为 NV21、NV12、Texture 和 I420 格式的视频数据。。
  • 为确保音视频同步,声网建议你将 VideoFrame 的时间戳参数设置为系统 Monotonic Time。你可以调用 getCurrentMonotonicTimeInMs 获取当前的 Monotonic Time。
+
  • 以下代码演示推送 I420、NV21、NV12 和 Texture 格式的视频数据。。
  • 为确保音视频同步,声网建议你将 VideoFrame 的时间戳参数设置为系统 Monotonic Time。你可以调用 getCurrentMonotonicTimeInMs 获取当前的 Monotonic Time。
```java -// 创建不同类型的 VideoFrame -VideoFrame.Buffer frameBuffer; -// 将 YUV 视频数据转换为 NV21 格式 -if ("NV21".equals(selectedItem)) { - int srcStrideY = width; - int srcHeightY = height; - int srcSizeY = srcStrideY * srcHeightY; - ByteBuffer srcY = ByteBuffer.allocateDirect(srcSizeY); - srcY.put(yuv, 0, srcSizeY); - - int srcStrideU = width / 2; - int srcHeightU = height / 2; - int srcSizeU = srcStrideU * srcHeightU; - ByteBuffer srcU = ByteBuffer.allocateDirect(srcSizeU); - srcU.put(yuv, srcSizeY, srcSizeU); - - int srcStrideV = width / 2; - int srcHeightV = height / 2; - int srcSizeV = srcStrideV * srcHeightV; - ByteBuffer srcV = ByteBuffer.allocateDirect(srcSizeV); - srcV.put(yuv, srcSizeY + srcSizeU, srcSizeV); - - int desSize = srcSizeY + srcSizeU + srcSizeV; - ByteBuffer des = ByteBuffer.allocateDirect(desSize); - YuvHelper.I420ToNV12(srcY, srcStrideY, srcV, srcStrideV, srcU, srcStrideU, des, width, height); - - byte[] nv21 = new byte[desSize]; - des.position(0); - des.get(nv21); - - frameBuffer = new NV21Buffer(nv21, width, height, null); -} - // 将 YUV 视频数据转换为 NV12 格式 - else if ("NV12".equals(selectedItem)) { - int srcStrideY = width; - int srcHeightY = height; - int srcSizeY = srcStrideY * srcHeightY; - ByteBuffer srcY = ByteBuffer.allocateDirect(srcSizeY); - srcY.put(yuv, 0, srcSizeY); - - int srcStrideU = width / 2; - int srcHeightU = height / 2; - int srcSizeU = srcStrideU * srcHeightU; - ByteBuffer srcU = ByteBuffer.allocateDirect(srcSizeU); - srcU.put(yuv, srcSizeY, srcSizeU); - - int srcStrideV = width / 2; - int srcHeightV = height / 2; - int srcSizeV = srcStrideV * srcHeightV; - ByteBuffer srcV = ByteBuffer.allocateDirect(srcSizeV); - srcV.put(yuv, srcSizeY + srcSizeU, srcSizeV); - - int desSize = srcSizeY + srcSizeU + srcSizeV; - ByteBuffer des = ByteBuffer.allocateDirect(desSize); - YuvHelper.I420ToNV12(srcY, srcStrideY, srcU, srcStrideU, srcV, srcStrideV, des, width, height); - - frameBuffer = new NV12Buffer(width, height, width, height, des, null); +private void pushVideoFrameByI420(byte[] yuv, int width, int height){ + // 创建一个 i420Buffer 对象,将原始的 YUV 数据存储到 I420 格式的缓冲区中 + JavaI420Buffer i420Buffer = JavaI420Buffer.allocate(width, height); + i420Buffer.getDataY().put(yuv, 0, i420Buffer.getDataY().limit()); + i420Buffer.getDataU().put(yuv, i420Buffer.getDataY().limit(), i420Buffer.getDataU().limit()); + i420Buffer.getDataV().put(yuv, i420Buffer.getDataY().limit() + i420Buffer.getDataU().limit(), i420Buffer.getDataV().limit()); + + // 获取 SDK 当前的 Monotonic Time + long currentMonotonicTimeInMs = engine.getCurrentMonotonicTimeInMs(); + // 创建一个 VideoFrame 对象,传入要推送的 I420 视频帧和视频帧的 Monotonic Time (单位为纳秒) + VideoFrame videoFrame = new VideoFrame(i420Buffer, 0, currentMonotonicTimeInMs * 1000000); + + // 通过视频轨道将视频帧推送到 SDK + int ret = engine.pushExternalVideoFrameEx(videoFrame, videoTrack); + // 推送成功后,释放 i420Buffer 对象占用的内存资源 + i420Buffer.release(); + + if (!success) { + Log.w(TAG, "pushExternalVideoFrame error"); } - // 将 YUV 视频数据转换为 Texture 格式 - else if ("Texture2D".equals(selectedItem)) { - if (textureBufferHelper == null) { - textureBufferHelper = TextureBufferHelper.create("PushExternalVideoYUV", EglBaseProvider.instance().getRootEglBase().getEglBaseContext()); - } - if (yuvFboProgram == null) { - textureBufferHelper.invoke((Callable) () -> { - yuvFboProgram = new YuvFboProgram(); - return null; - }); - } - Integer textureId = textureBufferHelper.invoke(() -> yuvFboProgram.drawYuv(yuv, width, height)); - frameBuffer = textureBufferHelper.wrapTextureBuffer(width, height, VideoFrame.TextureBuffer.Type.RGB, textureId, new Matrix()); +} + + +private void pushVideoFrameByNV21(byte[] nv21, int width, int height){ + // 创建一个 frameBuffer 对象,将原始的 YUV 数据存储到 NV21 格式的缓冲区中 + VideoFrame.Buffer frameBuffer = new NV21Buffer(nv21, width, height, null); + + // 获取 SDK 当前的 Monotonic Time + long currentMonotonicTimeInMs = engine.getCurrentMonotonicTimeInMs(); + // 创建一个 VideoFrame 对象,传入要推送的 NV21 视频帧和视频帧的 Monotonic Time (单位为纳秒) + VideoFrame videoFrame = new VideoFrame(frameBuffer, 0, currentMonotonicTimeInMs * 1000000); + + // 通过视频轨道将视频帧推送到 SDK + int ret = engine.pushExternalVideoFrameEx(videoFrame, videoTrack); + + if (!success) { + Log.w(TAG, "pushExternalVideoFrame error"); } - // 将 YUV 视频数据转换为 I420 格式 - else if("I420".equals(selectedItem)) - { - JavaI420Buffer i420Buffer = JavaI420Buffer.allocate(width, height); - i420Buffer.getDataY().put(yuv, 0, i420Buffer.getDataY().limit()); - i420Buffer.getDataU().put(yuv, i420Buffer.getDataY().limit(), i420Buffer.getDataU().limit()); - i420Buffer.getDataV().put(yuv, i420Buffer.getDataY().limit() + i420Buffer.getDataU().limit(), i420Buffer.getDataV().limit()); - frameBuffer = i420Buffer; +} + + +private void pushVideoFrameByNV12(ByteBuffer nv12, int width, int height){ + // 创建一个 frameBuffer 对象,将原始的 YUV 数据存储到 NV12 格式的缓冲区中 + VideoFrame.Buffer frameBuffer = new NV12Buffer(width, height, width, height, nv12, null); + + // 获取 SDK 当前的 Monotonic Time + long currentMonotonicTimeInMs = engine.getCurrentMonotonicTimeInMs(); + // 创建一个 VideoFrame 对象,传入要推送的 NV12 视频帧和视频帧的 Monotonic Time (单位为纳秒) + VideoFrame videoFrame = new VideoFrame(frameBuffer, 0, currentMonotonicTimeInMs * 1000000); + + // 通过视频轨道将视频帧推送到 SDK + int ret = engine.pushExternalVideoFrameEx(videoFrame, videoTrack); + + if (!success) { + Log.w(TAG, "pushExternalVideoFrame error"); } +} -// 获取 SDK 当前的 Monotonic Time -long currentMonotonicTimeInMs = engine.getCurrentMonotonicTimeInMs(); -// 创建 VideoFrame,并将 SDK 当前的 Monotonic Time 赋值到 VideoFrame 的时间戳参数 -VideoFrame videoFrame = new VideoFrame(frameBuffer, 0, currentMonotonicTimeInMs * 1000000); -// 通过视频轨道推送视频帧到 SDK -int ret = engine.pushExternalVideoFrameEx(videoFrame, videoTrack); -if (ret < 0) { - Log.w(TAG, "pushExternalVideoFrameEx error code=" + ret); +private void pushVideoFrameByTexture(int textureId, VideoFrame.TextureBuffer.Type textureType, int width, int height){ + // 创建一个 frameBuffer 对象,用于存储 Texture 格式的视频帧 + VideoFrame.Buffer frameBuffer = new TextureBuffer( + EglBaseProvider.getCurrentEglContext(), + width, + height, + textureType, + textureId, + new Matrix(), + null, + null, + null + ); + + // 获取 SDK 当前的 Monotonic Time + long currentMonotonicTimeInMs = engine.getCurrentMonotonicTimeInMs(); + VideoFrame videoFrame = new VideoFrame(frameBuffer, 0, currentMonotonicTimeInMs * 1000000); + + // 通过视频轨道将视频帧推送到 SDK + int ret = engine.pushExternalVideoFrameEx(videoFrame, videoTrack); + + if (!success) { + Log.w(TAG, "pushExternalVideoFrame error"); + } } ``` From 848761dba9ec9ee3f933ae9d8f3437f15ac1db71 Mon Sep 17 00:00:00 2001 From: jinyu Date: Thu, 14 Sep 2023 16:25:27 +0800 Subject: [PATCH 11/11] Update custom_video_source_android_ng.md --- .../custom video source/ custom_video_source_android_ng.md | 1 + 1 file changed, 1 insertion(+) diff --git a/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md b/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md index dd08289d057..ae13050595c 100644 --- a/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md +++ b/markdown/RTC 4.x/custom video source/ custom_video_source_android_ng.md @@ -209,6 +209,7 @@ private void pushVideoFrameByTexture(int textureId, VideoFrame.TextureBuffer.Typ // 获取 SDK 当前的 Monotonic Time long currentMonotonicTimeInMs = engine.getCurrentMonotonicTimeInMs(); + // 创建一个 VideoFrame 对象,传入要推送的 Texture 视频帧和视频帧的 Monotonic Time (单位为纳秒) VideoFrame videoFrame = new VideoFrame(frameBuffer, 0, currentMonotonicTimeInMs * 1000000); // 通过视频轨道将视频帧推送到 SDK