Zoom Windows Meeting SDK 屏幕共享原始数据采集实战:基于 IZoomSDKRenderer 捕获与处理 YUV420 分享帧

Zoom Windows Meeting SDK 屏幕共享原始数据采集实战:基于 IZoomSDKRenderer 捕获与处理 YUV420 分享帧 Zoom Windows Meeting SDK 屏幕共享原始数据采集实战基于 IZoomSDKRenderer 捕获与处理 YUV420 分享帧【免费下载链接】knowledge-work-pluginsOpen source repository of plugins primarily intended for knowledge workers to use in Claude Cowork项目地址: https://gitcode.com/GitHub_Trending/kn/knowledge-work-plugins导读本文围绕 Zoom Windows Meeting SDKC 原生 SDK中屏幕共享原始数据Screen Share Raw Data采集这一主题完整讲解如何通过IZoomSDKRenderer与IZoomSDKRendererDelegate订阅共享者的屏幕内容实时获取 YUV420I420格式的原始帧并进一步实现帧落盘、YUV→RGB 转换、分享事件监听与权限处理等能力。读完本文你将掌握一套从加入会议 → 获取分享控制器 → 开启 Raw Recording → 订阅分享流 → 处理原始帧 → 优雅退出的完整可运行工程范式可直接用于构建会议录制工具、屏幕内容分析、自定义渲染引擎等桌面应用。本文核心依据是仓库中的实战文档 share-raw-data-capture.md并结合同目录下的 raw-video-capture.md、send-raw-data.md 以及 SKILL.md 等文档进行纵深印证与扩充。一、原理概述分享原始数据与视频原始数据的同构关系Zoom Meeting SDK 允许你的应用接收正在共享屏幕的参与者产生的原始屏幕共享数据YUV420 帧。这套能力与视频原始数据采集使用的是同一套IZoomSDKRenderer与IZoomSDKRendererDelegate接口模式唯一的区别在于订阅时传入的数据类型不同采集摄像头视频订阅RAW_DATA_TYPE_VIDEO采集屏幕共享内容订阅RAW_DATA_TYPE_SHARE从仓库文档 custom-ui-vs-raw-data.md 的自渲染Self-Rendered路线可以看到SDK 在内部完成视频/屏幕解码后通过IZoomSDKRendererDelegate::onRawDataFrameReceived(YUVRawDataI420*)把原始像素数据Y、U、V 三个平面交给你由你自行决定用 D3D11、OpenGL、GDI 等任意引擎渲染或用于滤镜、水印、录制、计算机视觉等下游处理。屏幕共享原始数据采集正是这条自渲染能力在分享场景下的具体落地。调用链架构整个分享原始数据采集由四个关键对象协作完成IMeetingService ├── GetMeetingShareController() → IMeetingShareController │ └── GetViewableSharingUserList() │ └── GetMeetingRecordingController() → IMeetingRecordingController └── StartRawRecording() createRenderer() → IZoomSDKRenderer ├── subscribe(userId, RAW_DATA_TYPE_SHARE) └── unSubscribe() IZoomSDKRendererDelegate (your implementation) └── onRawDataFrameReceived(YUVRawDataI420*)这条链路遵循仓库 sdk-architecture-pattern.md 中总结的通用三步模式① 通过meetingService-Get[Feature]Controller()获取控制器单例 → ② 实现事件监听接口 → ③ 注册监听器并调用控制器方法。分享原始数据采集正是先拿到 Share 控制器与 Recording 控制器再实现 Renderer 委托最后subscribe()订阅。必需头文件#include meeting_service_interface.h #include meeting_service_components/meeting_sharing_interface.h #include meeting_service_components/meeting_recording_interface.h #include rawdata/zoom_rawdata_api.h #include rawdata/rawdata_renderer_interface.h注意SKILL.md 中特别强调Windows 平台头文件包含顺序会影响编译windows.h必须最先包含cstdint紧随其后SDK 头文件依赖uint32_t随后再引入 SDK 相关头文件否则可能触发构建错误。若在工程中同时使用YUVRawDataI420类型还需按 raw-video-capture.md 的做法引入zoom_sdk_raw_data_def.h。二、Step 1实现 Renderer 委托接收分享帧的核心类IZoomSDKRendererDelegate是你的应用与 SDK 原始数据通道之间的接收端。它有三个必须实现的关键回调onRawDataFrameReceived(YUVRawDataI420* data)每当收到一帧分享画面时被调用是数据处理的主入口onRawDataStatusChanged(RawDataStatus status)原始数据通道开启/关闭时被调用onRendererBeDestroyed()渲染器销毁前被调用适合做资源清理。下面给出文档中的完整委托实现并附带帧统计与文件输出开关两个实用扩展。// ZoomSDKShareRendererDelegate.h #pragma once #include rawdata/rawdata_renderer_interface.h #include fstream #include string class ZoomSDKShareRendererDelegate : public ZOOMSDK::IZoomSDKRendererDelegate { public: ZoomSDKShareRendererDelegate(); virtual ~ZoomSDKShareRendererDelegate(); // Called when a share frame is received virtual void onRawDataFrameReceived(ZOOMSDK::YUVRawDataI420* data) override; // Called when renderer status changes virtual void onRawDataStatusChanged( ZOOMSDK::RawDataStatus status ) override; // Called when resolution changes virtual void onRendererBeDestroyed() override; // Frame statistics unsigned int getFrameCount() const { return m_frameCount; } void resetFrameCount() { m_frameCount 0; } // Enable/disable file output void enableFileOutput(const std::string filename); void disableFileOutput(); private: unsigned int m_frameCount; std::ofstream m_outputFile; bool m_writeToFile; int m_lastWidth; int m_lastHeight; };// ZoomSDKShareRendererDelegate.cpp #include ZoomSDKShareRendererDelegate.h #include iostream using namespace ZOOMSDK; ZoomSDKShareRendererDelegate::ZoomSDKShareRendererDelegate() : m_frameCount(0) , m_writeToFile(false) , m_lastWidth(0) , m_lastHeight(0) {} ZoomSDKShareRendererDelegate::~ZoomSDKShareRendererDelegate() { disableFileOutput(); } void ZoomSDKShareRendererDelegate::onRawDataFrameReceived(YUVRawDataI420* data) { if (!data) return; m_frameCount; // Get frame dimensions unsigned int width >ffplay -f rawvideo -pixel_format yuv420p -video_size 1920x1080 -framerate 30 share_capture.yuv三、Step 2初始化分享采集获取控制器进入会议后MEETING_STATUS_INMEETING状态第一步是拿到 Share 控制器与 Recording 控制器并创建委托实例。这一步本身不产生数据流只是为后续订阅做好准备。// Global variables ZoomSDKShareRendererDelegate* g_shareDelegate nullptr; IZoomSDKRenderer* g_shareRenderer nullptr; IMeetingShareController* g_shareController nullptr; IMeetingRecordingController* g_recordController nullptr; void initializeShareCapture(IMeetingService* meetingService) { // Get controllers g_shareController meetingService-GetMeetingShareController(); g_recordController meetingService-GetMeetingRecordingController(); if (!g_shareController || !g_recordController) { std::cerr Failed to get controllers std::endl; return; } // Create delegate g_shareDelegate new ZoomSDKShareRendererDelegate(); std::cout Share capture initialized std::endl; }两个控制器分别承担不同职责对应文档中的调用链架构控制器获取方式在本流程中的作用IMeetingShareControllerGetMeetingShareController()查询正在共享的用户列表GetViewableSharingUserList()并可通过SetEvent()注册分享状态监听IMeetingRecordingControllerGetMeetingRecordingController()开启原始录制StartRawRecording()这是原始数据通道生效的前提四、Step 3查找共享用户并订阅分享流4.1 关键前置必须先StartRawRecording()这是最容易踩的坑之一与视频采集一致分享原始数据同样要求在订阅之前调用StartRawRecording()。从底层实现看StartRawRecording()并不会在磁盘上生成 MP4 录制文件它只是开启 SDK 回调侧的原始数据通道不调用它onRawDataFrameReceived()永远不会被触发。raw-video-capture.md 建议在调用后等待约 500ms 再继续订阅给录制初始化留出时间。4.2 查询共享者屏幕共享与视频不同视频通过IMeetingParticipantsController::GetParticipantsList()获取参与者列表而分享者列表必须通过 Share 控制器的GetViewableSharingUserList()获取。这正对应文档结尾与视频采集的差异表中的User List一栏。unsigned int getSharingUserId() { if (!g_shareController) return 0; // Get list of users currently sharing IListunsigned int* sharingUsers g_shareController-GetViewableSharingUserList(); if (!sharingUsers || sharingUsers-GetCount() 0) { std::cout No one is sharing std::endl; return 0; } // Get first sharing user unsigned int userId sharingUsers-GetItem(0); std::cout User userId is sharing std::endl; return userId; }GetViewableSharingUserList()返回的是当前正在共享的用户 ID 列表。多共享者场景下可以遍历该列表逐个订阅若返回数量为 0说明此刻无人共享应等待分享事件见第五节触发后再订阅。4.3 创建渲染器并订阅订阅流程遵循先开录制 → 建渲染器 → 取分享者 → 订阅的固定顺序void startShareCapture() { if (!g_shareDelegate) return; // Start raw recording first (required for raw data access) SDKError err g_recordController-StartRawRecording(); if (err ! SDKERR_SUCCESS) { std::cerr Failed to start raw recording: err std::endl; return; } // Create renderer err createRenderer(g_shareRenderer, g_shareDelegate); if (err ! SDKERR_SUCCESS) { std::cerr Failed to create renderer: err std::endl; return; } // Get sharing user unsigned int userId getSharingUserId(); if (userId 0) { std::cout No sharing user to subscribe to std::endl; return; } // Subscribe to share raw data err g_shareRenderer-subscribe(userId, RAW_DATA_TYPE_SHARE); if (err SDKERR_SUCCESS) { std::cout Subscribed to share from user userId std::endl; // Optionally save to file g_shareDelegate-enableFileOutput(share_capture.yuv); } else { std::cerr Failed to subscribe: err std::endl; } }几个实现细节值得注意createRenderer(g_shareRenderer, g_shareDelegate)是全局函数而非CreateRenderer()或new由 SDK 负责创建渲染器实例这一点与 raw-video-capture.md 中的说明一致subscribe(userId, RAW_DATA_TYPE_SHARE)的第二个参数是数据类型枚举RAW_DATA_TYPE_SHARE与RAW_DATA_TYPE_VIDEO的区别见下文Raw Data Types小节订阅成功后才建议调用enableFileOutput()因为只有数据通道已建立文件输出才有意义与视频不同分享流不通过setRawDataResolution()设置分辨率——分享分辨率由共享者屏幕/窗口的实际尺寸动态决定SDK 不提供降采样控制这也是分辨率会动态变化这一特性见第七节陷阱清单的根源。4.4 停止采集void stopShareCapture() { if (g_shareRenderer) { g_shareRenderer-unSubscribe(); std::cout Unsubscribed from share std::endl; } if (g_shareDelegate) { g_shareDelegate-disableFileOutput(); } }unSubscribe()会解除与分享者的绑定并触发onRendererBeDestroyed()回调因此在委托的onRendererBeDestroyed()中同样调用了disableFileOutput()做兜底清理避免输出文件句柄泄漏。五、Step 4监听分享事件动态响应共享的开始与停止真实场景中分享者是动态的可能有人中途开始共享也可能中途停止。因此需要实现IMeetingShareCtrlEvent监听器通过IMeetingShareController::SetEvent()注册从而在分享状态变化时自动启动/停止采集。监听器头文件// MeetingShareCtrlEventListener.h #pragma once #include meeting_service_components/meeting_sharing_interface.h class MeetingShareCtrlEventListener : public ZOOMSDK::IMeetingShareCtrlEvent { public: using ShareStartedCallback std::functionvoid(unsigned int); using ShareStoppedCallback std::functionvoid(); MeetingShareCtrlEventListener( ShareStartedCallback onStarted nullptr, ShareStoppedCallback onStopped nullptr ); // Sharing status changed virtual void onSharingStatus( ZOOMSDK::SharingStatus status, unsigned int userId ) override; // Someone started sharing virtual void onLockShareStatus(bool bLocked) override; // Share content changed virtual void onShareContentNotification( ZOOMSDK::ShareInfo* shareInfo ) override; // Multi-share virtual void onMultiShareSwitchToSingleShareNeedConfirm( ZOOMSDK::IShareSwitchMultiToSingleConfirmHandler* handler ) override; virtual void onShareSettingTypeChangedNotification( ZOOMSDK::ShareSettingType type ) override; virtual void onSharedVideoEnded() override; private: ShareStartedCallback m_onStarted; ShareStoppedCallback m_onStopped; };监听器实现// MeetingShareCtrlEventListener.cpp #include MeetingShareCtrlEventListener.h #include iostream using namespace ZOOMSDK; MeetingShareCtrlEventListener::MeetingShareCtrlEventListener( ShareStartedCallback onStarted, ShareStoppedCallback onStopped ) : m_onStarted(onStarted), m_onStopped(onStopped) {} void MeetingShareCtrlEventListener::onSharingStatus( SharingStatus status, unsigned int userId ) { switch (status) { case Sharing_Self_Send_Begin: std::cout Started sharing (self) std::endl; break; case Sharing_Self_Send_End: std::cout Stopped sharing (self) std::endl; break; case Sharing_Other_Share_Begin: std::cout User userId started sharing std::endl; if (m_onStarted) m_onStarted(userId); break; case Sharing_Other_Share_End: std::cout User userId stopped sharing std::endl; if (m_onStopped) m_onStopped(); break; case Sharing_View_Other_Sharing: std::cout Viewing share from user userId std::endl; break; case Sharing_Pause: std::cout Sharing paused std::endl; break; case Sharing_Resume: std::cout Sharing resumed std::endl; break; default: std::cout Sharing status: status std::endl; } } void MeetingShareCtrlEventListener::onLockShareStatus(bool bLocked) { std::cout Share lock: (bLocked ? LOCKED : UNLOCKED) std::endl; } void MeetingShareCtrlEventListener::onShareContentNotification(ShareInfo* shareInfo) { if (shareInfo) { std::cout Share content changed std::endl; } } void MeetingShareCtrlEventListener::onMultiShareSwitchToSingleShareNeedConfirm( IShareSwitchMultiToSingleConfirmHandler* handler ) { // Handle multi-share to single-share switch } void MeetingShareCtrlEventListener::onShareSettingTypeChangedNotification( ShareSettingType type ) { std::cout Share setting changed std::endl; } void MeetingShareCtrlEventListener::onSharedVideoEnded() { std::cout Shared video ended std::endl; }完整集成示例把各步骤串起来// Global share event listener MeetingShareCtrlEventListener* g_shareEventListener nullptr; void onInMeeting(IMeetingService* meetingService) { // Initialize share capture initializeShareCapture(meetingService); // Set up share event listener g_shareEventListener new MeetingShareCtrlEventListener( // On share started [](unsigned int userId) { std::cout Starting capture for user userId std::endl; startShareCapture(); }, // On share stopped []() { std::cout Stopping capture std::endl; stopShareCapture(); } ); g_shareController-SetEvent(g_shareEventListener); // Check if someone is already sharing unsigned int sharingUser getSharingUserId(); if (sharingUser ! 0) { startShareCapture(); } } // Cleanup void cleanup() { stopShareCapture(); if (g_shareRenderer) { // Renderer will be destroyed when unsubscribed g_shareRenderer nullptr; } delete g_shareDelegate; g_shareDelegate nullptr; delete g_shareEventListener; g_shareEventListener nullptr; }这个集成示例体现了两个健壮性设计双路径兜底既通过事件回调响应分享开始/停止又会在进入会议时主动查询一次getSharingUserId()——覆盖进入会议时已有人正在共享的初始场景回调驱动而非轮询onSharingStatus中的Sharing_Other_Share_Begin/Sharing_Other_Share_End是自动启动/停止采集的触发点无需应用侧轮询共享状态。注意SDK 的异步回调依赖 Windows 消息泵派发。根据 windows-message-loop.md 的说明主线程必须持续执行PeekMessage()/GetMessage()TranslateMessage()DispatchMessage()否则onSharingStatus、onRawDataFrameReceived等所有回调都会被排队而永不触发表现为分享事件不响应、无任何帧到达。六、权限要求Raw Recording 与录制权限要接收屏幕共享原始数据必须同时满足两类前提Raw Recording 开启订阅前必须调用StartRawRecording()录制权限你必须是主持人host、联席主持人co-host或已被授予本地录制权限。对于非主持人场景可以在运行时主动向主持人申请权限bool checkPermission() { SDKError err g_recordController-CanStartRecording(false, 0); if (err ! SDKERR_SUCCESS) { std::cout Requesting recording permission... std::endl; g_recordController-RequestLocalRecordingPrivilege(); return false; } return true; }关于权限体系的更完整说明可参考 raw-video-capture.md其中的权限模型对视频与分享通用Raw Recording依赖本地录制权限由主持人/联席主持人授予或向主持人申请Raw Streaming另一种原始数据通道依赖直播live streaming权限要求会议主持人拥有 Pro/Business/Education/Enterprise 授权账号对应IMeetingLiveStreamController::StartRawLiveStream()OAuth App Privilege Token进阶可通过 OAuth 应用申请Meeting_token:read:local_recording等权限后调用 REST API 获取令牌通过app_privilege_token入会参数跳过向主持人申请环节权限申请结果通过onRecordPrivilegeChanged(bool bCanRec)等回调返回建议在回调中再触发真正的采集流程相关回调清单可对照 local-recording.md 中的IMeetingRecordingCtrlEvent实现。七、YUV420 到 RGB/图像的转换分享原始帧是 YUV420I420格式无法直接用于大多数显示与图像处理管线需要转换为 RGB。文档给出的 OpenCV 方案是把 Y、U、V 三个平面按 I420 的物理内存排布拷贝进一个height height/2行、宽width的CV_8UC1矩阵再交给cv::cvtColor做一次性色彩空间转换。// Using OpenCV #include opencv2/opencv.hpp void saveFrameAsImage(YUVRawDataI420* data, const std::string filename) { int width >enum SharingStatus { Sharing_Self_Send_Begin, // You started sharing Sharing_Self_Send_End, // You stopped sharing Sharing_Other_Share_Begin, // Someone else started sharing Sharing_Other_Share_End, // Someone else stopped sharing Sharing_View_Other_Sharing, // Viewing someones share Sharing_Pause, // Sharing paused Sharing_Resume, // Sharing resumed Sharing_ContentTypeChange, // Content type changed Sharing_SelfStartAudioShare, // Started audio share Sharing_SelfStopAudioShare // Stopped audio share };其中与采集逻辑最相关的两个是Sharing_Other_Share_Begin他人开始共享 → 触发订阅与Sharing_Other_Share_End他人结束共享 → 触发退订。Sharing_Pause/Sharing_Resume对应的暂停/恢复场景可根据业务需要决定是否在暂停时暂停文件写入。RawDataType原始数据类型enum RawDataType { RAW_DATA_TYPE_VIDEO 0, // Video frames RAW_DATA_TYPE_SHARE // Screen share frames };RAW_DATA_TYPE_SHARE即本文所用订阅类型需要同时采集摄像头画面时再创建另一个渲染器并以RAW_DATA_TYPE_VIDEO订阅详见 raw-video-capture.md。此外若需要向会议发送自定义屏幕共享内容可参考 send-raw-data.md 中IZoomSDKShareSourceStartShareWithPreviewEnabled()的虚拟共享源方案其帧格式同样是 YUV420。九、常见陷阱清单实战排错文档总结的七条高频踩坑点结合仓库其他文档可归纳如下#陷阱说明与对策1无分享数据订阅前确保确实有人在共享。可通过GetViewableSharingUserList()或onSharingStatus回调确认2未开 Raw Recording订阅前必须调用StartRawRecording()否则数据通道未建立、帧永远不会到达3权限不足原始数据访问依赖录制权限。非主持人需先CanStartRecording(false, 0)检查再RequestLocalRecordingPrivilege()申请4多共享者用GetViewableSharingUserList()获取全部共享者遍历后逐个订阅5分辨率动态变化屏幕共享分辨率会随窗口大小/内容动态改变务必在回调中监听宽高变化并适配缓冲对应m_lastWidth/m_lastHeight的检测逻辑6帧率不稳定分享帧率通常低于摄像头视频且随内容波动静态文档低、动态视频高处理逻辑不应假设固定帧率7订阅时机应在分享开始之后订阅而非提前订阅等待数据除以上七条外从仓库 raw-video-capture.md 的排查清单还可复用以下经验回调不触发优先检查 Windows 消息循环是否存在见 windows-message-loop.md帧内容花屏检查是否按 Y→U→V 顺序写入、UV 平面尺寸是否为width*height/4而非width*height/2性能问题/丢帧不要在onRawDataFrameReceived()内做重活如 H.264 编码、网络上传官方建议的做法是回调内只做快速拷贝入队由独立工作线程消费——因为重操作会阻塞回调导致帧延迟或丢失。十、与视频采集的核心差异一览方面视频采集Video Capture分享采集Share Capture数据类型RAW_DATA_TYPE_VIDEORAW_DATA_TYPE_SHARE数据来源摄像头Webcam屏幕/窗口/应用Screen/Window/App分辨率固定摄像头分辨率可setRawDataResolution()指定动态变化随窗口尺寸SDK 不提供降采样帧率相对稳定约 30fps可变取决于共享内容用户列表来源参与者控制器Participants controller分享控制器Share controller底层接口IZoomSDKRendererIZoomSDKRendererDelegate同一套接口仅订阅类型不同核心结论两种采集共用同一套IZoomSDKRenderer/IZoomSDKRendererDelegate渲染器体系。掌握本文的分享采集流程后对照 raw-video-capture.md 即可在同一工程中同时实现摄像头与屏幕共享的双路原始数据采集。十一、延伸阅读原始视频采集完整指南——YUV420 格式详解、Raw Recording 与 Raw Streaming 权限模型、旋转处理、性能建议SDK 通用架构模式——获取控制器 → 实现监听器 → 注册使用三步法适用于全部 35 功能控制器SDK 渲染 vs 自渲染对比——理解原始数据采集在自定义 UI 体系中的定位与混合模式发送原始数据虚拟摄像头/麦克风/共享源——与本文接收方向对应的发送端实现Windows 消息循环——回调不触发的第一排查项Windows Meeting SDK 总览——SDK 初始化、JWT 认证、加入会议等前置流程的完整代码。本文档基于 Zoom Windows Meeting SDK v6.7.x 编写与 raw-video-capture.md 标注版本一致。SDK/API 名称可能随版本演进变化正式发布前请以当前所集成 SDK 的头文件为准进行校验。【免费下载链接】knowledge-work-pluginsOpen source repository of plugins primarily intended for knowledge workers to use in Claude Cowork项目地址: https://gitcode.com/GitHub_Trending/kn/knowledge-work-plugins创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考