IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> iOS WebRTC多人音视频建立的流程 -> 正文阅读

[移动开发]iOS WebRTC多人音视频建立的流程

在这里插入图片描述

前言

本文主要以“代码是最好的注释”为基点,介绍在处理iOS端多人音视频的建立流程。 在看本篇前建议先了解一下多人音视频通讯现在的常用架构,参考《WebRTC多人音视频聊天架构与实战》。 本方案使用的是当中提到的第一种架构:Mesh 架构

六大事件

  • join : 加入房间
  • offer : 本端群发 offer及接收对端offer的处理
  • answer : 本端发送answer及接收对端发来answer的处理
  • ice_candidate : 对端的网络地址通过 socket 转发给本端
  • new_peer: 成员进入
  • remove_peer: 成员离开

join

  • 首先,多人音视频的发起者,通过信令发送’__join’ 加入房间消息。
  • 信令服务器收到消息后,会根据用户所加入的房间, 向房间里的其他用户发送’_peers’ 消息 ,信令中包含聊天室中其他用户的信息,客户端根据信息来逐个构建与其他用户的点对点连接

offer

群发offer

上面说到,信令服务器在接收到’__join’ 消息后, 转而发送包含房间内其他用户信息的 ‘__peer’ 消息。 那么其他端在收到’__peer’ 消息后该如何处理呢 ?

没错,在收到’_peer'消息后, 本端需要向房间里的其他用户发送offer 。 发送offer的过程如下:

  • 记录所有对端连接的ID
- (void)_addToConnectionIDS:(NSArray <NSString *>*)connections
{
    if (connections.count == 0) return;
    
    [connections enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSLog(@"%@", obj);
    }];
    [_peerConnectionIDS addObjectsFromArray:connections];
}

其中数组connections__peer信令返回的房间内其他用户的连接ID, 本端维护一个对端各连接点ID的数组_peerConnectionIDS

  • 建立本地流信息
- (void)_setupForLocalStream
{  
    if (!_localStream) {
        // 设置点对点工厂
        [self _setupForFactory];
        
		//本地流 通过工厂来创建
		_localStream = [_factory mediaStreamWithStreamId:@"ARDAMS"];
       
        //音频轨对象 通过工厂 来创建
        RTCAudioTrack *audioTrack = [_factory audioTrackWithTrackId:@"ARDAMSa0"];
		 // 将音频轨迹添加到本地流媒体
        [_localStream addAudioTrack:audioTrack];

        AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
        NSArray<AVCaptureDevice *> *devices;
        if(authStatus == AVAuthorizationStatusRestricted || authStatus == AVAuthorizationStatusDenied) { // 摄像头权限
            if ([_delegate respondsToSelector:@selector(webRTCHelper:setLocalStream:)]) {
                [_delegate webRTCHelper:self setLocalStream:nil];
            }
        }
        else {
#if __IPHONE_OS_VERSION_MIN_REQUIRED < 100000
            if ([AVCaptureDeviceDiscoverySession class]) {
                AVCaptureDeviceDiscoverySession *deviceDiscoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInWideAngleCamera] mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionFront];
                devices = [deviceDiscoverySession devices];
            }
            else {
                devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
            }
#else
            AVCaptureDeviceDiscoverySession *deviceDiscoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInWideAngleCamera] mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionFront];
            devices = [deviceDiscoverySession devices];
#endif
            
            AVCaptureDevice *device = [devices lastObject];
            
            if (device) {
                
                RTCAVFoundationVideoSource *videoSource = [_factory avFoundationVideoSourceWithConstraints:[self _setupForLocalVideoConstraints]];
                [videoSource setUseBackCamera:NO];

				//视频轨对象 也是通过工厂来创建
                RTCVideoTrack *videoTrack = [_factory videoTrackWithSource:videoSource trackId:@"ARDAMSv0"];
                // 添加视频轨迹
                [_localStream addVideoTrack:videoTrack];
                
                
                if ([_delegate respondsToSelector:@selector(webRTCHelper:setLocalStream:)]) {
                    [_delegate webRTCHelper:self setLocalStream:_localStream];   
                }
                
            }
            else {
                if ([_delegate respondsToSelector:@selector(webRTCHelper:setLocalStream:)]) {
                    [_delegate webRTCHelper:self setLocalStream:nil];
                }
            }
        }
    }
}

