跳到主要内容

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.xVP8/H.264,VP9 解码起步
16.xVP9 解码稳定,WebCodecs 部分能力
17.xAV1 解码(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 上 &lt;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 上更严):

场景是否能自动播
&lt;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音频 TrackPeerConnection
App 切后台(无 audio background mode)立即 mute~30s 后 muteTCP/UDP socket 不断,但 30s 后基本卡死
App 切后台(开了 audio background mode)mute保持保持
锁屏mute视配置同上
页面切 tab(多 tab WKWebView 极少见)限帧到 1fps保持节流
来电强抢音频通道mute通话结束需重启

唤醒后的"冻屏"问题:iOS 唤醒回前台后,本地视频元素可能停在最后一帧不动。这不是 WebRTC 本身问题,是 &lt;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 默认走系统路由,但有一个坑:远端音频经 &lt;audio> 播放,不走 voice chat 路由,可能默认从顶部听筒出而不是扬声器。

工程做法两种:

  1. 原生侧设置 audio session category:在加载 WKWebView 前 setCategory(.playAndRecord, mode: .voiceChat, options: .defaultToSpeaker),让整个 App 默认外放。
  2. 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 端 &lt;video playsinline muted autoplay>
  • 进房按钮内 await audio.play() 解锁
  • visibilitychange 事件刷本地预览 + 必要时 ICE Restart
  • 远端视频订阅按可见数限制
  • 原生侧统一 AVAudioSession 配置,避免与 WebRTC 抢通道
  • iframe 加 allow="camera; microphone"
  • 接到系统电话挂断后能自动恢复

12. 失败时的样子

现象大概原因处理
getUserMedia 直接 NotAllowedErrorInfo.plist 缺权限说明补 plist
进房后远端无声&lt;audio> 没有用户手势就 play把 play 放进 click handler
切后台 30s 后整个房间断没开 audio background modeUIBackgroundModes 加 audio
唤醒后本地预览静止&lt;video> 渲染节流visibilitychange 重设 srcObject
接听系统电话挂断后无声AVAudioSession 没恢复监听 AVAudioSession 中断通知,恢复
大房间观看 5 分钟后白屏WebContent 进程 OOM限制订阅路数 + simulcast 下发低层
iframe 内拿不到摄像头父页 iframe 没加 allowallow="camera; microphone"

13. 反模式

反模式后果
假设 WKWebView == Safari后台/权限/路由行为不同,到处踩坑
不写 playsinline远端视频拉起全屏播放器,业务 UI 失效
&lt;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. 权威资料