WKWebView 的视频画中画在 iOS 14 之前基本没有稳定公开方案,多数项目要么关闭这个能力,要么依赖容易在审核时被拒的私有接口。iOS 14 加入公开配置项后,大家以为只要打开开关就能用,但升级到 iOS 15 又发现回调、按钮状态和恢复流程出现了新的差异。这些差异并不是单纯的系统 Bug,而是苹果对 AVPictureInPictureController 与 WKWebView 内部媒体源绑定方式做了收紧。下面从两个系统版本的底层行为切入,给出可落地的兼容代码。

一、iOS 14 与 iOS 15 在 WKWebView 画中画上的行为差异
iOS 14 为 WKWebView 引入了 allowsPictureInPictureMediaPlayback 这个公开配置属性。只要在创建 WKWebView 时把它置为 YES,网页中的 <video> 元素在满足内联播放条件后,系统默认控制器上就有机会出现画中画按钮。这个阶段的问题是它高度依赖网页自身的视频控制条。如果网页使用原生 controls 属性,系统会接管大部分交互;如果网页完全自定义控制条,画中画入口就不一定能够稳定展示。
到了 iOS 15,系统对 WKWebView 内部媒体源的暴露策略发生了收紧。WKWebView 不再像 iOS 14 那样在视频加载后立即把 AVPlayerLayer 会话完整交出来,而是把 AVPictureInPictureController 的内容源绑定变成了延迟创建。也就是说,即使 allowsPictureInPictureMediaPlayback 已经是 YES,如果视频还没有进入真正可播放状态,或者网页没有正确声明内联属性,画中画按钮仍然不会出现。很多团队在这个阶段误以为是配置失效,其实核心原因是系统对媒体源生命周期的校验更严格了。
另一个差异表现在事件回调上。iOS 14 中,网页端的 enterpictureinpicture 和 leavepictureinpicture 可以比较稳定地触发;iOS 15 上则可能出现系统先触发 webkitendfullscreen 或 pause,随后才派发画中画事件。如果前端监听顺序写错,很容易在从画中画返回时出现画面停在最后一帧、播放进度丢失或者控制条状态错乱。
二、兼容方案:按系统版本分层配置与事件注入
处理第一层差异时,不能只依赖一个全局开关,而是要根据系统版本做分层配置。创建 WKWebView 时,iOS 14 及以上可以直接开启画中画能力,同时建议允许视频无需用户手势即可播放,这样自动进入画中画时不会被自动播放策略拦截。下面是 Objective-C 的配置示例。
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
if (@available(iOS 14.0, *)) {
config.allowsPictureInPictureMediaPlayback = YES;
}
if (@available(iOS 10.0, *)) {
config.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone;
}
WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:config];
在 iOS 15 上,单靠原生配置还不够。网页端的视频元素需要被主动标记为内联播放,并且最好通过标准画中画 API 再补一层触发逻辑。可以在 WKUserScript 中注入一段脚本,监听视频元素动态创建的过程,自动补上 playsinline 属性,并监听画中画事件。这样即使网页前端忘记处理,原生容器也能兜底。
function setupWKWebViewPiP() {
function enhanceVideo(video) {
if (!video.hasAttribute('playsinline')) {
video.setAttribute('playsinline', '');
}
video.setAttribute('controls', 'controls');
video.addEventListener('enterpictureinpicture', function() {
window.webkit.messageHandlers.pipHandler.postMessage({ type: 'enter' });
});
video.addEventListener('leavepictureinpicture', function() {
window.webkit.messageHandlers.pipHandler.postMessage({ type: 'leave' });
});
}
function scan() {
var videos = document.querySelectorAll('video');
for (var i = 0; i < videos.length; i++) {
enhanceVideo(videos[i]);
}
}
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.addedNodes) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeName === 'VIDEO') {
enhanceVideo(node);
}
});
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
document.addEventListener('DOMContentLoaded', scan);
scan();
}
setupWKWebViewPiP();
注入脚本之后,还需要在原生侧实现对应的消息处理器。WKScriptMessageHandler 接收到 enter 或 leave 后,可以更新导航栏按钮状态、调整音频会话,或者记录当前播放进度,方便从画中画返回时恢复。这样网页端和原生端就有了统一的状态同步入口,不再各自猜测系统行为。
三、使用 AVPictureInPictureController 接管原生媒体源
如果项目里的播放器并不是纯网页实现,而是原生 AVPlayer 和 WKWebView 混合,那么在 iOS 15 上更稳妥的做法是直接用 AVPictureInPictureController 接管原生媒体源。这样做的好处是避免系统在 WKWebView 内部做延迟绑定时出现不可控的时序问题。只要播放器已经持有 AVPlayer 和 AVPlayerLayer,画中画能力就可以由原生代码完全掌控。
import AVKit
class PlayerViewController: UIViewController {
private var pipController: AVPictureInPictureController?
func setupPiP(playerLayer: AVPlayerLayer) {
guard #available(iOS 15.0, *) else { return }
pipController = AVPictureInPictureController(playerLayer: playerLayer)
pipController?.delegate = self
pipController?.canStartPictureInPictureAutomaticallyFromInline = true
}
func startPiP() {
guard let controller = pipController else { return }
if controller.isPictureInPictureActive {
controller.stopPictureInPicture()
} else {
controller.startPictureInPicture()
}
}
}
extension PlayerViewController: AVPictureInPictureControllerDelegate {
func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
// 记录进入画中画,暂停隐藏不必要的UI
}
func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
// 返回内联播放,恢复播放器UI
}
}
这套原生接管方案与 WKWebView 配置并不冲突。iOS 14 上仍可保留网页端画中画入口,iOS 15 上如果检测到原生播放器已经具备画中画能力,就优先使用原生控制器。判断逻辑可以放在播放器初始化阶段,通过系统版本和当前媒体源类型决定走网页端还是原生端。这样既能兼容旧项目,又能避免新版系统上 WKWebView 的延迟绑定问题。
四、画中画窗口恢复、中断与音频会话处理
画中画窗口关闭后的恢复流程是兼容处理中最容易出问题的地方。如果只处理了 leavepictureinpicture 事件,iOS 15 上可能会因为系统先触发暂停事件,导致恢复逻辑被重复执行或根本不执行。更可靠的做法是在原生侧统一处理 AVPictureInPictureControllerDelegate 的恢复回调,并在网页端只负责基础状态同步。
func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
// 把播放器UI重新挂回原来的容器
view.addSubview(playerView)
playerView.frame = view.bounds
completionHandler(true)
}
音频会话是另一个必须提前处理的点。画中画进入后台后,系统要求当前 AVAudioSession 的 category 必须是 playback 或 playAndRecord,否则画面虽然切到悬浮窗,但声音会被系统切断。配置音频会话时建议在应用启动或播放器初始化阶段完成,而不是等到用户点击画中画按钮时才设置,否则在 iOS 15 上可能因为设置过晚导致后台无声音。
NSError *error = nil; AVAudioSession *session = [AVAudioSession sharedInstance]; [session setCategory:AVAudioSessionCategoryPlayback mode:AVAudioSessionModeMoviePlayback options:AVAudioSessionCategoryOptionAllowAirPlay error:&error]; [session setActive:YES error:&error];
中断场景同样需要覆盖。来电、闹钟、AirPods 断开等系统中断会导致画中画停止,这时 AVPictureInPictureControllerDelegate 的中断代理会被触发。实现中断回调后,要检查当前播放状态是否需要恢复,但不要立即强制播放,否则用户可能在通话状态下突然听到视频声音。比较合理的做法是记录中断前的播放位置,等系统恢复到前台后由用户决定是否继续播放。
五、完整兼容流程梳理
把前面的步骤串起来,一个稳定的 WKWebView 画中画兼容流程可以这样设计:创建 WKWebView 时按系统版本开启配置;注入脚本自动补全网页视频的内联属性和事件监听;原生侧写好消息处理器和 AVPictureInPictureControllerDelegate;音频会话提前配置为 playback;中断恢复时保存状态而不是盲目续播。
在实际测试中,iOS 14 设备上应当重点检查系统默认控制条上的画中画按钮是否出现,以及网页自定义控制条是否会遮挡系统入口。iOS 15 设备上则要重点检查视频首次加载后是否需要用户点击一次播放才能建立画中画内容源。如果项目对首次自动进入画中画有要求,可以在视频真实开始播放后再调用一次原生控制器的状态检查,避免延迟绑定导致启动失败。
最终目标是让用户在两个系统版本上都能从同一个入口进入画中画,并且在关闭、中断、切后台再返回三个路径上保持一致的播放体验。做到这一点并不需要为每个版本写一套完全独立的代码,只要把配置、事件、恢复三个边界理清楚,版本差异就能收敛在很小的分支逻辑里。

