Enhance voice communication with Push to Talk
System & Services 进阶 20m

用 Push to Talk 增强语音通信

Enhance voice communication with Push to Talk

2022年6月6日

在 Apple 官方观看视频

一句话判断

Apple 终于把 Push to Talk(一键对讲)做成了系统级能力——PTTManager 框架让你不用自己管理 VoIP 推送、音频会话和网络连接,三步就能接入类微信对讲的功能。

这场 Session 讲了什么

这场 Session 介绍了 iOS 16 中新增的 Push to Talk 框架。如果你做过对讲类 App(比如微信的语音消息、Discord 的 Push to Talk),你应该知道这背后的技术实现有多麻烦——VoIP 推送唤醒、音频会话管理、网络传输、后台保活……PTTManager 把这些都封装好了。

PTTManager 架构。 Push to Talk 的核心是 PTTManager 单例。它管理着语音通道的建立、维护和销毁。你的 App 只需要关注三件事:发起通话请求、发送音频数据、接收音频数据。底层的网络传输、音频编解码、系统通知都由框架处理。

语音通道(Channel)。 每个 Push to Talk 会话对应一个 channel。channel 有发送者(transmitter)和接收者(receiver)。同一时间一个 channel 里只有一个发送者——这是对讲的基本规则(半双工通信)。当一个人在说话时,其他人只能听。框架会自动管理”谁在说话”的仲裁逻辑。

后台唤醒。 当你的 App 收到 VoIP 推送时,系统会唤醒你的 App 并把它带到前台。PTTManager 会自动处理音频会话的激活和停用。这意味着用户可以像接电话一样接收对讲——锁屏状态下也能听到语音并回复。

音频处理。 PTTManager 使用系统提供的音频编解码器(Opus),你不需要自己处理 PCM 数据。框架会把编码后的音频数据通过你提供的网络通道传输。如果你的 App 已经有自己的网络层(比如 WebSocket),可以把 PTTManager 的音频数据和你的网络层对接。

UI 组件。 框架提供了一个系统级的 PTT 按钮 UI,也可以自定义。系统按钮会自动显示当前状态(空闲、说话中、接收中),并且在锁屏和控制中心里也能操作。

系统集成。 PTTManager 和系统的音频路由管理集成——用户可以在控制中心选择输出设备(扬声器、蓝牙耳机等)。框架也支持音频中断处理(比如来电话时自动暂停对讲)。

值得深挖的点

半双工通信的仲裁机制。 PTTManager 的 channel 管理遵循一个简单规则:先到先得。第一个人按下按钮开始说话,其他人如果想说话需要等当前说话的人松开按钮。框架通过 PTTChanneltransmitter 属性告诉你当前谁在说话。如果你的 App 需要更复杂的仲裁(比如管理员优先),需要在应用层自己实现——框架只提供基础的”一人说话多人听”模型。

VoIP 推送和 PTT 的配合。 PTT 依赖 VoIP 推送来唤醒 App。你需要在 APNs 里注册 VoIP 推送类型,然后在收到推送时调用 PTTManager.handlePush(payload:)。框架会根据 payload 里的 channel 信息自动恢复或创建 channel。如果你之前没有用过 VoIP 推送,需要先实现 PKPushRegistry 相关代码。

代码片段

初始化 PTTManager 并加入 channel:

import PushToTalk

class VoiceChatManager: NSObject, PTTManagerDelegate {
    let pttManager = PTTManager.shared

    func setup() {
        pttManager.delegate = self
        // 注册 VoIP 推送
        let pushRegistry = PKPushRegistry(queue: nil)
        pushRegistry.delegate = self
        pushRegistry.desiredPushTypes = [.voIP]
    }

    // 加入或创建 channel
    func joinChannel(id: String) {
        let channelDescriptor = PTTChannelDescriptor(
            channelID: id,
            channelName: "Team Alpha"
        )
        pttManager.joinChannel(descriptor: channelDescriptor)
    }
}

// PTTManagerDelegate
extension VoiceChatManager {
    func pttManager(_ manager: PTTManager, didJoinChannel channel: PTTChannel) {
        // 成功加入 channel
        print("已加入频道: \(channel.channelID)")
    }

    func pttManager(_ manager: PTTManager, didReceiveTransmission transmission: PTTTransmission) {
        // 收到语音传输
        // transmission 包含发送者信息和音频数据
        playAudio(transmission.audioData)
    }

    func pttManagerDidDisconnect(_ manager: PTTManager, reason: PTTDisconnectReason) {
        // 断开连接
        handleDisconnect(reason)
    }
}

处理 VoIP 推送:

extension VoiceChatManager: PKPushRegistryDelegate {
    func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) {
        guard type == .voIP else { return }
        // 让 PTTManager 处理推送
        pttManager.handlePush(payload: payload.dictionaryPayload)
    }
}

最佳实践

音频数据传输不要用 HTTP 请求——延迟太高。推荐使用 WebSocket 或 UDP 来传输 PTT 音频数据。PTTManager 负责音频的采集和播放,但数据的”搬运”需要你自己的网络层来完成。

在 channel 里实现”说话超时”逻辑。如果一个用户按住按钮超过 30 秒还不松手,应该自动结束他的发言。PTTManager 不会帮你做这个——它只管理通道状态,不管业务逻辑。你需要在 App 层用 Timer 来检测超时并调用 endTransmission()

处理音频中断事件。当用户正在对讲时来了电话或者打开了另一个音频 App,PTTManager 会收到音频中断通知。你的 App 应该在 pttManagerDidDisconnect 回调里给用户一个明确的提示(比如”对讲已暂停”),而不是让用户莫名其妙地发现声音没了。

还有什么值得关注

  • PTTManager 需要 com.apple.developer.push-to-talk entitlement,在 App Store Connect 的 App ID 配置里开启。
  • Push to Talk 目前只支持 iOS,不支持 macOS 或 watchOS。
  • 音频编解码使用 Opus codec,采样率 16kHz,比特率约 32kbps。如果你需要更高音质,可能需要自己实现音频处理管线。
  • PTT 的 VoIP 推送配额和普通 VoIP 推送共享。如果你的 App 同时还有 VoIP 通话功能,注意不要超出推送限制。
WWDC 2022