iOS WebRTC 视频聊天 — 可靠连接示例

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 { … }

核心算法

相互连接的两个对等点称为:hostguest。核心算法包括两个主要步骤:

  • SDP交换
  • ICE候选人交换

每个步骤都有一个与之关联的超时 – 如果该步骤未在时间范围内完成,则整个流程将被重置。由于对等方清除了信令服务器上的连接数据(SDP、ICE 候选),该流程也可能被重置。清除连接数据是在外部连接循环中完成的。例如,由于连接超时,可能会发生这种情况。

iOS WebRTC 视频聊天 — 可靠连接示例
连接回路内部流程图

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用作信令服务器

iOS WebRTC 视频聊天 — 可靠连接示例
Firebase 作为信令服务器

单元测试

项目包含多个单元测试:

  • 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

(0)

相关推荐

发表回复

登录后才能评论