本地流的创建是通过点对点工厂RTCPeerConnectionFactory 来创建。 本地流创建出来后,需要将本端的音频轨及视频轨添加到流媒体中。本地流创建成功后,可以通过回调方法,回调给使用者用来显示本端视频

  • 建立所有连接
- (void)_setupForConnections
{
    [_peerConnectionIDS enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        RTCPeerConnection *connection = [self _createConnection:obj];
        [_peerConnections setObject:connection forKey:obj];
    }];
}


- (RTCPeerConnection *)_createConnection:(NSString *)connectionID
{
    [self _setupForFactory];
    [self _setupForIceServers];
    RTCConfiguration *configuration = [[RTCConfiguration alloc] init];
    [configuration setIceServers:_iceServers];
    return [_factory peerConnectionWithConfiguration:configuration constraints:[self _setupForPeerVideoConstraints] delegate:self];
}

如果要与对端所有人进行通讯,必须要先和对端所有人建立连接。 上面维护了一个对端各连接点ID的数组_peerConnectionIDS, 通过遍历这个数组创建对端各点对点的连接。 接着,本地再维护一个字典_peerConnections , key为连接点ID, value为点对点连接的对象RTCPeerConnection

  • 所有连接添加本地流信息
- (void)_addLocalStreamToConnections
{
    [_peerConnections enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, RTCPeerConnection * _Nonnull obj, BOOL * _Nonnull stop) {
        [self _setupForLocalStream];
        [obj addStream:_localStream];
    }];
}

遍历本地维护的所有对端点对点连接的字典_peerConnections, 让对端所有连接都添加本地流信息。

  • 所有连接发送 SDP offer
- (void)_sendSDPOffersToConnections
{
    [_peerConnections enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, RTCPeerConnection * _Nonnull obj, BOOL * _Nonnull stop) {
        [self _sendSDPOfferToConnection:obj];
    }];
}

- (void)_sendSDPOfferToConnection:(RTCPeerConnection *)connection
{
    _role = RoleCaller;
    /** Generate an SDP offer. */
    [connection offerForConstraints:[self _setupForOfferOrAnswerConstraint] completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) {
        if (error) {
            NSLog(@"offerForConstraints error");
            return;
        }
        
        if (sdp.type == RTCSdpTypeOffer) {
            __weak __typeof(connection) wConnection = connection;
            /** Apply the supplied RTCSessionDescription as the local description. */
            // A:设置连接本端 SDP
            [connection setLocalDescription:sdp completionHandler:^(NSError * _Nullable error) {
                if (error) {
                    NSLog(@"setLocalDescription error");
                    return;
                }
                
                [self _didSetSessionDescription:wConnection];
            }];
        }
    }];
}

此处是关键点,上面所有的操作都在为此做准备。发送offer的过程如下

  • 通过 RTCPeerConnection 对象的offerForConstraints:生成 sdp offer
  • 将拿到的sdp 通过RTCPeerConnection 对象的setLocalDescription:设置为本地sdp
  • 本地sdp设置成功后需要通过信令将本端sdp发送到对端
