iOS
A SwiftUI app with Liquid Glass, on the webrtc-sdk 150.7871.01 binary. iOS 26 or newer, built with Xcode 26.
File: docs/public/media/call-ios.pngOpen ios/WebRTCDemo.xcodeproj, pick the WebRTCDemo scheme and run on a device. The camera and screen sharing need real hardware.
Where things are
Files in ios/WebRTCDemo. Most of them are also compiled into the Mac app.
| File | What it does |
|---|---|
CallViewModel.swift | @Observable call state. Owns LocalMedia and the 1:1 or group engine. |
LocalMedia.swift | Factory, local tracks, the single RTCVideoSource, sharing, audio session, VP8 first |
PeerConnectionClient.swift | WebRTCClient, the 1:1 engine: one peer connection and data channel |
GroupCallClient.swift | The group engine: SFU WebSocket, publish and subscribe connections |
SignalingSocket.swift | JSON over URLSessionWebSocketTask. Used by both engines. |
FrameEncryption.swift | E2EE key provider and frame cryptors |
EffectsProcessor.swift | Vision + Core Image, as a proxy capturer delegate |
PictureInPicture.swift | System PiP through an AVSampleBufferDisplayLayer |
VideoView.swift | RTCMTLVideoView wrapper and the placeholder snapshotter |
CallView.swift, GroupCallView.swift | The call screens |
WebRTCDemoScreenBroadcast/ | The ReplayKit broadcast extension |
WebRTCClient calls its delegate from WebRTC threads, and the view model hops to the main queue before touching state.
Screen sharing
iOS only lets an app capture the whole screen, including after you leave it, through a ReplayKit broadcast upload extension. The extension runs in its own process and can't run WebRTC, so it sends the frames to the app over a Unix domain socket in the shared app group. FlutterBroadcastScreenCapturer then feeds them into the same RTCVideoSource the camera uses.
Each frame is an HTTP-style message: Content-Length, Buffer-Width, Buffer-Height and Buffer-Orientation headers, then a JPEG body. The app side of the socket is copied from the flutter-webrtc plugin. Keep its reading logic as it is: if the reader ever asks the stream for 0 bytes, iOS reports end of stream and the broadcast stops right after the first frame.
Sharing keeps going when you leave the app, which needs two things:
- The app stays alive.
UIBackgroundModeshasaudioandvoip, and the call keeps aplayAndRecordaudio session active, so iOS doesn't suspend the app and its socket. - The encoder keeps working. iOS invalidates the VideoToolbox hardware H264 encoder in the background, so every frame would fail and the other side would see a frozen picture. iOS therefore always puts VP8 (software) first, with or without E2EE. It costs more CPU and battery.
The app and the extension must share the same App Group (group.com.ducmai.webrtc.broadcast). If you change the team or bundle ID, update it in both .entitlements files, RTCAppGroupIdentifier in Info.plist, SampleHandler.swift, and the extension bundle ID in RTCScreenSharingExtension.
The audio session
The app configures the audio session itself: before every call, CallViewModel.configureCallAudio() sets WebRTC's configuration to playAndRecord + voiceChat. The webrtc-sdk fork otherwise copies the session's launch category (soloAmbient), which iOS rejects together with the Bluetooth HFP option, and calls end up with no microphone and no sound. See Troubleshooting.
How it does each part
- Switching sources. Camera, file and screen capturers all feed one
RTCVideoSource, so the track, the sender and its cryptor never change. Switching video sources - E2EE.
RTCFrameCryptorwith the shared options. - Effects. A proxy
RTCVideoCapturerDelegatebetween the camera and the source runs Vision and Core Image on Metal. Backgrounds and effects - Picture-in-picture. Video call PiP, 1:1 only. Picture-in-picture
- Group calls. Every subscribe offer is parsed for
a=midanda=msid, because libwebrtc doesn't fire a new receiver event when an m-line is reused for someone else. Group calls (SFU)
Notes
- WebRTC comes from the local Swift package
ios/Packages/WebRTC, because the Specs manifest for this release doesn't resolve. To upgrade, change the URL and checksum in itsPackage.swift. Info.plistallows plain HTTP (NSAllowsArbitraryLoads) so any LAN server works.
