WebRTC 是通过网络进行实时通信的事实上的标准库。它支持声音、视频和通用数据流。它是开源的,所有主要平台都支持。
尽管在互联网上看到过许多优秀的 iOS 示例,但它们要么非常过时,要么非常基本。本文旨在弥补现有示例与 WebRTC 实际用例之间的差距。
建立 WebRTC 连接包括多个步骤(简化列表):
- 呼叫方请求 STUN 服务器生成ice候选者
- 呼叫方使用信令服务器将报价传输给呼叫的预期接收者
- 接收方收到报价
- 接收方创建应答
- 呼叫方收到应答。
所有这些步骤都涉及异步网络通信,随时可能因网络不稳定等各种问题而失败。我们该如何恢复呢?如果对等方因某种原因放弃连接尝试怎么办?如何重新启动整个流程?
异步代码编程的 “传统方法 “是使用回调。如果流程比较复杂,如建立 WebRTC 连接,就不可避免地会导致代码错综复杂和“回调地狱”。
WebRTC iOS 视频聊天
VideoChat 是我制作的一个玩具项目,旨在展示我建立可靠 WebRTC 连接和故障恢复的解决方案:
https://github.com/grbaczek/iOS_WebRTC_VideoChat
VideoChat app 由几个屏幕组成,用户可以在其中创建聊天室或加入现有聊天室。建立连接后,摄像机视频和声音将传输到连接的对等点。
使用的技术:
- WebRTC
- Swift 编程语言
- Swift 结构化并发
- SwiftUI
- Firebase firestore
结构化并发在涉及回调代码的场景中非常有用。回调可以转换为异步函数。这意味着错误会传播到父作用域,并且可以使用 Swift 本机错误处理:do { try … } catch { … }
核心算法
相互连接的两个对等点称为:host
和guest
。核心算法包括两个主要步骤:
- SDP交换
- ICE候选人交换
每个步骤都有一个与之关联的超时 – 如果该步骤未在时间范围内完成,则整个流程将被重置。由于对等方清除了信令服务器上的连接数据(SDP、ICE 候选),该流程也可能被重置。清除连接数据是在外部连接循环中完成的。例如,由于连接超时,可能会发生这种情况。
ThrowingTaskGroup
用于收集相关任务。每个Task
都是异步执行的。但是,如果任何组任务失败,则该任务组及其所属的所有任务都将被取消。错误将传播给调用者。内部连接循环实现:
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
if let rtcSessionDescription = try await signalingClient.getRTCSessionDescriptions(
currentPeer.watchKey,
chatRoomId
).first(where: { _ in true }) {
try await webRTCClient.set(remoteSdp: rtcSessionDescription)
connectionStateContainer.info = "Remote SDP set"
if currentPeer == .guest {
let sdp = try await webRTCClient.answer()
try await signalingClient.send(sdp: sdp, chatRoomId: chatRoomId, collection: currentPeer.sendKey)
connectionStateContainer.info = "SDP answer sent"
}
}
}
if currentPeer == .host {
group.addTask {
let sdp = try await webRTCClient.offer()
try await signalingClient.send(sdp: sdp, chatRoomId: chatRoomId, collection: currentPeer.sendKey)
connectionStateContainer.info = "SDP offer sent"
}
}
group.addTask {
for _ in 1...40 {
if webRTCClient.isRemoteDescriptionSet {
return
}
try await Task.sleep(milliseconds: 100)
}
connectionStateContainer.info = "Connection timeout"
throw connectionError.connectionTimeoutError
}
try await group.waitForAll()
}
connectionStateContainer.info = "RTC exchanged"
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
for try await candidate in signalingClient.getCandidates(currentPeer.watchKey, chatRoomId) {
try await webRTCClient.set(remoteCandidate: candidate)
}
connectionStateContainer.info = "Candidates set"
}
group.addTask {
for await state in webRTCClient.getConnectionState() where state == .failed {
connectionStateContainer.info = "Connection failed"
throw connectionError.connectionFailed
}
}
group.addTask {
try await Task.sleep(seconds: 15)
if connectionStateContainer.state != .connected {
connectionStateContainer.info = "Connection timeout"
throw connectionError.connectionTimeoutError
}
}
group.addTask {
// peer has deleted sdp and candidates - reset connection
try await signalingClient.waitUntilSdpAndCandidatesDeleted(
collection: currentPeer.watchKey,
chatRoomId: chatRoomId)
connectionStateContainer.info = "Peer connection reset"
throw connectionError.connectionReset
}
try await group.waitForAll()
}
信令服务器
不幸的是,如果中间没有某种服务器,WebRTC 就无法创建连接。我们称之为信号通道或信令服务。它是在建立连接之前交换信息的任何形式的通信渠道,无论是通过电子邮件、明信片还是信鸽。由你决定。
Firebase firestore用作信令服务器
单元测试
项目包含多个单元测试:
testSimultaneousConnection()
testGuestConnectingFirst()
testHostConnectingFirst()
testHostDisconnected()
testGuestDisconnected()
testRandomConnectionScheme()
用于模拟同时连接的函数:
private func simulateConnection(
_ chatRoomId: String,
guestDelaySec: Int = 0,
hostDelaySec: Int = 0) async
{
let guestWebRTCManager = WebRTCManager()
let hostWebRTCManager = WebRTCManager()
let t = Task {
try await Task.sleep(nanoseconds: 1_000_000_000 * UInt64(guestDelaySec))
await guestWebRTCManager.retryConnect(
chatRoomId: chatRoomId,
currentPeer: WebRTCManager.peer.guest
)
}
let t2 = Task {
try await Task.sleep(nanoseconds: 1_000_000_000 * UInt64(hostDelaySec))
await hostWebRTCManager.retryConnect(
chatRoomId: chatRoomId,
currentPeer: WebRTCManager.peer.host
)
}
let t3 = Task {
for await connectionState in guestWebRTCManager.connectionState {
if connectionState == .connected {
t.cancel()
break
}
}
}
let t4 = Task {
for await connectionState in hostWebRTCManager.connectionState {
if connectionState == .connected {
t2.cancel()
break
}
}
}
await t3.value
await t4.value
}
设置指南
- 克隆 VideoChatApp
- 在克隆的 VideoChat 目录中执行 pod install 命令 – 这将安装 WebRTC 依赖项。本项目使用了非官方的 WebRTC CocoaPods 发行版。对于生产应用程序,您应该构建自己版本的 WebRTC 库。
- 创建新的 Firebase 项目
- 将 iOS 应用添加到新创建的 Firebase 项目中
- 下载 GoogleService-Info.plist,并将其放入项目的 VideoChat 目录中
- 在 Firebase 控制台中创建 Cloud Firestore 数据库(测试模式)
参考:
WebRTC网站: https: //webrtc.org/
WebRTC源代码: https: //webrtc.googlesource.com/src
WebRTC iOS 编译指南:https://webrtc.github.io/webrtc-org/native-code/ios/
作者:Grzegorz Bączek
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/webrtc/31911.html