- (void)_didSetSessionDescription:(RTCPeerConnection *)connection
{
    NSLog(@"signalingState:%zd role:%zd", connection.signalingState, _role);
    NSString *connectionID = [self _findConnectionID:connection];

    if (connection.signalingState == RTCSignalingStateHaveRemoteOffer) { // 新人进入房间就调(远端发起 offer)
        /** Generate an SDP answer. */
        [connection answerForConstraints:[self _setupForOfferOrAnswerConstraint] completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) {
            if (error) {
                NSLog(@"answerForConstraints error");
                return;
            }
            
            if (sdp.type == RTCSdpTypeAnswer) {
                __weak __typeof(connection) wConnection = connection;
                // B:设置连接本端 SDP
                [connection setLocalDescription:sdp completionHandler:^(NSError * _Nullable error) {
                    if (error) {
                        NSLog(@"setLocalDescription error");
                        return;
                    }
         
                    [self _didSetSessionDescription:wConnection];
                }];
            }
        }];
    }
    else if (connection.signalingState == RTCSignalingStateHaveLocalOffer) { // 本地发送 offer
        if (_role == RoleCaller) {
            NSDictionary *dic = @{@"eventName": @"__offer",
                                  @"data": @{@"sdp": @{@"type": @"offer",
                                                       @"sdp": connection.localDescription.sdp
                                                       },
                                             @"socketId": connectionID
                                             }
                                  };
            RTMMessage *message = [RTMMessage createChannelMessageWithChannelId:self.channelId data:[dic mj_JSONString]];
            [_channel sendMessage:message completion:^(RTMSendChannelMessageStatusCode code) { }];
        }
    }
    else if (connection.signalingState == RTCSignalingStateStable) { // 本地发送 answer
        if (_role == RoleCalled) {
            NSDictionary *dic = @{@"eventName": @"__answer",
                                  @"data": @{@"sdp": @{@"type": @"answer",
                                                       @"sdp": connection.localDescription.sdp
                                                       },
                                             @"socketId": connectionID
                                             }
                                  };
           
            RTMMessage *message = [RTMMessage createChannelMessageWithChannelId:self.channelId data:[dic mj_JSONString]];
            [_channel sendMessage:message completion:^(RTMSendChannelMessageStatusCode code) {}];
        }
    }
}

这个方法内汇总了各情况下处理,此处我们只需要看这里:

else if (connection.signalingState == RTCSignalingStateHaveLocalOffer) { // 本地发送 offer
        if (_role == RoleCaller) {
            NSDictionary *dic = @{@"eventName": @"__offer",
                                  @"data": @{@"sdp": @{@"type": @"offer",
                                                       @"sdp": connection.localDescription.sdp
                                                       },
                                             @"socketId": connectionID
                                             }
                                  };
            RTMMessage *message = [RTMMessage createChannelMessageWithChannelId:self.channelId data:[dic mj_JSONString]];
            [_channel sendMessage:message completion:^(RTMSendChannelMessageStatusCode code) { }];
        }
    }

通过signalingState属性可以拿到当前RTCPeerConnection 对象的信令状态,当状态值为RTCSignalingStateHaveLocalOffer时表示本地发送offer。 在构造好约定的传输格式@{@"type": @"offer",@"sdp": connection.localDescription.sdp } 及信令eventName '__offer'后,通过信令服务器发出。

接收offer

/**
 远端发来 offer
 */
- (void)_receiveOffer:(NSMutableDictionary *)resultDic
{
    // 设置当前角色状态为被呼叫,(被发offer)
    _role = RoleCalled;
    NSDictionary *dataDic = resultDic[@"data"];
    NSDictionary *sdpDic = dataDic[@"sdp"];
    // 拿到SDP
    NSString *sdp = sdpDic[@"sdp"];
    NSString *type = sdpDic[@"type"];
    NSString *connectionID = dataDic[@"socketId"];
    RTCSdpType sdpType = [self _typeForString:type];
    // 拿到这个点对点的连接
    RTCPeerConnection *connection = [_peerConnections objectForKey:connectionID];
    // 根据类型和SDP 生成SDP描述对象
    RTCSessionDescription *remoteSdp = [[RTCSessionDescription alloc] initWithType:sdpType sdp:sdp];
    
    if (sdpType == RTCSdpTypeOffer) {
        // 设置给这个点对点连接
        __weak __typeof(connection) wConnection = connection;
        // B:设置连接对端 SDP
        [connection setRemoteDescription:remoteSdp completionHandler:^(NSError * _Nullable error) {
            if (error) {
                NSLog(@"setRemoteDescription error");
            }
            
            [self _didSetSessionDescription:wConnection];
        }];
    }
}


