You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 09:15:36