Highlight
Apple 在 Group Activities 框架中加入 SharePlay 媒体同步能力,并通过 AVPlaybackCoordinator 让开发者把 AVPlayer 播放、暂停和跳转状态同步到 FaceTime 通话中的所有设备。
核心内容
远程一起看视频,最难的部分从来不是播放视频本身。
麻烦来自同步。一个人按下播放,另一个人还在缓冲。一个人暂停说话,另一个人的进度继续往前走。有人拖动进度条,群组里每台设备都要追上同一个位置。
过去,开发者需要自己处理这些状态。App 要知道用户是否在通话中,要把播放命令发给远端设备,还要处理网络延迟、缓冲、加入中途会话、离开会话这些细节。媒体 App 最终会写出一套小型实时协作系统。
Apple 在 WWDC21 给出的方案是 Group Activities(群组活动)和 SharePlay。App 只需要定义“大家正在一起做什么”,系统负责把活动发送给 FaceTime 通话中的其他参与者,并把 GroupSession(群组会话)交给每台设备。
媒体播放还有一层特殊处理。AVFoundation 新增 AVPlaybackCoordinator(播放协调器)。开发者把 AVPlayer 的 playbackCoordinator 连接到 GroupSession,播放、暂停、seek 这类传输控制命令就会被协调到同一个群组状态。
从本地播放切换到 SharePlay
用户点击播放按钮时,App 不应该直接假设用户想分享。
prepareForActivation() 会让系统判断当前环境和用户意图。如果用户没有在 FaceTime 通话中,App 继续本地播放。如果用户正在通话并选择共享,App 调用 activate(),系统创建会话并把它送到本机和远端设备。
这一步解决的是入口问题。播放按钮仍然是播放按钮,用户无需学习新的操作路径。
从 GroupSession 接管播放状态
会话创建后,App 通过 MovieWatchingActivity.sessions() 监听系统送来的 GroupSession。
本机发起的活动会收到会话。远端用户接受活动后也会收到会话。App 拿到会话后,加载对应内容,把 AVPlayer 连接到这个会话,最后调用 join() 加入实时连接。
这一步解决的是分发问题。App 不直接创建 GroupSession,也不自己管理远端设备列表。
让 AVPlayer 负责同步细节
AVPlaybackCoordinator 会拦截改变速率或当前时间的 AVPlayer API。
当一个参与者按下播放,协调器先让本机等待,再把命令发送给其他设备。所有设备准备好后一起开始播放。seek 也是同样流程:各设备完成跳转后,再一起恢复。
这一步解决的是同步问题。开发者仍然调用熟悉的 AVPlayer API,协调器负责把这些命令变成群组行为。
处理每个人的临时中断
群组播放不代表每次暂停都要影响所有人。
一个人的闹钟响了,系统音频会中断本机播放。网络卡顿也可能只发生在一台设备上。AVPlayerPlaybackCoordinator 会为这类情况创建 suspension(暂停协调),把这台设备临时从群组时序里分离出来。
中断结束后,本机会跳到群组当前时间并重新同步。其他人不用等待这次本地中断。
详细内容
定义 GroupActivity
(03:06)GroupActivity 是用户一起体验的内容。视频 App 可以把它理解成“一部正在一起看的电影”。框架要把活动数据通过网络发送给其他设备,所以协议继承自 Codable。
protocol GroupActivity: Codable {
/// An identifier so the system knows how to reference this activity
static var activityIdentifier: String { get }
/// Information that the system uses to show this activity, such as title and a preview image
var metadata: GroupActivityMetadata { get async }
}
关键点:
GroupActivity表示共享体验中的单个内容,例如一部电影或一首歌。Codable让框架可以把活动里的属性发到网络另一端。activityIdentifier是系统引用这个活动类型的唯一标识。metadata提供标题、预览图等系统 UI 需要展示的信息。get async使用 Swift Concurrency(Swift 并发)的异步只读属性形式。
在播放按钮里判断是否启动共享
(04:42)用户选择内容后,App 先构造活动,再调用 prepareForActivation()。系统会根据 FaceTime 通话状态和用户选择返回结果。
func playButtonTapped() {
let activity = MovieWatchingActivity(movie: movie)
Task {
switch await activity.prepareForActivation() {
case .activationDisabled:
// Playback coordination isn't active. Queue movie
// for local playback.
self.enqueuedMovie = movie
case .activationPreferred:
// Activate the activity. The system enqueues the movie
// when the activity starts.
activity.activate()
case .cancelled:
// The user cancelled the operation. Nothing to perform.
break
default:
break
}
}
}
关键点:
MovieWatchingActivity(movie: movie)把当前电影包装成群组活动。Task用来执行异步调用。await activity.prepareForActivation()让系统判断是否应该共享到 FaceTime 通话。.activationDisabled表示播放协调未启用,或者用户选择本地观看;代码直接设置enqueuedMovie。.activationPreferred表示系统建议启动共享;代码调用activity.activate()。.cancelled表示用户取消分享流程,代码不做额外操作。
接收 GroupSession
(08:31)App 不直接实例化 GroupSession。系统通过 GroupActivity.sessions() 返回的 AsyncSequence(异步序列)交付会话。
// Receiving a GroupSession from the GroupSession AsyncSequence
func listenForGroupSession() {
Task {
for await session in MovieWatchingActivity.sessions() {
...
}
}
}
关键点:
listenForGroupSession()应该在 App 启动后尽早开始监听。Task启动异步循环。for await持续等待系统交付新的会话。MovieWatchingActivity.sessions()是接收GroupSession的入口。session代表当前活动在多台设备之间的实时会话。
把 AVPlayer 连接到 GroupSession
(09:03)拿到 GroupSession 后,媒体 App 的关键步骤是把 AVPlayer 的 playbackCoordinator 连接到这个会话。
let player = AVPlayer()
...
func listenForGroupSession() {
Task {
for await groupSession in MovieWatchingActivity.sessions() {
// Verify content is available, prepare for playback to begin
player.playbackCoordinator.coordinateWithSession(groupSession)
...
}
}
}
关键点:
AVPlayer()是实际播放媒体内容的播放器。for await groupSession接收系统送来的群组会话。- 注释提醒开发者先确认内容可用,并完成播放前准备。
player.playbackCoordinator访问 AVPlayer 绑定的播放协调器。coordinateWithSession(groupSession)把播放器接入群组会话。- 连接完成后,播放、暂停、seek 这类改变速率或时间的命令会进入协调流程。
(09:24)会话最初处于 waiting 状态。App 还需要调用 join(),让实时连接开始工作。演讲说明,连接成功后,播放同步才会开始。
groupSession.join()
关键点:
join()让当前设备加入群组实时连接。- 加入后,设备之间可以发送和接收会话消息。
- 对媒体播放来说,加入会话后播放协调才开始生效。
理解 AVPlaybackCoordinator 如何拦截播放命令
(16:17)AVPlaybackCoordinator 会拦截 transport control API(传输控制 API),也就是改变 rate 或 currentTime 的调用。
player.play()
player.pause()
player.seek(to: time)
关键点:
player.play()会改变播放器速率,协调器会把这个意图发送给其他参与者。player.pause()同样属于速率变化,会影响群组中播放同一内容的设备。player.seek(to: time)改变当前时间,协调器会让其他设备跳到一致的位置。- 演讲强调,协调器只会在参与者播放相同内容时应用这些状态。
(18:03)默认情况下,协调器用 AVPlayerItem 的 URL 判断两台设备是否在播放同一内容。URL 相同,状态才会互相影响。URL 不同,命令不会套用到远端播放器。
let item = AVPlayerItem(url: movieURL)
player.replaceCurrentItem(with: item)
关键点:
AVPlayerItem(url:)使用 URL 创建播放项目。- 协调器默认把相同 URL 视为相同内容。
replaceCurrentItem(with:)让项目成为当前播放内容。- 项目成为 current item 后,协调器才会开始应用群组状态。
为本地缓存和合成内容提供自定义标识
(23:20)如果一个用户播放本地缓存文件,另一个用户播放云端 URL,默认 URL 判断会失效。AVFoundation 提供 delegate 入口,让 App 为 AVPlayerItem 返回自定义字符串标识。
func playbackCoordinator(_ playbackCoordinator: AVPlayerPlaybackCoordinator,
identifierFor playerItem: AVPlayerItem) -> String? {
return movieIdentifier
}
关键点:
playbackCoordinator(_:identifierFor:)来自 AVPlayerPlaybackCoordinator delegate。playerItem是协调器正在询问身份的播放项目。- 返回的字符串会取代 URL,用来判断两台设备是否播放同一内容。
movieIdentifier应该表示内容身份,不能表示本地文件路径。- 演讲提醒,两个项目被视为相同时,同一时间点必须对应同一段内容。
(24:40)个性化广告和插播内容不要混进主内容时间轴。演讲建议把广告和其他 interstitials(插播内容)放到单独播放器中,让主资产的时间保持一致。HLS 内容可以结合 AVPlayerInterstitialEvent API。
let event: AVPlayerInterstitialEvent
关键点:
AVPlayerInterstitialEvent是演讲提到的 HLS 插播相关 API。- 插播内容有自己的播放时段,不应该改变主内容的时间对应关系。
- 主内容时间保持一致后,协调器才能把不同设备重新拉回同一进度。
使用 suspension 做个人临时脱离
(31:26)有些操作只应该影响本机。演讲里的例子是“我错过了刚才几秒”:本机后退几秒,二倍速追上群组,其他设备继续播放。
class AVPlaybackCoordinator {
func beginSuspension(for reason: AVCoordinatedPlaybackSuspension.Reason) -> AVCoordinatedPlaybackSuspension
}
class AVCoordinatedPlaybackSuspension {
func end()
func end(proposingNewTime newTime: CMTime)
}
关键点:
beginSuspension(for:)让当前参与者的协调器临时脱离群组。reason记录这次脱离的原因,App 可以用自定义字符串常量扩展。- 返回的
AVCoordinatedPlaybackSuspension代表这段脱离状态。 - suspension 存在期间,本机 seek 和 rate 改变不会影响其他人。
end()结束脱离,本机会重新加入群组当前时间和速率。end(proposingNewTime:)结束脱离时可以向群组提议新的时间,适合拖动进度条这类交互。
(32:40)AVPlaybackCoordinator 还有几个辅助入口。它可以让某些 suspension 原因触发其他人等待,也可以暴露其他参与者状态,并通过 AVPlayer 通知标记远端参与者造成的速率变化或时间跳转。
coordinator.suspensionReasonsThatTriggerWaiting
coordinator.otherParticipants
player.reasonForWaitingToPlay
player.playImmediately(atRate: rate)
关键点:
suspensionReasonsThatTriggerWaiting用来配置哪些暂停原因会让其他参与者等待。otherParticipants可以查看其他参与者状态,包括 suspension reason。reasonForWaitingToPlay会出现新的 coordinated playback 等待原因。playImmediately(atRate:)可以绕过等待立即播放,演讲提醒这可能让其他参与者错过内容。
自定义播放器使用 AVDelegatingPlaybackCoordinator
(34:46)如果 App 没有使用 AVPlayer,可以使用 AVDelegatingPlaybackCoordinator。它不会替你观察播放器状态,开发者要自己把播放、暂停、seek、缓冲命令接到 delegate。
let coordinator = AVDelegatingPlaybackCoordinator()
关键点:
AVDelegatingPlaybackCoordinator面向非 AVPlayer 的播放实现。- UI 不应该直接命令自定义播放器,应该先把意图交给协调器。
- 协调器决定是否需要等待其他参与者后,再让播放对象改变时间或速率。
- 当前播放项目需要由开发者用字符串标识。
- 自动 suspension 不会由 delegating coordinator 添加,系统事件需要开发者自己开始和结束 suspension。
核心启发
-
做什么:给视频 App 增加“一起看”按钮。
- 为什么值得做:
prepareForActivation()能保留原有播放入口,同时在 FaceTime 通话中把播放升级为 SharePlay。 - 怎么开始:定义
MovieWatchingActivity,在播放按钮中调用prepareForActivation(),根据结果选择本地 enqueue 或activate()。
- 为什么值得做:
-
做什么:做一个在线课程的同步观看功能。
- 为什么值得做:
AVPlaybackCoordinator会同步播放、暂停和 seek,适合讲师和学员一起看同一段课程视频。 - 怎么开始:让课程视频 URL 成为
GroupActivity的内容数据,监听CourseWatchingActivity.sessions(),再调用player.playbackCoordinator.coordinateWithSession(groupSession)。
- 为什么值得做:
-
做什么:给体育回放加入“我错过了”个人追赶按钮。
- 为什么值得做:
AVCoordinatedPlaybackSuspension允许一名参与者短暂后退和加速,不打断其他人观看。 - 怎么开始:点击按钮时调用
beginSuspension(for:),本机 seek 到几秒前并设置更高速率,追上后调用suspension.end()。
- 为什么值得做:
-
做什么:支持本地缓存视频的 SharePlay 播放。
- 为什么值得做:缓存文件和云端文件 URL 不同,默认 URL 身份无法同步;自定义 identifier 可以把它们映射到同一内容。
- 怎么开始:实现 AVPlayerPlaybackCoordinator delegate 的
identifierFor playerItem,返回服务端内容 ID。
-
做什么:为带广告的视频做稳定的群组同步。
- 为什么值得做:个性化插播会破坏主内容时间轴,导致设备在不同片段上同步失败。
- 怎么开始:把广告放进单独播放器,主内容保持一致;HLS 场景继续研究
AVPlayerInterstitialEvent。
关联 Session
- Meet Group Activities — 介绍 SharePlay 背后的 Group Activities 框架、架构和基本概念。
- Build custom experiences with Group Activities — 讲解更新 GroupSession activity、观察会话状态和使用 GroupSessionMessenger。
- Design for Group Activities — 说明 SharePlay 体验的设计原则和以群组为中心的交互方式。
- Coordinate media playback in Safari with Group Activities — 展示 Web 和配套 App 如何接入 Group Activities 做媒体同步。
- Explore dynamic pre-rolls and mid-rolls in HLS — 讲解 HLS 动态插播和 AVPlayerInterstitialEvent,适合处理 SharePlay 中的广告时间轴。
评论
GitHub Issues · utterances