WWDC Quick Look 💓 By SwiftGGTeam
Coordinate media playback in Safari with Group Activities

Coordinate media playback in Safari with Group Activities

观看原视频

Highlight

Safari 15 通过 Media Session API 和 Coordinator 扩展,把 SharePlay 的同步播放能力带到网页,让 Mac 用户无需安装 App Store 应用,也能在 FaceTime 中加入同一段媒体播放。

核心内容

你已经有一个 iPhone 视频应用。用户可以在 FaceTime 里一起看电影预告片,因为应用已经接入 Group Activities。

问题出在 Mac 用户身上。

你的服务也有网站。Mac 用户打开 Safari 就能看同一段内容,但以前网页只负责本地播放。它不知道 FaceTime 里还有其他人,也不能把播放、暂停、拖动进度这些动作同步给其他设备。

这会让体验断开。iPhone 上的人发起 Watch Together,Mac 上的人还要去找网页、找片源、手动对齐播放时间。

Safari 15 给网页补上了这段链路。

第一步,原生 App 在 Group Activity 的 metadata 里提供 fallbackURL。这个 URL 指向网站上的具体内容。Mac 用户接受邀请后,Safari 会打开这个 URL。

第二步,网页接入 Media Session API(媒体会话 API)。它把当前媒体的标题、封面、播放状态、进度告诉 Safari,也把播放、暂停、跳转这些系统控制交回网页处理。

第三步,Safari 在 navigator.mediaSession 上增加 Coordinator(协调器)。网页通过它加入 SharePlay 会话,把本地用户的播放意图发给其他设备,并接收其他设备的播放意图。

这样,一个从 iPhone 发起的共享播放,可以落到 Safari 网页。网页仍然播放自己的 <video>,但播放命令由 Coordinator 协调。

详细内容

为 Safari 准备可打开的内容 URL

02:50

Safari 不能凭空加入一次 Group Activities 会话。原生 App 发起共享时,需要在 activity metadata 里提供 fallbackURL

这个 URL 不能只指向网站首页。它必须能定位到要播放的具体内容。演讲里的例子是电影预告片网站,URL 中带着 contentIdentifier

import Foundation
import GroupActivities

struct WatchTogether: GroupActivity {
    var contentIdentifier: String

    func metadata() async -> GroupActivityMetadata {
        var metadata = GroupActivityMetadata()
        metadata.fallbackURL = URL(string: "https://example.com/title/\(contentIdentifier)")
        return metadata
    }
}

关键点:

  • import GroupActivities 引入 SharePlay 背后的框架。
  • WatchTogether 遵守 GroupActivity,表示一次可共享的观看活动。
  • contentIdentifier 保存当前媒体内容的标识。
  • metadata() 返回这次共享活动的元数据。
  • fallbackURL 指向网站上的具体内容页。
  • Mac 用户接受邀请后,Safari 会加载这个 URL。

这一步解决的是入口问题。Safari 能打开正确网页后,网页还要让浏览器理解当前正在播放什么。

用 Media Session 暴露播放状态和系统控制

05:29

Media Session 是标准 Web API。网页用它告诉浏览器当前媒体的标题、封面、播放状态、时长和进度。

Safari 拿到这些信息后,可以把它们交给系统的 Now Playing。用户在 macOS 菜单栏里点播放或暂停时,Safari 会调用网页注册的 handler。

const video = document.querySelector('video');
const myPlayer = {
  titleString: 'The Mosquito Coast — Trailer',
  artworkURL: '/artwork/trailer.jpg'
};

if (navigator.mediaSession) {
  navigator.mediaSession.setActionHandler('play', () => video.play());
  navigator.mediaSession.setActionHandler('pause', () => video.pause());
  navigator.mediaSession.setActionHandler('seekto', details => {
    video.currentTime = details.seekTime;
  });
}

let updateMediaSessionState = function() {
  if (!navigator.mediaSession) {
    return;
  }

  let playbackState = video.paused ? 'paused' : 'playing';
  navigator.mediaSession.playbackState = playbackState;

  let positionState = {
    duration: video.duration,
    playbackRate: video.playbackRate,
    position: video.currentTime
  };
  navigator.mediaSession.setPositionState(positionState);
};

for (let event of ['playing', 'pause', 'durationchange', 'ratechange', 'timeupdate']) {
  video.addEventListener(event, updateMediaSessionState);
}

if (navigator.mediaSession) {
  navigator.mediaSession.metadata = new MediaMetadata({
    title: myPlayer.titleString,
    artwork: [{ src: myPlayer.artworkURL }]
  });
}

关键点:

  • document.querySelector('video') 找到页面里的视频元素。
  • navigator.mediaSession 用来判断浏览器是否支持 Media Session。
  • setActionHandler('play', ...) 把系统播放按钮连接到 video.play()
  • setActionHandler('pause', ...) 把系统暂停按钮连接到 video.pause()
  • setActionHandler('seekto', ...) 接收系统传入的跳转时间,并更新 video.currentTime
  • playbackState 把网页的播放或暂停状态同步给浏览器。
  • setPositionState() 把时长、播放速度、当前位置同步给浏览器。
  • MediaMetadata 提供标题和封面,供 Now Playing 展示。

这一步让网页能被系统控制。接下来还要把这些控制扩展到同一个 SharePlay 会话里的其他设备。

用 Coordinator 加入 SharePlay 同步播放

09:32

Safari 15 在 Media Session 上增加了 coordinator。网页通过它加入现有会话,发送播放意图,并接收其他设备的播放状态变化。

演讲强调了三个限制。

