iOS
一个使用 Liquid Glass 的 SwiftUI 应用,基于 webrtc-sdk 150.7871.01 二进制包,要求 iOS 26 或更高版本,用 Xcode 26 构建。
文件: docs/public/media/call-ios.png打开 ios/WebRTCDemo.xcodeproj,选择 WebRTCDemo scheme,在真机上运行。摄像头和屏幕共享都需要真实硬件。
代码结构
文件位于 ios/WebRTCDemo,其中大部分也会编译进 Mac 应用。
| 文件 | 作用 |
|---|---|
CallViewModel.swift | @Observable 的通话状态,持有 LocalMedia 以及 1:1 或多人通话引擎。 |
LocalMedia.swift | Factory、本地 track、唯一的 RTCVideoSource、共享、音频会话、VP8 优先 |
PeerConnectionClient.swift | WebRTCClient,即 1:1 通话引擎:一个 peer connection 和一个 data channel |
GroupCallClient.swift | 多人通话引擎:SFU WebSocket、publish 和 subscribe 连接 |
SignalingSocket.swift | 基于 URLSessionWebSocketTask 收发 JSON。两个引擎共用。 |
FrameEncryption.swift | E2EE 的 key provider 和 frame cryptor |
EffectsProcessor.swift | Vision + Core Image,作为代理采集器 delegate |
PictureInPicture.swift | 通过 AVSampleBufferDisplayLayer 实现系统画中画 |
VideoView.swift | RTCMTLVideoView 的封装和占位画面的截帧器 |
CallView.swift、GroupCallView.swift | 通话界面 |
WebRTCDemoScreenBroadcast/ | ReplayKit 广播扩展 |
WebRTCClient 在 WebRTC 线程上调用它的 delegate,view model 会先切换到主队列再修改状态。
屏幕共享
iOS 只允许应用通过 ReplayKit 广播上传扩展(broadcast upload extension)采集整个屏幕,包括离开应用之后的画面。扩展运行在独立的进程中,无法运行 WebRTC,所以它通过共享 App Group 中的 Unix domain socket 把帧发送给应用。然后 FlutterBroadcastScreenCapturer 把这些帧送入摄像头所用的同一个 RTCVideoSource。
每一帧都是一条类似 HTTP 的消息:先是 Content-Length、Buffer-Width、Buffer-Height 和 Buffer-Orientation 这几个 header,然后是 JPEG 正文。socket 的应用端代码复制自 flutter-webrtc 插件。请保持其中的读取逻辑原样不动:只要读取端向 stream 请求读取 0 字节,iOS 就会报告 stream 已结束,广播会在第一帧之后立刻停止。
离开应用后共享还能继续,这需要满足两个条件:
- 应用保持存活。
UIBackgroundModes中包含audio和voip,并且通话期间会保持一个playAndRecord音频会话处于激活状态,这样 iOS 就不会挂起应用和它的 socket。 - 编码器保持工作。 在后台时,iOS 会让 VideoToolbox 的 H264 硬件编码器失效,导致每一帧都编码失败,对方看到的画面就会卡住。因此无论是否开启 E2EE,iOS 都会把 VP8(软件编码)排在首位。代价是更耗 CPU 和电量。
应用和扩展必须使用同一个 App Group(group.com.ducmai.webrtc.broadcast)。如果你修改了 team 或 bundle ID,需要同步更新两个 .entitlements 文件、Info.plist 中的 RTCAppGroupIdentifier、SampleHandler.swift,以及 RTCScreenSharingExtension 中的扩展 bundle ID。
音频会话
应用会自己配置音频会话:每次通话前,CallViewModel.configureCallAudio() 都会把 WebRTC 的配置设为 playAndRecord + voiceChat。否则 webrtc-sdk fork 会沿用会话启动时的 category(soloAmbient),而 iOS 不接受它与 Bluetooth HFP 选项的组合,结果通话既没有麦克风也没有声音。见常见问题。
各部分的实现
- 切换视频源。 摄像头、文件和屏幕采集器都向同一个
RTCVideoSource供帧,所以 track、sender 和它的 cryptor 都不会变。切换视频源 - E2EE。
RTCFrameCryptor,使用统一的选项。 - 特效。 在摄像头和 source 之间插入一个代理
RTCVideoCapturerDelegate,在 Metal 上运行 Vision 和 Core Image。背景与特效 - 画中画。 视频通话画中画,仅支持 1:1。画中画
- 多人通话。 每个 subscribe offer 都会被解析出
a=mid和a=msid,因为当一个 m-line 被复用给其他人时,libwebrtc 不会触发新的 receiver 事件。多人通话(SFU)
备注
- WebRTC 来自本地 Swift package
ios/Packages/WebRTC,因为这个版本的 Specs manifest 无法解析。升级时修改它的Package.swift中的 URL 和校验和即可。 Info.plist允许明文 HTTP(NSAllowsArbitraryLoads),所以任何局域网服务器都能用。
