掌握 tvOS 画中画
Master Picture in Picture on tvOS
2020年6月25日
一句话判断
tvOS 14 的画中画不只是把视频缩小——它涉及多窗口管理、音频焦点策略和用户交互设计的完整体系,如果你做 tvOS 视频应用,这堂课必修。
这场 Session 讲了什么
tvOS 14 引入了系统的画中画(Picture in Picture,PiP)支持。Session 详细介绍了如何在 tvOS 应用中实现 PiP 功能,包括 AVPlayer 的自动 PiP 行为、自定义 PiP 交互,以及 tvOS 特有的多窗口管理。
在 tvOS 上,PiP 的工作方式与 iOS 有显著不同。由于 TV 屏幕更大,PiP 窗口可以同时显示主内容和画中画内容,两者都清晰可辨。当用户按 Home 键时,正在播放的视频会自动缩小为 PiP 窗口悬浮在主界面或其他应用上方。开发者可以控制 PiP 窗口的位置(四个角落)、大小,以及是否允许用户在 PiP 模式下进行播放控制。
Session 还讨论了音频焦点(audio focus)策略。当主应用和 PiP 窗口同时有音频输出时,系统需要决定哪个音频流优先。对于视频应用来说,通常 PiP 窗口的音频应该保持为主音频,而新打开的应用的音频降低为次要。这个策略可以通过 AVAudioSession 的 category 和 mode 配置来控制。
值得深挖的点
AVPlayerViewController 的自动 PiP 行为
使用 AVPlayerViewController 播放视频时,tvOS 会自动处理 PiP 切换——当用户按 Home 键或切换应用时,播放器自动缩小为 PiP 窗口。你只需要在 Info.plist 中设置 UIBackgroundModes 包含 audio,并在 AVPlayerViewController 上设置 allowsPictureInPicturePlayback = true。
自定义 PiP 窗口的交互
如果你使用的是自定义播放器而非 AVPlayerViewController,需要通过 AVPictureInPictureController 手动管理 PiP 状态。这包括创建 PiP controller、响应 PiP 生命周期回调(start/stop/restore),以及处理 PiP 窗口中的用户交互。Session 演示了如何在 PiP 窗口中添加自定义的播放控制按钮。
代码片段
import AVKit
import AVFoundation
// 使用 AVPlayerViewController 实现自动 PiP
class VideoPlayerViewController: AVPlayerViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 启用 PiP 支持
allowsPictureInPicturePlayback = true
// 配置播放器
let url = URL(string: "https://example.com/movie.m3u8")!
let player = AVPlayer(url: url)
self.player = player
// 配置音频会话
setupAudioSession()
player.play()
}
private func setupAudioSession() {
do {
let audioSession = AVAudioSession.sharedInstance()
// 设置为播放模式,支持 PiP 后台播放
try audioSession.setCategory(.playback, mode: .default)
try audioSession.setActive(true)
} catch {
print("音频会话配置失败: \(error)")
}
}
}
// 使用 AVPictureInPictureController 自定义 PiP
import AVKit
class CustomPlayerViewController: UIViewController {
var player: AVPlayer!
var playerLayer: AVPlayerLayer!
var pipController: AVPictureInPictureController?
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "https://example.com/stream.m3u8")!
player = AVPlayer(url: url)
playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = view.bounds
view.layer.addSublayer(playerLayer)
// 创建 PiP 控制器
if AVPictureInPictureController.isPictureInPictureSupported() {
pipController = AVPictureInPictureController(
playerLayer: playerLayer
)
pipController?.delegate = self
}
}
// 用户触发的 PiP 切换
@objc func togglePiP() {
guard let pipController = pipController else { return }
if pipController.isPictureInPictureActive {
pipController.stopPictureInPicture()
} else {
pipController.startPictureInPicture()
}
}
}
extension CustomPlayerViewController: AVPictureInPictureControllerDelegate {
func pictureInPictureControllerDidStartPictureInPicture(
_ pictureInPictureController: AVPictureInPictureController) {
print("PiP 已启动")
// 更新 UI 状态,隐藏主播放器控制
}
func pictureInPictureControllerDidStopPictureInPicture(
_ pictureInPictureController: AVPictureInPictureController) {
print("PiP 已停止")
// 恢复全屏播放器 UI
}
func pictureInPictureController(
_ pictureInPictureController: AVPictureInPictureController,
restoreUserInterfaceForPictureInPictureWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
// 用户点击 PiP 窗口恢复全屏
// 在这里恢复完整的播放器界面
completionHandler(true)
}
}
// Info.plist 配置
/*
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
确保在 Capabilities 中开启:
- Audio, AirPlay, and Picture in Picture
*/
最佳实践
- 始终在
Info.plist中声明audiobackground mode,否则 PiP 无法在后台继续播放 - 使用
AVPlayerViewController获得免费的 PiP 支持,只有在需要自定义行为时才使用AVPictureInPictureController - PiP 启动时释放不必要的资源(如 UI 动画、大的图片缓存),减少内存占用
- 处理好 PiP 恢复全屏的回调,确保播放进度和 UI 状态无缝衔接
- 在 tvOS 上测试 PiP 时使用真实的 Apple TV 设备,Simulator 的 PiP 行为与真机有差异
还有什么值得关注
- tvOS 的 PiP 窗口支持用户通过 Siri Remote 拖动位置,应用不需要处理位置逻辑
- 多个应用同时请求 PiP 时,系统会自动管理音频焦点,最后活跃的应用获得焦点
- Session 提到 AirPlay 和 PiP 可以同时工作——用户可以将 PiP 内容 AirPlay 到其他设备