ReplayKit SampleHandler如何向主应用传递广播扩展采集的帧数据
Broadcast Extension和主应用运行在两个独立的沙盒进程里,MMWormhole这类基于App Group UserDefaults实现的通信方案,本身设计目标就是传小体积的配置、状态类字符串/数值数据,不仅同步延迟高,单次写入还有系统大小限制,完全不适合传输屏幕录制的帧数据。
针对原生WebRTC音视频通话的场景,下面是经过生产验证的可行方案:
前置配置
所有跨进程通信方案都需要先配置App Groups能力:
- 给主应用和Broadcast Upload Extension两个target同时开启App Groups能力,配置完全一致的Group ID,格式参考
group.com.yourbundle.screenshare - 确保两个target的签名Profile都包含App Groups权限,否则会出现跨进程读不到共享数据的问题
方案1:共享内存+Darwin通知传输(WebRTC场景最优,端到端延迟<100ms)
这个方案是实时音视频场景的首选,通过内存映射实现零拷贝数据传输,没有序列化/反序列化开销,性能最高,不会触发Extension的内存杀进程机制。
实现步骤:
- 提前在Extension和主应用两侧,基于App Group的共享容器映射同一块固定大小的共享内存,内存大小按推流最高规格预留:1080p@30fps的NV12格式原始帧大小约3MB,开4MB足够;音频PCM帧单独开128KB的独立共享内存即可,不要和视频共用避免读写冲突
- 用Darwin通知中心(
CFNotificationCenterGetDarwinNotifyCenter)做帧同步信号,通知本身只传帧的元数据(宽高、时间戳、平面步长、旋转角度),绝对不要把帧数据塞到通知的userInfo里 - Extension侧的
SampleHandler收到CMSampleBuffer后,直接锁CVPixelBuffer基地址,把Y/UV平面的原始数据拷贝到共享内存,拷贝完成发Darwin通知给主应用 - 主应用侧收到通知后,直接从共享内存读取原始像素数据,包装成WebRTC可识别的
CVPixelBuffer,直接喂给RTCVideoCapturer的代理方法即可,不需要二次拷贝
核心实现代码参考(Extension侧):
// 初始化共享内存,替换成你自己的Group ID private let sharedGroupID = "group.com.yourbundle.screenshare" private let videoSharedMemSize = 4 * 1024 * 1024 // 4MB预留 private var videoSharedData: NSMutableData! private func setupSharedMemory() { let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: sharedGroupID)! let memURL = containerURL.appendingPathComponent("video_frame_buffer") if let existData = NSMutableData(contentsOf: memURL) { videoSharedData = existData } else { videoSharedData = NSMutableData(length: videoSharedMemSize) videoSharedData.write(to: memURL, atomically: true) } } override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer) { guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return } CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) // 拷贝Y/UV平面数据到共享内存 let yStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0) let yHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0) let ySize = yStride * yHeight let uvStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1) let uvHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, 1) let uvSize = uvStride * uvHeight memcpy(videoSharedData.mutableBytes, CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0), ySize) memcpy(videoSharedData.mutableBytes.advanced(by: ySize), CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1), uvSize) CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) // 发帧同步通知,只传元数据 let meta: [String: Any] = [ "width": CVPixelBufferGetWidth(pixelBuffer), "height": CVPixelBufferGetHeight(pixelBuffer), "timestamp": CMSampleBufferGetPresentationTimeStamp(sampleBuffer).seconds, "yStride": yStride, "ySize": ySize, "uvStride": uvStride ] CFNotificationCenterPostNotification( CFNotificationCenterGetDarwinNotifyCenter(), CFNotificationName("kNewScreenFrameNotify" as CFString), nil, meta as CFDictionary, true ) }
主应用侧核心逻辑:
// 提前映射同一块共享内存,逻辑和Extension侧一致 private func setupFrameObserver() { CFNotificationCenterAddObserver( CFNotificationCenterGetDarwinNotifyCenter(), Unmanaged.passUnretained(self).toOpaque(), { (_, observer, _, _, userInfo) in let weakSelf = Unmanaged<YourScreenCapturer>.fromOpaque(observer).takeUnretainedValue() guard let meta = userInfo as? [String: Any] else { return } // 从共享内存读数据,包装成CVPixelBuffer let frameBuffer = weakSelf.buildPixelBufferFromSharedMem(meta: meta) // 直接喂给WebRTC采集器 weakSelf.rtcCapturer.delegate?.capturer(weakSelf.rtcCapturer, didCapture: frameBuffer) }, "kNewScreenFrameNotify" as CFString, nil, .deliverImmediately ) }
方案2:共享文件轮询(备选,实现简单但延迟高)
如果不想处理共享内存的映射逻辑,可以用这个快速实现:
- Extension侧收到帧后,将
CVPixelBuffer转成质量系数0.3~0.5的JPEG数据,覆盖写入App Group共享目录下的固定文件,文件名带递增帧序号 - 主应用侧用
DispatchSourceFileSystemObject监听共享目录的文件写入事件,读到新文件后解码成CVPixelBuffer喂给WebRTC - 这个方案单帧延迟普遍在200~500ms,只适合对实时性要求不高的场景,注意定期清理旧文件避免占用存储空间
避坑提醒
- 绝对不要在Broadcast Extension进程做视频编码、渲染等重操作:系统给Broadcast Extension分配的内存上限只有50MB,超过会被系统直接杀死,Extension只做帧拷贝、发信号的轻量操作即可,所有逻辑尽量丢给主应用处理
- 不要用剪贴板、自定义URL Scheme传帧:剪贴板跨进程访问会触发系统隐私提示,频繁写入会被限流;URL Scheme单次传参大小限制只有几KB,完全不满足帧传输需求
- 音视频帧不要共用同一块共享内存,避免读写指针冲突导致花屏、音画不同步问题
- 如果遇到偶发帧丢失,可以在共享内存头部加个递增的帧序号,主应用侧做简单的丢帧、重传判断即可
内容的提问来源于stack exchange,提问作者Jaswant Singh
相关产品推荐
相关产品推荐

