用 Push to Talk 增强语音通信
Enhance voice communication with Push to Talk
2022年6月6日
一句话判断
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 管理遵循一个简单规则:先到先得。第一个人按下按钮开始说话,其他人如果想说话需要等当前说话的人松开按钮。框架通过 PTTChannel 的 transmitter 属性告诉你当前谁在说话。如果你的 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-talkentitlement,在 App Store Connect 的 App ID 配置里开启。 - Push to Talk 目前只支持 iOS,不支持 macOS 或 watchOS。
- 音频编解码使用 Opus codec,采样率 16kHz,比特率约 32kbps。如果你需要更高音质,可能需要自己实现音频处理管线。
- PTT 的 VoIP 推送配额和普通 VoIP 推送共享。如果你的 App 同时还有 VoIP 通话功能,注意不要超出推送限制。