Master Picture in Picture on tvOS
Audio & Video 进阶 22m

掌握 tvOS 画中画

Master Picture in Picture on tvOS

2020年6月25日

在 Apple 官方观看视频

一句话判断

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 中声明 audio background 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 到其他设备
WWDC 2020