iOS-WKWebView差异
iOS WKWebView 差异
WKWebView 不是 Safari,但它跑的也是 WebKit。WebRTC 直到 iOS 14.3 才进入 WKWebView,落地之后又有一堆和原生 Safari 不一致的行为:自动播放策略更严、后台挂起更激进、内存上限更小、和原生侧的 AVAudioSession / RTCAudioSession 互相争抢通道。本篇讲这些差异和怎么写客户端 + Web 两端代码。
1. WKWebView 与 Safari 的关系
- 同一个 WebKit 内核,但运行在宿主 App 进程外的
com.apple.WebKit.WebContent进程里。 - WebKit 的版本随 iOS 系统升级,不能像桌面 Chrome 那样独立升级。
- 自动播放、网络权限、后台行为由系统策略 + 宿主 App 配置共同决定。
- iOS 14.3 之前 WKWebView 完全不支持 WebRTC,原生
WKWebView加载 H5 直接拿不到RTCPeerConnection。
什么时候适用 H5 嵌入方案:业务以 H5 为主、iOS 上只是壳套 WKWebView、对延迟没有极端要求(300ms 是底)。 什么时候不适用:低延迟(<150ms 端到端)、需要稳定后台保持、需要做强 ANC/3A 调优——这些场景必须用原生 libwebrtc 或 TRTC iOS SDK。
2. iOS 各版本的 WebRTC 能力
| iOS 版本 | WKWebView WebRTC | 备注 |
|---|---|---|
| < 14.3 | ❌ | 必须用原生 SDK 或 SFSafariViewController(受限) |
| 14.3 ~ 14.5 | ⚠️ 基本可用 | getDisplayMedia 不支持,部分 API bug |
| 15.x | ✅ | VP8/H.264,VP9 解码起步 |
| 16.x | ✅ | VP9 解码稳定,WebCodecs 部分能力 |
| 17.x | ✅ | AV1 解码(A17 Pro 起硬解) |
| 18.x | ✅ | 后台音频策略改善,AirPods 切换更稳 |
| 26.x | ✅ | 当前主流,AV1 编码进入实验 |
3. 必须做的客户端配置
3.1 Info.plist 权限
<key>NSCameraUsageDescription</key>
<string>用于视频通话</string>
<key>NSMicrophoneUsageDescription</key>
<string>用于语音通话</string>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>voip</string>
</array>
少任何一项,第一次 getUserMedia 就会被系统拒绝,且 Web 层只能拿到 NotAllowedError,看不出是 plist 缺失还是用户拒绝。
3.2 WKWebViewConfiguration
import WebKit
let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true // 关键:playsinline 必须
config.mediaTypesRequiringUserActionForPlayback = [] // 允许媒体自动播放
config.allowsAirPlayForMediaPlayback = true
if #available(iOS 15.0, *) {
// 允许 getUserMedia 弹权限
config.preferences.javaScriptCanOpenWindowsAutomatically = false
}
// iOS 15+ 这步能让 Web 端 navigator.mediaDevices 不为空
if #available(iOS 15.0, *) {
config.preferences.isElementFullscreenEnabled = true
}
let webView = WKWebView(frame: .zero, configuration: config)
iOS 15 之前 WKUIDelegate 还要显式实现权限询问代理,否则永远拿不到摄像头。
3.3 权限代理(iOS 15+)
extension MyController: WKUIDelegate {
func webView(_ webView: WKWebView,
requestMediaCapturePermissionFor origin: WKSecurityOrigin,
initiatedByFrame frame: WKFrameInfo,
type: WKMediaCaptureType,
decisionHandler: @escaping (WKPermissionDecision) -> Void) {
// 业务里一般要校验来源 origin.host 后再放行
decisionHandler(.grant)
}
}
4. playsinline 的强制要求
iOS 上 <video> 默认会拉起原生全屏播放器,PeerConnection 渲染必须保留在页面内,所以:
<video
id="remote"
autoplay
playsinline
webkit-playsinline
muted
></video>
playsinline:iOS 10+ 标准。webkit-playsinline:兼容更老的内核(虽然现在基本无关了,但写上无副作用)。muted:iOS 自动播放只允许静音。需要声音的话必须等用户手势之后再video.muted = false。
WKWebView 还要 allowsInlineMediaPlayback = true,三者缺一就会进入"点了播放才放,否则全屏跳出"的行为。
5. 自动播放策略
iOS 自动播放规则(WKWebView 上更严):
| 场景 | 是否能自动播 |
|---|---|
<video muted autoplay playsinline> | ✅ |
静音视频,无 playsinline | ❌ 全屏拦截 |
| 有声视频,未交互 | ❌ |
| 有声视频,用户点击/触摸过页面后 | ✅ |
| WebRTC 远端音频 track | ⚠️ 需要在用户手势栈里调 play() |
实践:进入房间前必须有一次用户手势,且 play() 在手势事件回调里调用。
async function unlockAudio(audioEl: HTMLAudioElement) {
// 必须在按钮 click handler 里立刻 await
audioEl.muted = false;
try {
await audioEl.play();
} catch (e) {
// 仍可能失败:要在 UI 上提示用户再点一次
console.warn('autoplay blocked', e);
}
}
button.addEventListener('click', async () => {
await unlockAudio(remoteAudio);
await rtcEngine.joinRoom();
});
6. 后台与挂起
iOS App 切到后台 / WKWebView 滚出可见区 / 锁屏,这几种状态对 WebRTC 影响不一致:
| 状态 | 视频 Track | 音频 Track | PeerConnection |
|---|---|---|---|
| App 切后台(无 audio background mode) | 立即 mute | ~30s 后 mute | TCP/UDP socket 不断,但 30s 后基本卡死 |
| App 切后台(开了 audio background mode) | mute | 保持 | 保持 |
| 锁屏 | mute | 视配置 | 同上 |
| 页面切 tab(多 tab WKWebView 极少见) | 限帧到 1fps | 保持 | 节流 |
| 来电 | 强抢音频通道 | mute | 通话结束需重启 |
唤醒后的"冻屏"问题:iOS 唤醒回前台后,本地视频元素可能停在最后一帧不动。这不是 WebRTC 本身问题,是 <video> 渲染节流。
修复模板:
document.addEventListener('visibilitychange', async () => {
if (document.visibilityState !== 'visible') return;
// 重新拉一次本地预览
const v = localVideoEl;
if (v.srcObject) {
const ms = v.srcObject as MediaStream;
v.srcObject = null;
await new Promise(r => requestAnimationFrame(r));
v.srcObject = ms;
v.play().catch(() => {});
}
// 检查远端 track,必要时 ICE Restart
if (pc.connectionState !== 'connected') {
await pc.restartIce();
}
});
7. 内存限制
WKWebView 进程是受限的:
- 单进程上限大约 ~1.5 GB(机型不同),超过会整个 WebContent 崩溃,页面变白。
- WebRTC 视频解码会显著占用 GPU 内存,一路 1080p H.264 大约 80~120 MB。
- 6 路同屏会议在低端机上极易触发 OOM。
应对:
// 远端视频路数控制
const MAX_VISIBLE_REMOTE = 4;
function adjustSubscription(users: RemoteUser[]) {
const visible = users.slice(0, MAX_VISIBLE_REMOTE);
visible.forEach(u => u.subscribe('video'));
users.slice(MAX_VISIBLE_REMOTE).forEach(u => u.unsubscribe('video'));
}
iOS 上做大房间观看必须配合 SFU 的 simulcast,下发低层。
8. AVAudioSession 与 RTCAudioSession 冲突
如果宿主 App 同时使用了原生 RTC(比如壳里有原生通话功能),WebRTC SDK 要在 AVAudioSession 上设置 playAndRecord + voiceChat mode,而 WKWebView 内的 WebRTC 会自己抢一次。最常见的症状:
- 进入 H5 通话后,原生侧再发起通话音频路由错乱(声音在听筒/扬声器之间乱跳)。
- 接听系统电话回到 H5,AGC/AEC 失效。
策略:
// 进入 H5 通话页前,主动让出
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
// 离开 H5 通话页,恢复
try? AVAudioSession.sharedInstance().setCategory(.playAndRecord,
mode: .voiceChat,
options: [.allowBluetooth, .defaultToSpeaker])
try? AVAudioSession.sharedInstance().setActive(true)
并在 H5 端通过 webkit.messageHandlers 通知原生切换。
9. AirPods / 蓝牙 / 听筒切换
WKWebView 默认走系统路由,但有一个坑:远端音频经 <audio> 播放,不走 voice chat 路由,可能默认从顶部听筒出而不是扬声器。
工程做法两种:
- 原生侧设置 audio session category:在加载 WKWebView 前
setCategory(.playAndRecord, mode: .voiceChat, options: .defaultToSpeaker),让整个 App 默认外放。 - Web 端用
Audio Output Devices API:iOS 不支持setSinkId,这条路在 iOS 上走不通,必须靠原生。
10. iframe 与第三方域
WKWebView 内嵌的 H5 如果再嵌 iframe(比如客服 SDK),iframe 里调 getUserMedia 必须设置:
<iframe
src="https://rtc.example.com/room"
allow="camera; microphone; autoplay; display-capture"
></iframe>
少了 allow 直接 NotAllowedError,且不会再次弹权限。
11. 完整的 iOS 集成 Checklist
- iOS 系统版本 ≥ 14.3
- Info.plist 配置 NSCameraUsageDescription / NSMicrophoneUsageDescription
- Background Modes 勾 audio + voip(如需后台保持音频)
- WKWebViewConfiguration 设置 allowsInlineMediaPlayback / mediaTypesRequiringUserActionForPlayback
- 实现 WKUIDelegate.requestMediaCapturePermissionFor
- H5 端
<video playsinline muted autoplay> - 进房按钮内 await
audio.play()解锁 - visibilitychange 事件刷本地预览 + 必要时 ICE Restart
- 远端视频订阅按可见数限制
- 原生侧统一 AVAudioSession 配置,避免与 WebRTC 抢通道
- iframe 加
allow="camera; microphone" - 接到系统电话挂断后能自动恢复
12. 失败时的样子
| 现象 | 大概原因 | 处理 |
|---|---|---|
getUserMedia 直接 NotAllowedError | Info.plist 缺权限说明 | 补 plist |
| 进房后远端无声 | <audio> 没有用户手势就 play | 把 play 放进 click handler |
| 切后台 30s 后整个房间断 | 没开 audio background mode | UIBackgroundModes 加 audio |
| 唤醒后本地预览静止 | <video> 渲染节流 | visibilitychange 重设 srcObject |
| 接听系统电话挂断后无声 | AVAudioSession 没恢复 | 监听 AVAudioSession 中断通知,恢复 |
| 大房间观看 5 分钟后白屏 | WebContent 进程 OOM | 限制订阅路数 + simulcast 下发低层 |
| iframe 内拿不到摄像头 | 父页 iframe 没加 allow | 加 allow="camera; microphone" |
13. 反模式
| 反模式 | 后果 |
|---|---|
| 假设 WKWebView == Safari | 后台/权限/路由行为不同,到处踩坑 |
不写 playsinline | 远端视频拉起全屏播放器,业务 UI 失效 |
<audio autoplay> 不解锁 | 进房静音,用户以为故障 |
| 用 H5 做 1v1 视频 + 长时间后台 | 30s 必断,必须用原生或 PiP |
| 同时跑原生 RTCAudioSession 与 WKWebView WebRTC | 音频路由互相抢,AEC 失效 |
| 大房间在 iOS 上拉全分辨率 | OOM 白屏 |
| 用 UA "Safari" 做能力探测 | WKWebView UA 不带 Safari/,全部判断失败 |
14. 何时该放弃 H5 改原生
如果业务对以下任何一项有要求,建议直接走原生 libwebrtc / TRTC iOS SDK:
- 端到端延迟 < 150ms 的语聊房 / 1v1 会议。
- 后台必须保持 5 分钟以上视频或语音。
- 需要做 AGC/AEC 调参或自定义 3A。
- 房间稳定性 SLA > 99.9%。
- 与原生通话能力(CallKit、PushKit)结合。
详见 移动原生libwebrtc.md。
15. 权威资料
- WKWebView 文档: https://developer.apple.com/documentation/webkit/wkwebview
- WKUIDelegate Media Capture: https://developer.apple.com/documentation/webkit/wkuidelegate/3601237-webview
- AVAudioSession Programming Guide: https://developer.apple.com/documentation/avfaudio/avaudiosession
- WebKit WebRTC 状态: https://webkit.org/status/#?search=webrtc
- Apple WWDC "Build advanced WebRTC experiences" (2024): https://developer.apple.com/videos/play/wwdc2024/
- 核对日期:2026-06-22