在接收到对端发送的’__offer'消息后, 本端需要做如下处理:

  • 解析接收到的消息结构,解析出当前的sdp类型及sdp信息,并构造RTCSessionDescription对象
  • 根据解析的connectId 获取到点对点连接RTCPeerConnection对象
  • 将接收到的对端sdp通过点对点连接对象的setRemoteDescription:设置为远端sdp
  • 设置成功后,本端通过信令服务器发送’__answer'消息来响应

answer

收到远端offer后主动发送answer

接上面:在收到对端’__offer‘消息后,先设置remote sdp, 接着主动发送’__answer'消息响应
_didSetSessionDescription:方法中的处理:

else if (connection.signalingState == RTCSignalingStateStable) { // 本地发送 answer
        if (_role == RoleCalled) {
            NSDictionary *dic = @{@"eventName": @"__answer",
                                  @"data": @{@"sdp": @{@"type": @"answer",
                                                       @"sdp": connection.localDescription.sdp
                                                       },
                                             @"socketId": connectionID
                                             }
                                  };
           
            RTMMessage *message = [RTMMessage createChannelMessageWithChannelId:self.channelId data:[dic mj_JSONString]];
            [_channel sendMessage:message completion:^(RTMSendChannelMessageStatusCode code) {}];
        }
    }

构造好约定的传输格式@{@"type": @"answer",@"sdp": connection.localDescription.sdp } 及信令eventName '__answer'后,通过信令服务器发出。

接收远端answer

/**
 远端发来 answer
 */
- (void)_receiveAnswer:(NSMutableDictionary *)resultDic
{
    NSDictionary *dataDic = resultDic[@"data"];
    NSDictionary *sdpDic = dataDic[@"sdp"];
    NSString *sdp = sdpDic[@"sdp"];
    NSString *type = sdpDic[@"type"];
    NSString *connectionID = dataDic[@"socketId"];
    RTCSdpType sdpType = [self _typeForString:type];
    RTCPeerConnection *connection = [_peerConnections objectForKey:connectionID];
    RTCSessionDescription *remoteSdp = [[RTCSessionDescription alloc] initWithType:sdpType sdp:sdp];
    
    if (sdpType == RTCSdpTypeAnswer) {
        __weak __typeof(connection) wConnection = connection;
        /** Apply the supplied RTCSessionDescription as the remote description. */
        // A:设置连接对端 SDP
        [connection setRemoteDescription:remoteSdp completionHandler:^(NSError * _Nullable error) {
            if (error) {
                NSLog(@"setRemoteDescription error");
            }
            
            [self _didSetSessionDescription:wConnection];
        }];
    }
}

接收到对端的’__answer’消息后,解析出消息的结构。 将对端的sdp信息设置为本端的remote sdp。 这时的代码处理如下:

if (connection.signalingState == RTCSignalingStateHaveRemoteOffer) { // 新人进入房间就调(远端发起 offer)
        /** Generate an SDP answer. */
        [connection answerForConstraints:[self _setupForOfferOrAnswerConstraint] completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) {
            if (error) {
                NSLog(@"answerForConstraints error");
                return;
            }
            
            if (sdp.type == RTCSdpTypeAnswer) {
                __weak __typeof(connection) wConnection = connection;
                // B:设置连接本端 SDP
                [connection setLocalDescription:sdp completionHandler:^(NSError * _Nullable error) {
                    if (error) {
                        NSLog(@"setLocalDescription error");
                        return;
                    }
         
                    [self _didSetSessionDescription:wConnection];
                }];
            }
        }];
    }

通过RTCPeerConnection对象的answerForConstraints: 生成响应端(本端)的sdp. 并通过 setLocalDescription:设置为本端的sdp

未完待续

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-01-14 02:05:58  更:2022-01-14 02:07:35 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 11:04:15-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码