大多数浏览器和
Developer App 均支持流媒体播放。
-
利用 Push to Talk 优化语音通信
我们将帮助为您的 App 添加对讲机通信功能,让对话变得一清二楚!了解如何为您的 Push to Talk App 添加醒目的系统 UI,一键实现快速沟通。我们将介绍 PushToTalk 框架,解释如何配置您的 App 以便其随时 (甚至在后台) 都能收发音频。为能更好地理解此讲座,我们建议您先熟悉如何在 App 后台处理音频传输。另外,对 APN 有一些基本的事先了解也会很有帮助。
资源
相关视频
Tech Talks
WWDC22
-
下载
♪ 柔和乐器演奏的嘻哈音乐 ♪ ♪ Kevin Ferrell: 嗨 我是 Kevin 是研究 PushToTalk 新框架的工程师 该框架能使 iOS 上的 App 体验对讲机式的系统 稍后我的同事 Trevor 会加入进来 介绍如何在您的 App 中 使用这个新框架 增强语音交流的方法 首先我来介绍一下 PushToTalk 框架 并解释它如何适配您的 App 其次 我们会介绍 如何为 PushToTalk 配置您的 App 随后 Trevor 将详细介绍 使用此框架传输和接收音频的方法 最后 Trevor 将总结增强 一键通用户体验的最佳实践 同时如何为您的用户节省电量 我先从 PushToTalk 新框架的 关键功能讲起 PushToTalk 框架能使您 在 iOS 上构建一类 新的音频通信 App 为您的用户提供对讲机式的体验 一键通 App 在需要快速沟通的领域有很多用途 如医疗保健和紧急服务 为了实现良好的一键通体验 用户需要某种方法来快速访问 音频传输功能 同时还能看到 谁在回应他们 与此同时 一键通 App 必须节能 确保用户 在使用 App 时 能保持一整天的电量 PushToTalk 框架 为您提供 API 以利用系统 UI 用户无需直接启动您的 App 就可访问系统的任何位置 系统 UI 允许用户快速激活 音频传输 这将在后台启动您的 App 录制并将音频传输到您的服务器 当您的 App 从服务器 播放音频时 该系统会显示说话人 从而为用户增加透明度 PushToTalk 框架 实现这一点的方法 是通过引入新的推送通知类型 当有新的音频 可供播放时就会通知您的 App 您的 App 收到此通知后 它就会在后台启动 以便传输和播放音频 PushToTalk 框架可与 现有的端到端通信方法 以及后端基础设施兼容 如果您已在 您的 App 中实施了 一键通工作流程 那么应该很容易 将 PushToTalk 框架 集成到您现有的代码中 该框架允许您的 App 使用 自己的音频编码和传输程序 从而在用户之间传输音频 这为您的 App 处理音频传输方式 提供了灵活性 并能与其他平台兼容 最后 许多一键通 App 依赖无线蓝牙配件 触发录音和传输功能 您的 App 可使用 CoreBluetooth 框架 持续与上述配件集成使用 并且可以在 PushToTalk 中触发录音 如果您正在构建 您的第一个一键通 App 在开始构建代码时请牢记 以上集成注意事项 在开始详细介绍 PushToTalk 新框架的代码之前 我们想先展示一键通 在您 App 中的运行方式 我和 Trevor 构建了一个演示 App 来展示 PushToTalk 的工作原理 首先 我将点击加入按钮 连接到一键通会话 我们称之为频道 加入频道后 我可以接收或将音频 发送给频道上的其他成员 Trevor 和我们的同事 加入了同一频道 这样我们就可全天候交流 使用麦克风按钮 我可以直接 从 App 传输音频 而 PushToTalk 框架 可允许我从系统中的任何位置 访问传输功能 当有激活的一键通频道时 一个蓝色药丸状图标 就会出现在状态栏中 点击该图标可显示系统 UI 系统 UI 会显示 我所加入的一键通频道的名称 以及 App 提供的用于 帮助用户快速识别频道的图像 我可以按住通话按钮 向频道传输音频 然后等待系统提示音 我就可以开始说话了
嘿 Trevor 您准备好展示您的 WWDC 幻灯片了吗?完毕 Trevor Sheridan: 当我的设备收到 Kevin 的信息时 就显示出一个包含 他的名字和图像的通知 公开透明地显示谁向我发送了消息 一旦我启动系统 UI 我就可以快速回复 Kevin 的消息 或者继续做我手头的事 先不回复 我不想让 Kevin 久等 所以我现在就回复 嘿 Kevin 再有几分钟就准备好了 完毕 Kevin:PushToTalk 系统 UI 也可 从锁屏界面访问 用户无需解锁设备 即可接收回复消息
好的 一会儿见 完毕 我们已经讨论了 PushToTalk 的工作原理 现在来介绍一下如何 将框架集成到您自己的 App 中 您需要对您的 Xcode 项目 进行一些修改以支持 PushToTalk 框架 首先 您需要添加 新的一键通后台模式 这能让您的 App 在响应一键通事件时 可以在后台运行 接下来 您还需 为您的 App 添加 一键通功能以启用框架功能 需要推送通知功能 才能允许 APNS 在后台唤醒您的 App 播放接收到的音频 最后 您的 App 必须向用户请求 录音许可 并在其 Info.plist 文件中 包含麦克风用途字符串 现在我们准备开始集成代码 一键通工作流程的第一步 是加入一个频道 频道代表并描述了 到系统的一键通会话 您的 App 通过 频道管理器与频道交互 频道管理器是 您的 App 加入频道 执行发送和接收音频等操作的 主要界面 您加入频道后 一键通系统 UI 立即启用 您的 App 也会收到一个 APNS 设备令牌 可在频道的整个生命周期中使用 您必须先加入频道 然后才能开始传输 和接收音频 第一步是使用类初始化器 创建频道管理器 该初始化器要求您提供 频道管理器委托和频道恢复委托 多次调用初始化器 会导致相同的共享实例被返回 但我们建议您在实例变量中 存储频道管理器 App 在您 ApplicationDelegate 的 didFinishLaunchingWithOptions 方法中 启动期间 尽快初始化您的频道管理器 非常重要 这样可以确保快速初始化频道管理器 以便恢复现有频道 推送通知也会在您的 App 后台启动时发送过来 现在我们准备加入一个频道 当有人通过 您的 App 加入频道时 您必须提供一个 识别频道的 UUID 和一个描述系统频道的描述符 在该频道的 整个生命周期中 与管理器交互时 使用的是相同的 UUID 描述符包括名称和图像 提供独特的图像来代表频道 可让您的用户在与系统交互时 更易识别频道 您的 App 通过 在频道管理器上调用 requestJoin 方法加入频道 请注意 只有当您的 App 在前台运行时才可加入频道 当您的 App 加入频道时 频道管理器委托的 didJoinChannel 方法将被调用 这个委托方法是您的指示 表明您的 App 已加入频道 此外 该委托的 receivedEphemeralPushToken 方法也将被调用 同时 APNS 推送令牌可用于 将一键通通知发送到该设备 此令牌只在 一键通频道的生命周期内有效 请注意 APNS 推送令牌 长度可变 但您不应将其长度 硬编码到您的 App 中 频道加入请求可能会失败 例如在另一个频道处于激活状态时 尝试加入频道 若发生此类情况 错误处理程序将被调用 错误会指明失败原因 当用户离开频道时 将调用 委托的 didLeaveChannel 方法 您的用户可通过您的 App 以编程方式请求离开频道 或者用户可以点击系统 UI 中的 “离开频道”按钮离开 通道管理器委托有一个关联的 LeaveChannel 错误处理方法将被调用 来应对请求离开频道失败的情况 每当您的 App 被终止 或在设备重启后 再次启动时 PushToTalk 都支持恢复 之前打开的频道 为让系统实现这一点 您必须提供频道描述符来更新系统 这里我们可以用一个辅助方法来获取 在恢复后的委托中缓存的频道描述符 为保持系统响应 您应尽快从该方法返回 且不应执行 任何长时间运行或阻塞的任务 比如检索您 频道描述符的网络请求 在您的一键通会话的 整个生命周期中 您应在频道信息发生变化时 为描述符提供更新 您还应使用服务状态对象 告知系统您的网络连接或 服务器是否可用 这里 我们正在更新频道的描述符 您可在您需要 更新频道名称或图像时调用此方法 在此示例中 我们向系统提供更新 以表明 App 与其服务器的 连接处于重新连接状态 这也会相应更新系统 UI 阻止用户在服务状态为 正在连接或断开时传输音频 重新建立连接后 您应将服务状态更新为“就绪” 现在我们来看 使用 PushToTalk 发送和接收音频的方法 Trevor 您准备好讲解 该 API 的其余内容了吗? 完毕
Trevor:准备好了 发送 完毕
我们已经看到了配置 PushToTalk 框架的方法 现在我们来探索 传输和接收音频的方法 PushToTalk 框架的 核心功能 是让您的用户快速传输音频 用户可以从您的 App 中 或从系统一键通 UI 中 开始音频传输 如果您的 App 可通过 CoreBluetooth 支持蓝牙配件 您也可在后台开始传输 响应外围设备的特性变化 传输时 PushToTalk 框架 会解锁设备麦克风 并激活您 App 的音频会话 在后台启用音频录制 我们再来详细研究一下这个过程 要从您的 App 中 开始传输 您可调用 requestBeginTransmitting 函数 每当您的 App 在前台运行 或对蓝牙外围设备特性的变化 做出反应时 都可以调用该函数 若系统无法开始传输 委托的 failedToBeginTransmittingInChannel 方法 将被调用并说明失败原因 例如 若用户正在进行蜂窝呼叫 他们就无法开启一键通传输 要停止传输 可调用频道管理器的 stopTransmitting 方法 为处理尝试停止传输时的失败情况 例如当用户未处于传输状态时 频道管理器委托有一个关联的 failedToStopTransmittingInChannel 方法 无论您是从 App 内开始传输 还是用户从系统 UI 开始传输 您的频道管理器委托都将收到 “已开始传输”回调 传输源将传递给方法 并指明传输 是从系统 UI 编程 API 还是 硬件按钮事件开始 一旦开始传输 系统将为您的 App 激活音频会话 这就是可以开始录制的信号 您不应开始或停止自己的音频会话 传输结束时 您的频道管理器委托 将收到结束传输 和音频会话停用事件 请记住 当您的传输处于激活状态时 您的音频会话可能会被其他来源中断 例如电话和 FaceTime 通话 这需要您在 App 内进行处理 PushToTalk 框架 还允许您的 App 在后台接收和播放来自 其他用户的音频 此过程依赖一种新的 专门针对一键通 App 的 Apple 推送通知类型 当您的一键通服务器有新的音频 供用户接收时 它应使用您在加入频道时 接收到的设备推送令牌 向用户发送一键通通知 当您的 App 收到推送通知时 必须向框架报告有源扬声器 这将导致系统激活 您 App 的音频会话 并允许其开始播放 新的一键通通知 类似于 iOS 上的其他通知类型 而且您必须设置某些具体属性 以启用您的 一键通 App 推送功能 首先 APNS 推送类型 必须在请求头中 设置为“pushtotalk” 其次 APNS 主题头必须设置为 您 App 的捆绑标识符 并在末尾加上 “.voip-ptt”后缀 推送负载可以包含 与您 App 相关的自定义密钥 比如有源扬声器的名称 或会话已结束 App 应退出 一键通频道的标识 “aps”属性的主体可以留空 此外 与其他与通信相关的 推送类型一样 一键通有效负载的 APNS 优先级应为 10 以便要求立即推送 APNS 到期时间应为零 以阻止 与未来推送不再相关的陈旧推送 当您的服务器发送一键通通知时 您的 App 将在后台启动 传入的推送委托方法将被调用 当您收到推送有效负载时 您需构建一个推送结果类型 指示推送通知后 应该执行的操作 为表明远程用户正在讲话 需返回一个推送结果 其中包含 活跃参与者的信息 包括他们的名称和可选图像 这会让系统 设置频道的活跃参与者 表明频道处于接收模式 然后系统将激活您的音频会话 调用 didActivateaudioSession 委托方法 您应在开始播放之前 等待该方法被调用 如果您的服务器决定 不应再让用户加入频道 它可能会在推送 有效负载时表明这一点 您可为其返回 leaveChannel 推送结果 务必注意 您应尽快 从此方法中返回 PTPushResult 而非阻塞线程 如果您尝试设置 正在活跃的远程参与者 但却没有将参与者的图像存储在本地 您可返回一个只有说话者名称的 activeRemoteParticipant 然后在单独的线程上下载其图像 一旦检索到图像 即可在频道管理器上 调用 setActiveRemoteParticipant 以更新 activeRemoteParticipant 当远程参与者说完话后 您应将 activeRemoteParticipant 设置为 nil 这会向系统表明您不再 在频道上接收音频 且系统应该停用您的音频会话 这也会更新系统一键通 UI 允许用户再次发送 现在我们已经介绍了 将 PushToTalk 集成到您 App 中的基本操作 我们再来看优化用户体验 以及保存电量的 最佳操作
PushToTalk 框架 提供了一个系统 UI 供用户开始传输 并从系统内的任何地方离开频道 此外 该框架非常灵活 允许您 在您的 App 前台运行时 使用自定义的一键通 UI PushToTalk 框架 可利用共享系统资源 系统上一次只能激活一个 一键通 App 且一键通通信可被 蜂窝电话 FaceTime 通话 和 VoIP 通话取代 您的 App 应妥善处理 PushToTalk 失败情况 并相应做出响应 如前所述 PushToTalk 框架 为您激活和停用 音频会话 但是 您仍应配置 音频会话类别 以便在您的 App 启动时 播放和录制音频 系统提供了内置音效 以便在传输时提醒用户 麦克风已激活还是已停用 您不应 自定义音效 监控您的 App 并响应 AVAudioSession 通知也很重要 例如会话中断 路线变化和故障等 同系统上的其他音频 App 一样 您的一键通 App 也可能会受到 这些音频会话事件的影响 优化您的 App 以保存电量非常重要 PushToTalk 框架 在需要时会 为您的 App 提供后台运行时间 比如在传输和接收音频时就会如此 当用户没有使用您的 App 时 它将被系统暂停以保存电量 您不应自己激活或停用 您的音频会话 系统将适时为您处理 音频会话激活程序 这可确保您的音频会话 在系统内有适当的优先级 可在不使用时暂停 您的一键通服务器 应该使用新的推送通知类型 提醒您的 App 有新的音频要播放 或一键通会话已结束 有关延长您的 App 电池续航时间的 更多信息 请参考 “断电:减少电池消耗”讲解视频 当您的一键通 App 在后台运行 且 App 没有传输或接收音频时 它将被系统暂停 您的 App 被暂停后 任何网络连接都将断开 您应考虑采用 Network.framework 和 QUIC 以减少 建立 TLS 安全连接 以及提高初始化连接速度所需的步骤 Network.framework 内置支持 QUIC 想了解更多信息 请观看 “减少网络延迟 使 App 更灵敏”的讲解 以了解使用 QUIC 的方法 PushToTalk 框架能让您 在 App 中构建 稳固 省电 对讲机式的 交流体验 如果您已经拥有一个 App 可在 iOS 上实现对讲机式体验 您可更新现有 App 以使用新的 API 如果您正在使用 对讲机式的新 App 您应该立即使用 PushToTalk 框架 最后 您在开始测试新框架 将其集成到您的 App 上时 可向我们提交反馈 谢谢 祝您体验 WWDC 愉快 完毕 退出 ♪
-
-
6:52 - Creating a Channel Manager
func setupChannelManager() async throws { channelManager = try await PTChannelManager.channelManager(delegate: self, restorationDelegate: self) }
-
7:33 - Joining a Channel
func joinChannel(channelUUID: UUID) { let channelImage = UIImage(named: "ChannelIcon") channelDescriptor = PTChannelDescriptor(name: "Awesome Crew", image: channelImage) // Ensure that your channel descriptor and UUID are persisted to disk for later use. channelManager.requestJoinChannel(channelUUID: channelUUID, descriptor: channelDescriptor) }
-
8:11 - PTChannelManagerDelegate didJoinChannel
func channelManager(_ channelManager: PTChannelManager, didJoinChannel channelUUID: UUID, reason: PTChannelJoinReason) { // Process joining the channel print("Joined channel with UUID: \(channelUUID)") } func channelManager(_ channelManager: PTChannelManager, receivedEphemeralPushToken pushToken: Data) { // Send the variable length push token to the server print("Received push token") }
-
8:45 - PTChannelManagerDelegate failedToJoinChannel
func channelManager(_ channelManager: PTChannelManager, failedToJoinChannel channelUUID: UUID, error: Error) { let error = error as NSError switch error.code { case PTChannelError.channelLimitReached.rawValue: print("The user has already joined a channel") default: break } }
-
9:00 - PTChannelManagerDelegate didLeaveChannel
func channelManager(_ channelManager: PTChannelManager, didLeaveChannel channelUUID: UUID, reason: PTChannelLeaveReason) { // Process leaving the channel print("Left channel with UUID: \(channelUUID)") }
-
9:22 - PTChannelRestorationDelegate
func channelDescriptor(restoredChannelUUID channelUUID: UUID) -> PTChannelDescriptor { return getCachedChannelDescriptor(channelUUID) }
-
10:12 - Provide channel descriptor updates
func updateChannel(_ channelDescriptor: PTChannelDescriptor) async throws { try await channelManager.setChannelDescriptor(channelDescriptor, channelUUID: channelUUID) }
-
10:20 - Provide service status updates
func reportServiceIsReconnecting() async throws { try await channelManager.setServiceStatus(.connecting, channelUUID: channelUUID) } func reportServiceIsConnected() async throws { try await channelManager.setServiceStatus(.ready, channelUUID: channelUUID) }
-
11:48 - Start transmission from within your app
func startTransmitting() { channelManager.requestBeginTransmitting(channelUUID: channelUUID) } // PTChannelManagerDelegate func channelManager(_ channelManager: PTChannelManager, failedToBeginTransmittingInChannel channelUUID: UUID, error: Error) { let error = error as NSError switch error.code { case PTChannelError.callIsActive.rawValue: print("The system has another ongoing call that is preventing transmission.") default: break } }
-
12:22 - Stop transmission from within your app
func stopTransmitting() { channelManager.stopTransmitting(channelUUID: channelUUID) } func channelManager(_ channelManager: PTChannelManager, failedToStopTransmittingInChannel channelUUID: UUID, error: Error) { let error = error as NSError switch error.code { case PTChannelError.transmissionNotFound.rawValue: print("The user was not in a transmitting state") default: break } }
-
12:41 - Responding to begin transmission delegate events
func channelManager(_ channelManager: PTChannelManager, channelUUID: UUID, didBeginTransmittingFrom source: PTChannelTransmitRequestSource) { print("Did begin transmission from: \(source)") } func channelManager(_ channelManager: PTChannelManager, didActivate audioSession: AVAudioSession) { print("Did activate audio session") // Configure your audio session and begin recording }
-
13:19 - Responding to end transmission delegate events
func channelManager(_ channelManager: PTChannelManager, channelUUID: UUID, didEndTransmittingFrom source: PTChannelTransmitRequestSource) { print("Did end transmission from: \(source)") } func channelManager(_ channelManager: PTChannelManager, didDeactivate audioSession: AVAudioSession) { print("Did deactivate audio session") // Stop recording and clean up resources }
-
15:29 - Receiving Push to Talk Pushes
func incomingPushResult(channelManager: PTChannelManager, channelUUID: UUID, pushPayload: [String : Any]) -> PTPushResult { guard let activeSpeaker = pushPayload["activeSpeaker"] as? String else { // If no active speaker is set, the only other valid operation // is to leave the channel return .leaveChannel } let activeSpeakerImage = getActiveSpeakerImage(activeSpeaker) let participant = PTParticipant(name: activeSpeaker, image: activeSpeakerImage) return .activeRemoteParticipant(participant) }
-
17:03 - Stop receiving audio
func stopReceivingAudio() { channelManager.setActiveRemoteParticipant(nil, channelUUID: channelUUID) }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。