第一,Coordinator 还是 experimental web API,接口可能变化。第二,当时只有 Safari 实现。第三,网页可以加入现有会话,但 GroupSession 需要从 iPhone、iPad 或 macOS 应用发起。

const video = document.querySelector('video');
const controls = {
  inSessionIcon: document.querySelector('#in-session-icon'),
  playButton: document.querySelector('#play-button'),
  pauseButton: document.querySelector('#pause-button'),
  timeline: document.querySelector('#timeline')
};

navigator.mediaSession.addEventListener('coordinatorchange', () => {
  let coordinator = navigator.mediaSession.coordinator;

  if (coordinator) {
    coordinator.join();
  }

  controls.inSessionIcon.hidden = !coordinator;
});

controls.inSessionIcon.addEventListener('click', () => {
  let coordinator = navigator.mediaSession.coordinator;

  if (coordinator && coordinator.state === 'joined') {
    coordinator.leave();
  }
});

controls.playButton.addEventListener('click', () => {
  let coordinator = navigator.mediaSession.coordinator;

  if (coordinator) {
    coordinator.play();
  } else {
    video.play();
  }
});

controls.pauseButton.addEventListener('click', () => {
  let coordinator = navigator.mediaSession.coordinator;

  if (coordinator) {
    coordinator.pause();
  } else {
    video.pause();
  }
});

controls.timeline.addEventListener('change', event => {
  let coordinator = navigator.mediaSession.coordinator;

  if (coordinator) {
    coordinator.seekTo(event.target.value);
  } else {
    video.currentTime = event.target.value;
  }
});

关键点:

  • coordinatorchange 在 Coordinator 可用状态变化时触发。
  • navigator.mediaSession.coordinator 取到当前协调器。
  • coordinator.join() 让网页加入这次共享播放会话。
  • controls.inSessionIcon.hidden 根据是否处在会话中显示状态图标。
  • 点击会话图标时,如果状态是 joined,调用 leave() 退出。
  • 播放按钮优先调用 coordinator.play(),由 Coordinator 通知所有设备开始播放。
  • 暂停按钮优先调用 coordinator.pause(),由 Coordinator 通知所有设备暂停。
  • 进度条变化时优先调用 coordinator.seekTo(),把跳转意图同步给会话。
  • 没有 Coordinator 时,代码回退到本地 video 控制。

Coordinator 会和会话里的其他设备通信,处理播放状态变化,解决冲突,并等待所有人准备好。准备完成后,它会调用前面注册的 Media Session action handler 来启动或暂停本地播放。

最小接入顺序

07:32

这场 session 的接入路径很短,但顺序不能乱。

先让原生 App 发出带 fallbackURL 的共享活动。再让网页用 Media Session 向 Safari 暴露状态和控制。最后用 Coordinator 把网页按钮改造成同步播放按钮。

function playFromControl() {
  let coordinator = navigator.mediaSession && navigator.mediaSession.coordinator;

  if (coordinator) {
    coordinator.play();
    return;
  }

  video.play();
}

关键点:

  • navigator.mediaSession && navigator.mediaSession.coordinator 同时检查 Media Session 和 Coordinator。
  • 有 Coordinator 时,用户的播放动作进入共享会话。
  • 没有 Coordinator 时,播放动作只影响本地网页。
  • 这个分支让同一套网页控制同时服务普通播放和 SharePlay 播放。

核心启发

1. 给视频 App 增加网页端 SharePlay 入口

  • 做什么:用户在 iPhone App 里发起一起看,Mac 用户接受邀请后直接进入 Safari 播放同一集内容。
  • 为什么值得做:session 里的 fallbackURL 和 Coordinator 让网页可以加入原生 App 发起的会话。
  • 怎么开始:在 GroupActivityMetadata 设置指向内容详情页的 fallbackURL,网页端接入 navigator.mediaSessionnavigator.mediaSession.coordinator

2. 做一个预告片共看片单

  • 做什么:朋友在 FaceTime 中一起看电影预告片,一方拖动进度或暂停,其他人的画面同步变化。
  • 为什么值得做:Coordinator 支持 play()pause()seekTo() 这类播放意图同步。
  • 怎么开始:先把每个预告片做成稳定 URL,再在网页播放器的按钮事件里优先调用 coordinator 方法。

3. 给音频网站补上 Now Playing 控制

  • 做什么:播客或音乐网页在 macOS 菜单栏的 Now Playing 中显示标题、封面,并响应播放暂停。
  • 为什么值得做:Media Session 可以暴露 playback state、position state 和 metadata。
  • 怎么开始:为 playpauseseekto 注册 setActionHandler(),监听音频元素事件后调用 setPositionState()

4. 给网页播放器加 SharePlay 状态提示

  • 做什么:播放器进入共享会话后显示一个会话图标,用户点击后退出会话。
  • 为什么值得做:session 里的例子用 coordinatorchange 控制图标显示,并用 leave() 退出。
  • 怎么开始:监听 navigator.mediaSessioncoordinatorchange 事件,根据 coordinator 是否存在切换 UI。

5. 同一套播放器同时支持本地播放和共享播放

  • 做什么:普通用户点击播放时本地播放,SharePlay 用户点击播放时同步给所有人。
  • 为什么值得做:网页可以在事件 handler 里检查 navigator.mediaSession.coordinator,按会话状态选择控制路径。
  • 怎么开始:把播放、暂停、进度条事件统一封装,分支里优先调用 coordinator.play()coordinator.pause()coordinator.seekTo(),否则操作 <video>

关联 Session

评论

GitHub Issues · utterances