大多数浏览器和
Developer App 均支持流媒体播放。
-
构建空间同播共享体验
了解如何使用 GroupActivities 框架为 visionOS 构建独特的共享与协作体验。向你介绍适用于此平台的同播共享 App,了解如何创建让人们感觉身处同一空间的体验,并探索沉浸式 App 如何协调参与者之间的环境。
资源
相关视频
WWDC23
WWDC21
WWDC19
-
下载
♪ 悦耳的器乐嘻哈 ♪ ♪ Willem Mattelaer: 大家好 我是 Willem Group Activities 团队的一名工程师 应 Mia 的邀请 我来和大家分享 这款优秀的 空间同播共享是如何构建的 同播共享一直 致力于打破空间束缚 为人们创造内容共享同步体验 在同播共享平台上 通过 FaceTime 通话 体验身临其境的感觉 同播共享上的 FaceTime 通话中 所有空间角色 都将摆脱窗口的束缚 在房间中占据一部分物理空间 这样使人们获得 一种聚在一起的感觉 在此之上 我们引入了共享环境 共享环境可以让每个人都 感觉其他人处于同样的相对位置 为了说明这一点 请大家从两个不同的视角进行理解 左边是 Connor 的视角 右边是 Mia 的视角 如果 Connor 朝向 Mia 的角色 Mia 会看到 Connor 的角色正面向她 我们把这一逻辑 扩展到同播共享的共享体验 系统将窗口放置在 跟每个人相同的相对位置 系统为此建立了一个模板 模板用于确定参与者和 共享 App 之间的相对排列方式 以此来实现空间一致性 App 无需处理位移 系统就可以完成 App 只需考虑 视觉一致性 视觉一致性意味着每个人 都在 App 内看到相同的内容 想象一下 人们一起站在白板前 每个人的白板视图视角都相同 如果有人指着一个东西 其他人可以看到他指的是什么 我们希望通过同播共享 在空间体验中复刻这一点 回到我们前面的例子 这里的 App 不能做到视觉一致 如果 Connor 指向 App 中的方块 Mia 就会看到 Connor 的角色指向圆圈 这破坏了人们在同一空间的感觉 但是 如果 App 能做到视觉一致 Mia 会清楚地看到 Connor 指向正方形 App 必须使所有 参与者看到的内容及其位置 保持同步 这样 所有人都看向同一个 App 就好像他们真的在一起一样 我们将在设计空间同播共享 体验部分详细介绍这些概念 我强烈建议大家观看这段视频 进一步了解更多内容 刚才我们介绍了 FaceTime 通话 在这个平台上引入的一些新概念 接下来让我们深入了解窗口 App 使用同播共享时的注意事项 以及现有的 iOS 同播共享 App 如何提升在空间中的用户使用体验 之后 Mia 将向你展示新的 API 打造令人惊叹的 沉浸式同播共享体验 为了在窗口 App 中 支持同播共享 我们为群组会话 添加了新的系统协调器 系统协调器负责两件事: 接收活动同播共享会话的系统状态 并允许你在使用同播共享期间 增添额外配置 我要介绍其中两个 与窗口 App 和 沉浸式 App 相关的应用: isSpatial 标志和模板首选项 确定要将一致性实现到何种程度前 一定要先了解参与者是否有空间 例如 这是 Connor 的第一人称视角 他正在一边和 穿红色衣服的 Mia 通话 一边看着我 在同播共享上分享的一份文件 Mia 对这个场景的视角在右侧 她看到穿蓝色衣服的 Connor 当 Connor 滚动文件页面时 我们希望滚动位置同步 这样 Mia 也能看到 Connor 滚动文件页面 我们在视觉上仍然保持一致 我们在同播共享中 平移 Freeform 文档时 也是如此 但如果你的 App 没有空间 就看不到另一个人 与文件交互的场景 所以如果 Connor 滚动文件页面 Mia 与我们就无法同步 只能看到始终没有变化的文件页面 要确认本地参与者是否有空间 并调整在参与者之间同步的内容 首先需要获得群组会话的 系统协调器 你可以通过检查本地参与者状态 上的 isSpatial 是否为 true 从而确定本地参与者 是否为空间参与者 若要观察状态 可以使用 localParticipantState 属性 该属性会返回一个异步序列 可以遍历本地参与者状态的更新 系统协调器还为 App 提供了 一种选取模板首选项的方法 模板用于确定为保证空间一致 系统如何相对于 App 位置 放置所有参与者 这里支持三种不同的模板 我们把所有参与者并排放置在 面向共享 App 的弧形中 这是 App 使用常规窗口场景时 要应用的默认模板 在会话模板中 所有参与者都放在一个半圆中 App 放在这个半圆的前方 这对于那些不重视内容体验的 App 来说是非常有帮助的 例如 在同播共享中播放音乐 我们使用环绕模板 参与者围城一个圆 App 放在圆的中心 此模板只能 在 App 使用体积场景时使用 参与者与 App 之间的距离 取决于 App 的大小 如果 App 很大 就可以放置在离人们更远的地方 如果 App 很小 就会放得更近 同播共享 App 可以 通过系统协调器 提供模板首选项 FaceTime 通话在群组会话 处于活动状态时 可以使用此模板 同播共享 App 有三个可用的首选项 .none 它遵循系统默认行为 也即 垂直 App 是并排的 体积 App 是环绕的 这是要使用的默认首选项 第二个首选项是 .sideBySide 无论是哪种 App 都用并排模板 最后一个首选项是 .conversation 它会尝试应用相应的模板 配置模板首选项 只需在系统协调器的配置中设置 然后在群组会话上 调用 join 即可 模板可以将 App 和参与者 置于最佳体验位置 但是 如果你的 App 有多个窗口场景 并且有多个前景 该怎么办? 这是一个包含三个场景的 App 左侧是一个浏览视图 中间的小场景 可以轻松浏览内容 最后一个场景 是内容的详细信息 所有这些场景都可以同时打开 因此你既可以浏览内容 又可以详细阅读 在同播共享中 我们希望共享细节场景 但是 如果所有这些场景都是打开的 模板中可能会出现混乱 为了解决这个问题 我们添加了场景关联 系统可以由此 了解 App 的哪个场景 正在执行同播共享活动 我们需要掌握这一点有两个原因 首先 窗口上方的 Share 菜单 会向用户显示共享场景 当有多个窗口打开时 它的指示就非常重要 可以预防尴尬情况 比如有人在不经意间 意外与共享窗口交互 当然 它更重要的作用在于 可以决定窗口场景 与模板一起使用 如果你使用的是单一场景的 App 则无需担心这一点 因为只有一个场景 我们会自动将该场景 与群组会话相关联 然而 多场景 App 在使用同播共享时 需要考虑这一点 如果不掌握场景关联信息 就会随机选择一个 打开的场景 从而出现混乱 让我们来看看如何使用场景关联 我们首先看一下 发起同播共享活动的人 当激活 Group Activity 时 它会遍历所有场景 并检查哪个场景合适 我们通过对照 Group Activity 的活动标识符 评估场景激活条件 来实现这一点 场景激活条件包括两项检查 Can 和 Prefer Can 表示场景可用 但 Prefer 将帮助其 定向到更适合的场景 因此 我们将首先检查左侧的场景 看看它是 Can 还是 Prefers 满足活动标识符 它是 Can 但不是 Prefers 接下来 检查 中间的场景 但是 Can't 然后检查最后一个场景 是 Can 和 Prefers 由于最后一个场景 即 Can 也 Prefers 因此我们将其与群组会话相关联 如果没有场景具备 满足条件的标识符 我们将启动一个新的窗口场景 如果你需要一个自己的场景 进行同播共享体验 也很不错 如果有多个 Can 场景 但没有一个场景是 Prefer 则随机拾取其中一个场景 对于未激活并正在接收 Group Activity 的参与者 如果 App 已经开始运行 则会执行相同的逻辑 如果你的 App 尚未运行 则启动并关联第一个场景 为使用 SwiftUI App 生命周期的 App 指定场景激活条件 可使用 handlesExternalEvents 只需将 Group Activity 的 activity 标识符 包含在首选字符串集 或允许字符串集内即可 如果你的 App 使用 UIKit App 生命周期 则可以通过指定谓词来 设置 UI 场景的激活条件 要进一步了解更多关于 场景激活条件的信息 请查看 2019 年的这一期讲座 让我们再看一个例子 本例中 这是一个基于文档的 App 每个场景代表一个不同的文档 现在 我们想使用同播共享 在第一个文档上进行协作 我们要再次使用 Group Activity 标识符 来评估每个场景的激活条件 可惜 这些场景 并没有本质上的不同 所以它们最终都匹配上了 也一起将错误的场景 关联了过来 在这种情况下 我们希望 更改用于匹配场景的标识符 为了实现这一点 我们添加了场景关联行为 App 可以提供一个 用于找到正确的场景的标识符 回到这个例子 我们可以使用所有参与者 都同意的文档唯一标识符 然后使用该标识符找到正确的场景 如果我们现在评估场景 可以很容易地配置场景激活条件 如此一来只有第一个场景是匹配的 就得到了正确的场景关联 你可以通过 在 Group Activity 的 元数据上设置场景关联行为 来指定所要使用的标识符 请记住 加入同播共享会话的 所有其他参与者 都将使用相同的标识符 接下来我们一起了解 如何使用大家都同意的标识符 我们支持三种场景关联行为 首先会查看 default 行为 其中用于关联场景的标识符 会作为 Group Activity 标识符 正如名称所示 如果你没有明确指定 则使用默认行为 你可以使用 content 行为 指定自定义标识符 这适用于每个场景 显示不同内容的 App 且 Group Activity 与该内容相关联 最后 我们还有 none 行为 none 行为禁用场景关联 这意味着不会关联任何场景 因此用户不会在 App 的 任何场景上方看到共享横幅 也就是说 共享不具备空间一致性 这种场景关联行为 应该只在特定情况下使用 例如不需要 额外场景的沉浸式 App 或者场景中显示的内容 对同播共享会话中的 每个参与者都不同的情况下 有时 你可能需要知道 群组会话与哪个场景相关联 如果可以选择 多个场景进行场景关联 则可能会发生这种情况 为了获得这些信息 我们在群组会话上 开放了一个新的名为 sceneSessionIdentifier 的属性 这一属性包括与群组会话 关联的场景会话的标识符 场景关联对于 构建良好的同播共享体验至关重要 因此如果你的 App 支持多个场景 一定要使用场景关联 最后 让我们从 Share 菜单 的角度谈谈同播共享 使用 FaceTime 通话时 每个窗口上方都有一个共享横幅 正如我们刚刚看到的 它会指示在同播共享中 共享哪个场景 但它在同播共享之外也很有用 当场景未共享时 它可以显示共享该场景的不同方式 一种是简单地共享窗口 其他人会看到窗口的非交互式 视频馈送 这适用于所有 App 然而 同播共享 App 不止能做到这些 通过公开与窗口当前内容 相关的 Group Activity 它将显示在该菜单中 这是提高同播共享 activity 启发性的好方法 还可以免除在 App 的 UI 中 设置专用同播共享按钮的需求 App 公开 Group Activity 的方式 与通过隔空投送启动 同播共享的方式相同 在 iOS 17 中 你可以通过隔空投送打开 同播共享 App 来启动同播共享 为了获取 Group Activity 系统会遍历正在显示场景的 UI 响应器链 并尝试找到 在其中一个响应器的 活动项配置中指定的 Group Activity 这样 你只需在显示 SharePlayable 内容的 视图控制器的活动项配置上 设置 Group Activity 它就会被自动拾取 若要配置活动项配置 请首先创建可激活的活动 接下来 你将 创建一个项目提供程序 并在上面注册 Group Activity 然后使用项提供程序 初始化 UIActivityItemsConfiguration 最后 你需要确保 配置公开了正确的元数据 因为这将在 Share 菜单中显示 为此 可以在 UIActivityItemsConfiguration 上 使用 metadataProvider 并为 LinkPresentationMetadata 键值 提供 LPLinkMetadata 对象 标题和图像提供程序 将在 Share 菜单中使用 如果你使用自己的符合 UIActivityItemsConfigurationReading 的类 则所有这些也将起作用 刚刚我向大家讲解了 在为该平台构建 窗口同播共享体验时的注意事项 Mia 可以向我们介绍 如何为沉浸式 App 构建共享体验吗? Mia Ren:当然可以 Willem 现在 让我们看看同播共享 如何构建沉浸式 App 在这个平台上 我们可以使用沉浸式空间 轻松创建沉浸式体验 沉浸式空间是一种特殊的场景 它支持 App 以不同的沉浸风格 使其内容体验更加丰富 当 App 打开一个沉浸式空间时 系统会将其他 App 全部设为背景 人们可以完全沉浸在 App 的无限世界中 观看本讲座 进一步了解 沉浸式空间和沉浸式风格 你可以在 FaceTime 通话期间 随时启动沉浸式空间 当你在一个私人的沉浸式空间里时 由于你在自己的私人世界里 等于离开了共享环境 因此 系统会为你隐藏其他人 他们会看到你的联系人照片 这表明你现在不在他们身边 所有人仍然可以通过音频进行交流 但是 我们不想 一个人在 FaceTime 通话上 所以接下来让我们分享 沉浸式世界中的神奇时刻 我有很多好的沉浸式体验的想法 想和朋友分享 例如 它可以是这样的集体活动 在外太空遨游 共同探索我们美丽的地球 想要实现这一点 我们的共享 App 需要配置一个 可与所有人共享的小组沉浸式空间 在系统协调器的配置中 你可以找到 supportsGroupImmersiveSpace 标志 通过启用它 App 会告知系统 要共享此 App 中的 沉浸式空间 并要支持沉浸式团体活动 当人们加入 启用了此标志的群组会话时 他们的沉浸式空间 将成为小组沉浸式空间 系统将空间原点移动到 由模板定义的共享位置 建立共享坐标系实现空间一致性 在团队沉浸式空间中 人们会将彼此视为角色 现在我们得到了一个有着 共享坐标系统的小组沉浸式空间 人们在其中可以将物体 放置在相同的相对位置 实现空间一致 在地球探索活动示例中 我们可以将地球 放置在原点上方的中心 并确保所有人 相对于原点的偏移量相同 所有人都会在同一个位置看到地球 接下来 我们将研究如何保持视觉一致性 例如 如果我们想旋转地球仪 则需要确保它的朝向在所有人之间 是同步的 进一步了解如何使用 GroupSession 和 GroupSessionMessenger 同步状态 请收看同播共享一节 我们还可以添加一些 与小组沉浸式空间中的 每个人相关的 UI 元素 例如 可以为每个参与者 提供这种个性化控制菜单 菜单放置在参与者面前 人们可以使用控制菜单 轻松操纵共享的地球 为了正确配置这一控件 我们必须知道本地用户 在小组沉浸式空间中的位置 我们可以在 GeometryReader3D 中使用 systemExperienceDisplacement 方法 这个方法可以 让系统将空间原点放置在 远离本地用户的位置 它会返回一个同时具有 平移和旋转功能的 Pose3D 结构 为了获得本地用户 相对于空间原点的位置 我们可以将其反转 然后我们可以使用它 对控件进行偏移和旋转 这样它就会出现在 与本地用户相对的位置 需要注意的一点是 如果用户移动 位移不会更新 它只提供最初放置时 空间的位移 这是一种用来放置 与人相关的内容的简单方法 无需完全跟踪人们的位置 环绕模板非常适合全球探索活动 App 内只有一个小组沉浸式空间 中心是共享内容 但是 如果你的 Group Activity 有一个共享窗口 和一个小组沉浸式空间呢? 例如 我想建立一个 Group Activity 用来研究太阳系 就像这样 人们可以从在共享窗口中 了解有关行星的信息开始 他们可以按下按钮 在集体沉浸式空间中 近距离观察行星模型 我希望人们 在这两种情况下都能聚在一起 让我们看看可以用模板做些什么 默认情况下 当你的 App 只有一个小组沉浸式空间时 将使用环绕模板 并且空间原点放置在中心 当你的 App 也有共享的体积场景时 会应用相同的布局 但是 如果你的 App 具有共享垂直窗口 则默认情况下将选择并排模板 并且空间原点 位于共享窗口的正下方 但请记住 你可以随时将模板偏好 更改为最适合团队活动的模板 你可以选择 .conversational 模板 或 .sideBySide 模板 我认为这两个模板 非常适合我们的太阳系探索活动 正如我们之前所看到的 模板中的距离是根据 共享窗口的大小动态调整的 你可能想了解 当只有一个小组沉浸式空间时 我们如何保证合适的距离 为了解决这个问题 我们 设置了一个旋钮来调整模板 称为环境范围 它是模板首选项的修改器 适用于沉浸式 和基于窗口的 Group Activities App 可以将此值 设置为从环境中心 到其最远边缘 的距离(以点为单位) 设置环境范围后 活动模板将考虑此值 以便对照 App 放置人员 好了 让我们运用学到的模板知识 为之前展示的太阳探索活动 配置群组会话 首先 我们应该启用 supportsGroupImmersiveSpace 这样人们就可以沉浸式查看对象 接下来 让我们将模板首选项 设置为 .sideBySide 并为 contentExtent 添加一个修饰符 这样人们就可以聚在一起 并与共享内容保持很远的距离 看上去很棒 让我们来看看 这个 Group Activity 效果如何 在这里 我和我的朋友 Connor 在一起 并排在一个共享的窗口浏览行星 之后 我按下一个按钮 进入一个小组沉浸式空间探索土星 当我感叹这个模型很精致时 发现 Connor 已经不见了 我看到的只是他的联系照片 但实际上 Connor 被留在了后面 还在看着窗口 他需要自己按下按钮 才能进入小组沉浸式空间 正如我们前面所说 当人们不在同一个 沉浸式小组空间中时 不会共享环境 因此 系统会隐藏空间人物角色 并显示联系人照片 如果这种情况下 我们还能看到彼此的角色 当我对他没有看到的景象 露出感叹的表情时 Connor 可能会 感到非常奇怪 如果我们的小组沉浸式空间 以不同的沉浸式风格呈现 也会出现同样的情况 例如 我正在一个 完全沉浸式的环境中欣赏四周 可 Connor 却从我身边穿过 但如果 Connor 能自动跟着我 进入同一个小组沉浸式空间 那就太好了 当他不和我在一起时 他也可以知道我在哪里并随时加入 为了最大限度地减少沉浸式 Group Activity 中的分裂联系人 我们提供一个很棒的工具 你可以在系统协调器中使用 这个工具叫做 groupImmersionStyle 它提供了一个 可选沉浸式风格的异步序列 将其他人加入的沉浸式空间的 特定沉浸式风格告知你的 App 或者当他们 离开沉浸式空间时将空值传回 例如 当我打开一个 小组沉浸式空间时 Connor 的 App 会接收到它的沉浸式风格 而他的 App 可以打开一个 风格相匹配的小组沉浸式空间 然后我们将一起 在小组沉浸式空间中 再次共享环境 同样 当 Connor 按下按钮 并离开小组沉浸式空间时 我的 App 将收到空值 并关闭沉浸式空间 现在 在沉浸式小组的帮助下 大家可以总是聚在一起 有时 你可能会因为一些 紧急的事情而暂时退出 沉浸式体验 你只需在数码表冠 上按一下就可以了 这样 系统既不会打扰其他人 也不会改变群体沉浸式风格 当你走出去时 会看到一个同播共享横幅通知 它告诉你组中其他人的位置 并提供一个让你轻松 回到小组沉浸式空间按钮 当你点击“加入”按钮时 你的 App 会收到 一个小组沉浸式风格 用于设置小组沉浸式空间 好了 这一节我们介绍了很多内容 一起来回顾下! 我们认识了共享环境 以及如何使用系统协调器 和场景关联来管理 窗口式和沉浸式共享 App 中的 空间和视觉一致性 为你的 App 引入了模板首选项 便于正确安置人员 最后 我们还为你演示了一种 使用 Share 菜单 启动同播共享的新方法 感谢收看 我们迫不及待地想看到 你会用同播共享 创造出什么样的精彩体验 ♪
-
-
4:08 - Observe the local participant state
for await session in ExploreActivity.sessions() { guard let systemCoordinator = await session.systemCoordinator else { continue } let isLocalParticipantSpatial = systemCoordinator.localParticipantState.isSpatial Task.detached { for await localParticipantState in systemCoordinator.localParticipantStates { if localParticipantState.isSpatial { // Start syncing scroll position } else { // Stop syncing scroll position } } } }
-
6:10 - Configure the spatial template preferences
for await session in ExploreActivity.sessions() { guard let systemCoordinator = await session.systemCoordinator else { continue } var configuration = SystemCoordinator.Configuration() configuration.spatialTemplatePreference = .sideBySide systemCoordinator.configuration = configuration session.join() }
-
9:10 - Configuring scene activation conditions
@main struct ExploreTogetherApp: App { var body: some Scene { WindowGroup { ContentView() .handlesExternalEvents( preferring: ["com.example.explore-together.activity"], allowing: ["com.example.explore-together.activity"] ) } } }
-
9:30 - Configuring scene activation conditions
class SceneDelegate: NSObject, UISceneDelegate { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // ... scene.activationConditions.canActivateForTargetContentIdentifierPredicate = NSPredicate(format: "self == %@", "com.example.explore-together.activity") scene.activationConditions.prefersToActivateForTargetContentIdentifierPredicate = NSPredicate(format: "self == %@", "com.example.explore-together.activity") } }
-
10:40 - Setting scene association behavior
struct ExploreActivity: GroupActivity { var metadata: GroupActivityMetadata { var metadata = GroupActivityMetadata() // ... metadata.sceneAssociationBehavior = .content("document-1") return metadata } }
-
13:44 - Starting SharePlay
// Create the activity let activity = ExploreActivity() // Register the activity on the item provider let itemProvider = NSItemProvider() itemProvider.registerGroupActivity(activity) // Create the activity items configuration let configuration = UIActivityItemsConfiguration(itemProviders: [itemProvider]) // Provide the metadata for the group activity configuration.metadataProvider = { key in guard key == .linkPresentationMetadata else { return nil } let metadata = LPLinkMetadata() metadata.title = "Explore Together" metadata.imageProvider = NSItemProvider(object: UIImage(named: "explore-activity")!) return metadata } self.activityItemsConfiguration = configuration
-
16:03 - Configure group ImmersiveSpace
for await session in ExploreActivity.sessions() { guard let systemCoordinator = await session.systemCoordinator else { continue } var configuration = SystemCoordinator.Configuration() configuration.supportsGroupImmersiveSpace = true systemCoordinator.configuration = configuration }
-
17:51 - System Experience Displacement
// Use immersiveSpaceDisplacement to offset contents in group immersive space var body: some Scene { ImmersiveSpace(id: "earth") { GeometryReader3D { proxy in let displacement = proxy.immersiveSpaceDisplacement(in: .global).inverse Control() .offset(displacement.position) .rotation3DEffect(displacement.rotation) } } }
-
20:46 - Spatial Template Preferences
// Configure the spatial template preferences with content extent for await session in ExploreSolarActivity.sessions() { guard let systemCoordinator = await session.systemCoordinator else { continue } var configuration = SystemCoordinator.Configuration() configuration.supportsGroupImmersiveSpace = true configuration.spatialTemplatePreference = .sideBySide.contentExtent(200) systemCoordinator.configuration = configuration }
-
22:32 - Receive group immersion style to configure group immersive space
// Receive group immersion style to configure group immersive space for await session in ExploreSolarActivity.sessions() { guard let systemCoordinator = await session.systemCoordinator else { continue } Task.detached { for await immersionStyle in systemCoordinator.groupImmersionStyle { if let immersionStyle { // Open an immersive space with the same immersion style } else { // Dismiss the immersive space } } } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。