WebRTC 实现P2P音视频通话——原生IOS端使用WebRTC实现一对一音视频通话

IOS端使用WebRTC实现一对一音视频通话

  • 前言
  • 环境
  • 一、环境配置
    • 搭建项目,配置权限,通过CocoaPods安装第三方库
  • 二、音视频通话的实现
    • 音视频通话实现主要分为两部分,信令客户端以及webrtc
  • 三、 效果

前言

WebRTC 实现P2P音视频通话——原生IOS端使用WebRTC实现一对一音视频通话将基于前两篇博客<<信令服务器>><>的基础环境之上实现IOS原生客户端P2P音视频通话。
WebRTC 实现P2P音视频通话——实现一对一音视频通话本文将记录获取摄像头,麦克风的音视频流->连接信令服务器 ->加入房间并创建PeerConnection配置stun/turn服务,设置回调,绑定流媒体 ->对端加入房间后创建offer/answer收集媒体信息,通过信令服务器转发给对端进行媒体协商(同时收集candidate并发送到turn服务进行连通性检测)->turn服务检查完成回调检查结果,将检查结果通过信令服务器转发给对对端 ->双方都收到检查结果,开始进行连通,传输音视频流 ->退出房间,释放资源。
废话不多说,实现过程都有注释,看代码

环境

1.开发环境:Mac,Xcode,
2.依赖环境:信令服务器,stun/trun P2P穿透和转发服务器(这两者需要自行搭建,可在内网,公网搭建),CocoaPods第三方依赖库管理工具
3.依赖库:Socket.IO(信令服务器交互socket),GoogleWebRTC(媒体协商,媒体流交换,渲染库),MBProgressHUD(提示库)

一、环境配置

搭建项目,配置权限,通过CocoaPods安装第三方库

1.新建项目

WebRTC 实现P2P音视频通话——原生IOS端使用WebRTC实现一对一音视频通话_第1张图片

2.配置摄像头,麦克风访问权限

WebRTC 实现P2P音视频通话——原生IOS端使用WebRTC实现一对一音视频通话_第2张图片

3.CocoaPods安装第三方依赖库

终端进入项目路径下,通过vi Podfile 新建Podfile文件,
在这里插入图片描述

进入文件后按 i 进行编辑模式,编辑Podfile文件,输入以下需要用到的依赖库

platform :ios, '11.0'

use_frameworks!

target 'WebRTC_IOS' do
        pod 'GoogleWebRTC'
        pod 'Socket.IO-Client-Swift', '~>13.3.0'
        pod 'MBProgressHUD'

end

编辑完成按ESC退出编辑,再按:wq退出并保存文件

执行pod install 安装依赖库前打开项目,设置Build Settings中的SWIFT_VIERSION为4.2(因为socket.io库是由swift编写的,所以需要配置一下swift的版本,不然安装时会遇到swift版本问题)

WebRTC 实现P2P音视频通话——原生IOS端使用WebRTC实现一对一音视频通话_第3张图片

WebRTC 实现P2P音视频通话——原生IOS端使用WebRTC实现一对一音视频通话_第4张图片

重新pod install安装,配置完成。打开项目敲代码

二、音视频通话的实现

音视频通话实现主要分为两部分,信令客户端以及webrtc

1.信令客户端SignalClient实现
单例创建SignalClient对象->传入信令服务器地址->建立连接,设置回调->加入,退出房间->交换信令。

//
//  SignalClient.m
//  WebRTC_IOS
//
//  Created by FF on 2022/3/14.
//

#import "SignalClient.h"
@import  SocketIO;
@implementation SignalClient{
    SocketManager *manager;
    SocketIOClient *socket;
}

static SignalClient *m_instance = NULL;
//单例创建SignalClient对象
+ (SignalClient *)getInstance
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        m_instance = [[SignalClient alloc]init];
    });
    return m_instance;;
}

//传入信令服务器地址,连接信令服务器,设置回调
- (void)createConnect:(NSString *)adder
{
    NSLog(@"the server adder is %@",adder);
    
    NSURL *url = [[NSURL alloc]initWithString:adder];
    /*
         log 是否打印日志
         forcePolling  是否强制使用轮询
         reconnectAttempts 重连次数,-1表示一直重连
         reconnectWait 重连间隔时间
         forceWebsockets 是否强制使用websocket
    */
    NSDictionary *configDic = @{
        @"log":@YES,
        @"forcePolling":@YES,
        @"forceWebsockets":@YES,
        @"reconnectAttempts":@(5),
        @"reconnectWait":@(1)
    };
    manager = [[SocketManager alloc]initWithSocketURL:url config:configDic];
//    socket = manager.defaultSocket;
    socket = [manager socketForNamespace:@"/"];
//    [socket connect];
    [socket on:@"connect" callback:^(NSArray * data, SocketAckEmitter * ack) {//连接服务器成功回调
        if (self.delegate && [self.delegate respondsToSelector:@selector(connected)]) {
            [self.delegate connected];
        }
        NSLog(@"socket connected");
    }];
    
    [socket on:@"error" callback:^(NSArray * data, SocketAckEmitter * ack) {//连接服务器失败回调
        NSLog(@"socket connect error");
        
    }];
    
    [socket on:@"reconnectAttempt" callback:^(NSArray * data, SocketAckEmitter * ack) {//重连服务器回调
        NSLog(@"socket reconnectAttempt");
        
    }];
    
    [socket on:@"joined" callback:^(NSArray * data, SocketAckEmitter * ack) {//成功加入房间回调
        NSString *room = [data objectAtIndex:0];
//        NSString *socketId = [data objectAtIndex:1];
        NSLog(@"join is room: %@",room);
        if (self.delegate && [self.delegate respondsToSelector:@selector(joined:)]) {
            [self.delegate joined:room];
        }
    }];
    
    [socket on:@"leaved" callback:^(NSArray * data, SocketAckEmitter * ack) {//成功退出房间回调
        
        NSLog(@"");
    }];
    
    [socket on:@"otherjoin" callback:^(NSArray * data, SocketAckEmitter * ack) {//其他用户加入房间后回调
        NSString *room = [data objectAtIndex:0];
        NSLog(@"otherjoin is room %@",room);
        
        if(self.delegate && [self.delegate respondsToSelector:@selector(otherjoin:User:)]){
            [self.delegate otherjoin:room User:@"nil"];
            
        }
    }];
    
    [socket on:@"full" callback:^(NSArray * data, SocketAckEmitter * ack) {//房间已满,加入房间失败回调
        NSString *room = [data objectAtIndex:0];
        NSLog(@"full is room %@",room);
        if(self.delegate && [self.delegate respondsToSelector:@selector(full:)]){
            [self.delegate full:room];
        }
        
    }];
    
    [socket on:@"bye" callback:^(NSArray * data, SocketAckEmitter * ack) {//收到其他用户退出的消息回调
        
    }];
    
    [socket on:@"message" callback:^(NSArray * data, SocketAckEmitter * ack) {//收到其他用户的消息,传回控制界面在处理
        NSString *room = [data objectAtIndex:0];
        NSDictionary *msg = [data objectAtIndex:1];
        
        NSString *type = msg[@"type"];
        if ([type isEqualToString:@"offer"]) {//收到其他用户的offer媒体信息
            if(self.delegate && [self.delegate respondsToSelector:@selector(offer:Message:)]){
                [self.delegate offer:room Message:msg];
            }
        }else if ([type isEqualToString:@"answer"]){//收到其他用户的answer媒体信息
            if(self.delegate && [self.delegate respondsToSelector:@selector(answer:Message:)]){
                [self.delegate answer:room Message:msg];
            }
        }else if ([type isEqualToString:@"candidate"]){//收到候选者的信息
            if(self.delegate && [self.delegate respondsToSelector:@selector(candidate:Message:)]){
                [self.delegate candidate:room Message:msg];
            }
        }else{
            NSLog(@"the msg is invalid!");
        }
    }];
    
    [socket connectWithTimeoutAfter:3 withHandler:^{//建立连接
        
    }];
}

- (void) joinRoom: (NSString *) room{ //发送加入房间消息
    if (socket) {
        if (socket.status == SocketIOStatusConnected) {
            NSLog(@"join room(%@)",room);
            [socket emit:@"join" with:@[room]];
        }
    }else{
        NSLog(@"error: socket is null");
    }

}
- (void) leaveRoom: (NSString *) room{ //发送退出房间消息
    
    if (socket){
        if (socket.status == SocketIOStatusConnected) {
            NSLog(@"leave room(%@)",room);
            [socket emit:@"leave" with:@[room]];
        }
    }else{
        NSLog(@"error: socket is null");
    }
    
}
- (void) sendMessage: (NSString *) room WithMsg:(NSDictionary *)msg{ //发送媒体协商信令相关消息
    
    if(socket){
        if(socket.status == SocketIOStatusConnected){
            if(msg){
                NSLog(@"sendMessage json:%@",msg);
                [socket emit:@"message" with:@[room,msg]];
            }else{
                NSLog(@"error: msg is null");
            }
        }else{
            NSLog(@"the socket has been disconnect!");
        }
    }else{
        NSLog(@"error: socket is null");
    }
}
@end

2.音视频的采集,传输,渲染
搭建webrtc工厂对象,配置本地音视频采集对象,创建本地,远端视频预览窗口->设置信令服务回调代理,加入房间成功回调, 配置ICEServer,创建PeerConnection->其他用户加入成功回调,收集candidate候选者,收集媒体信息,通过信令服务器交换candidate候选者及媒体信息,接收到消息后进行媒体信息协商->协商完成,PeerConnection回调远端视频流,添加到流媒体轨中,更新子视图布局layoutSubviews,就可以进行视频通话了。

//
//  CallViewController.m
//  WebRTC_IOS
//
//  Created by FF on 2022/3/14.
//

#import "CallViewController.h"
#import "SignalClient.h"

#import <WebRTC/WebRTC.h>
#import <MBProgressHUD/MBProgressHUD.h>
@interface CallViewController ()<SignalClientDelegate, RTCVideoViewDelegate,RTCPeerConnectionDelegate>{
    
    NSString *m_adder;
    NSString *m_room;
    
    NSString *myState;
    
    SignalClient *sigClient;
    
    RTCPeerConnectionFactory *factory;
    RTCCameraVideoCapturer *capturer;
    RTCPeerConnection *peerConnetion;
    
    
    RTCVideoTrack *videoTrack;
    RTCAudioTrack *audioTrack;
    
    RTCVideoTrack *remoteVideoTrack;
    CGSize remoteVideoSize;
    
    NSMutableArray *ICEServers;
    
}
@property (nonatomic, strong)RTCEAGLVideoView *remoteVideoView;//远端视频窗口
@property (nonatomic, strong)RTCCameraPreviewView *localVideoView;//本地视频窗口

@property (nonatomic, strong)UIButton *leaveBtn;//离开按钮

@end
static CGFloat const kLocalVideoViewSize = 120; //本地视频窗口的大小
static CGFloat const kLocalVideoViewPadding = 8; //本地视频窗口的边距

@implementation CallViewController
static NSString *const RTCSTUNServerURL = @"turn:111.67.203.111:3478";//配置自身的turnserver服务器IP都直接哦
static int logY = 0;
- (instancetype)initWithAdder:(NSString *)adder WithRoom:(NSString *)room
{
    if(self = [super init]){
        m_adder = adder;
        m_room = room;
    }
    return self;
}


- (void)viewDidLoad {
    [super viewDidLoad];
    logY = 0;
    myState = @"init";
    // Do any additional setup after loading the view.
    CGRect bounds = self.view.bounds;
    
    self.remoteVideoView = [[RTCEAGLVideoView alloc]initWithFrame:bounds];
    self.remoteVideoView.delegate = self;
    [self.view addSubview:self.remoteVideoView];
    
    self.localVideoView = [[RTCCameraPreviewView alloc]initWithFrame:CGRectZero];
    [self.view addSubview:self.localVideoView];
    
    CGRect localVideoViewFrame = CGRectMake(0, 0, kLocalVideoViewSize, kLocalVideoViewSize);
    localVideoViewFrame.origin.x = CGRectGetMaxX(bounds) - localVideoViewFrame.size.width-kLocalVideoViewPadding;
    localVideoViewFrame.origin.y = CGRectGetMaxY(bounds) - localVideoViewFrame.size.height - kLocalVideoViewPadding;
    self.localVideoView.frame = localVideoViewFrame;
    
    self.leaveBtn = [UIButton buttonWithType:UIButtonTypeCustom];
    [self.leaveBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    [self.leaveBtn setTintColor:[UIColor whiteColor]];
    [self.leaveBtn setTitle:@"leave" forState:UIControlStateNormal];
    [self.leaveBtn setBackgroundColor:[UIColor greenColor]];
    [self.leaveBtn setShowsTouchWhenHighlighted:YES];
    [self.leaveBtn.layer setCornerRadius:40];
    [self.leaveBtn.layer setBorderWidth:1];
    [self.leaveBtn setClipsToBounds:FALSE];
    self.leaveBtn.frame = CGRectMake(bounds.size.width/2-40, bounds.size.height/2-140, 80, 80);
    [self.leaveBtn addTarget:self action:@selector(leaveRoom:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:self.leaveBtn];
    
    //创建连接工厂
    [self createPeerConnectionFactory];
    
    //采集本地流媒体
    [self captureLocalMedia];
    
    //
    sigClient = [SignalClient getInstance];
    sigClient.delegate = self;
//    [[SignalClient getInstance] joinRoom:room];
    
}

- (void)leaveRoom:(UIButton *)button
{
    [self willMoveToParentViewController:nil];
    [self.view removeFromSuperview];
    [self removeFromParentViewController];
    
    if(!sigClient){
        sigClient = [SignalClient getInstance];
    }
    
    if([myState isEqualToString:@"leaved"]){
        [sigClient leaveRoom:m_room];
    }
    
    if(peerConnetion){//关闭webrtc资源
        [peerConnetion close];
        peerConnetion = nil;
    }
    
    NSLog(@"leave romm (%@)",m_room);
    [self addLogToScreen:@"leave romm (%@)",m_room];
}

- (void)addLogToScreen: (NSString *)format, ...{//将日志显示到屏幕上
    
    //可变参数va_list处理
    va_list paramList;
    va_start(paramList, format);
    NSString *log = [[NSString alloc]initWithFormat:format arguments:paramList];
    va_end(paramList);
    
    //显示日志
    CGRect labelFrame = CGRectMake(0, logY++ * 20, 500, 200);
    UILabel *logLabel = [[UILabel alloc]initWithFrame:labelFrame];
    logLabel.text = log;
    logLabel.textColor = [UIColor redColor];
    [self.view addSubview:logLabel];
    
}


#pragma  mark - WebRTC
//创建PeerConnection工厂
- (void)createPeerConnectionFactory
{
    //设置SSL传输
    //初始化工厂
    [RTCPeerConnectionFactory initialize];
    
    if(!factory){//点对点工厂为空
        RTCDefaultVideoDecoderFactory *decoderFactory = [[RTCDefaultVideoDecoderFactory alloc]init];
        RTCDefaultVideoEncoderFactory *encoderFactory = [[RTCDefaultVideoEncoderFactory alloc]init];
        NSArray *codecs = [encoderFactory supportedCodecs];
        [encoderFactory setPreferredCodec:codecs[2]];
        factory = [[RTCPeerConnectionFactory alloc]initWithEncoderFactory:encoderFactory decoderFactory:decoderFactory];
    }
    
}

//创建peerconnection的配置
- (RTCMediaConstraints *) defaultPeerConnContraints
{
    RTCMediaConstraints *mediaConstraints = [[RTCMediaConstraints alloc]initWithMandatoryConstraints:@{
        kRTCMediaConstraintsOfferToReceiveAudio:kRTCMediaConstraintsValueTrue,//开启接收音频
        kRTCMediaConstraintsOfferToReceiveVideo:kRTCMediaConstraintsValueTrue//开启接收视频
    } optionalConstraints:@{
        @"DtlsSrtpKeyAgreement":@"true"
    }];
    return mediaConstraints;
}

//初始化STUN Server (ICE Server)
- (RTCIceServer *)defaultSTUNServer{
    
    return [[RTCIceServer alloc]initWithURLStrings:@[RTCSTUNServerURL]
                                          username:@"flz"
                                        credential:@"123456"];
}

- (RTCPeerConnection *)createPeerConnection{
    
    //创建ICEServer 配置参数
    if(!ICEServers){
        ICEServers = [NSMutableArray array];
        [ICEServers addObject:[self defaultSTUNServer]];
    }
    
    //使用工厂创建连接
    RTCConfiguration *configuration = [[RTCConfiguration alloc]init];
    [configuration setIceServers:ICEServers];
    RTCPeerConnection *peerConn = [factory peerConnectionWithConfiguration:configuration
                                                                     constraints:[self
                                                                                  defaultPeerConnContraints] delegate:self];
    NSArray<NSString*> *mediaStreamLabels = @[@"ARDAMS"];
    [peerConn addTrack:videoTrack streamIds:mediaStreamLabels];
//    [peerConn addTrack:audioTrack streamIds:mediaStreamLabels];
    
    return peerConn;
    
}

- (void)captureLocalMedia
{
    
    NSDictionary *mandatoryConstraints = @{};
    RTCMediaConstraints *constraiots = [[RTCMediaConstraints alloc]initWithMandatoryConstraints:mandatoryConstraints optionalConstraints:nil];
    
    RTCAudioSource *audioSourec = [factory audioSourceWithConstraints:constraiots];
    //
    audioTrack = [factory audioTrackWithSource:audioSourec trackId:@"ADRAMSa0"];
    
    NSArray<AVCaptureDevice *> *captureDevices = [RTCCameraVideoCapturer captureDevices];
    AVCaptureDevicePosition position = AVCaptureDevicePositionFront;
    AVCaptureDevice *device = captureDevices[0];
    for (AVCaptureDevice *obj in captureDevices) {
        if(obj.position == position){
            device = obj;
            break;
        }
    }
    
    AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
    if(authStatus == AVAuthorizationStatusRestricted || authStatus == AVAuthorizationStatusDenied){
        NSLog(@"没有访问摄像头的权限");
        
    }
    
    if(device){
        RTCVideoSource *videoSource = [factory videoSource];
        capturer = [[RTCCameraVideoCapturer alloc]initWithDelegate:videoSource];
        AVCaptureDeviceFormat *format = [[RTCCameraVideoCapturer supportedFormatsForDevice:device] lastObject];
        CGFloat fps = [[format videoSupportedFrameRateRanges] firstObject].maxFrameRate;
        videoTrack = [factory videoTrackWithSource:videoSource trackId:@"ARDAMSv0"];
        self.localVideoView.captureSession = capturer.captureSession;
        [capturer startCaptureWithDevice:device
                                  format:format
                                     fps:fps];
        
    }
    
}

- (void)doStartCall{//开始进行媒体协商
    NSLog(@"Start Call, Wait ...");
    if(!peerConnetion){
        [self createPeerConnection];
    }
    [peerConnetion offerForConstraints:[self defaultPeerConnContraints] completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) {
        //创建offer完成回调
        if(error){//offer创建失败
            NSLog(@"Failed to create offer SDP, err=%@",error);
        }else{//offer创建成功
            __weak RTCPeerConnection *weakPeerConnection = self->peerConnetion;
            [self setLocalOffer:weakPeerConnection WithSdp:sdp];
        }
        
    }];
    
}

- (void)setLocalOffer:(RTCPeerConnection *)pc WithSdp:(RTCSessionDescription *)sdp
{//将媒体信息设置到PeerConnection连接中,并发送对端
    [pc setLocalDescription:sdp completionHandler:^(NSError * _Nullable error) {
        if(!error){
            NSLog(@"Successed to set Local ofeer sdp!");
        }else{
            NSLog(@"Failed to set Local ofeer sdp!,err=%@",error);
        }
    }];
    
    __weak NSString *weakRoom = m_room;
    NSDictionary *dict = [[NSDictionary alloc]initWithObjects:@[@"offer",sdp.sdp] forKeys:@[@"type",@"sdp"]];
    dispatch_async(dispatch_get_main_queue(), ^{
        [[SignalClient getInstance] sendMessage:weakRoom WithMsg:dict];
    });
    
}
//收到对方的offer媒体信息,创建自己answer媒体信息并发给对方
- (void) getAnswer:(RTCPeerConnection *)pc{
    
    [peerConnetion answerForConstraints:[self defaultPeerConnContraints] completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) {
        if(!error){
            NSLog(@"Success to create local answer sdp");
            __weak RTCPeerConnection *weakPeerConnection = self->peerConnetion;
            [self setLocalAnswer:weakPeerConnection WithSdp:sdp];
        }else{
            NSLog(@"Failed to create local answer sdp %@",error);
        }

    }];
}

- (void)setLocalAnswer:(RTCPeerConnection *)pc WithSdp:(RTCSessionDescription *)sdp
{//将媒体信息设置到PeerConnection连接中,并发送对端
    [pc setLocalDescription:sdp completionHandler:^(NSError * _Nullable error) {
        if(!error){
            NSLog(@"Successed to set Local ofeer sdp!");
        }else{
            NSLog(@"Failed to set Local ofeer sdp!,err=%@",error);
        }
    }];
    
    __weak NSString *weakRoom = m_room;
    NSDictionary *dict = [[NSDictionary alloc]initWithObjects:@[@"answer",sdp.sdp] forKeys:@[@"type",@"sdp"]];
    dispatch_async(dispatch_get_main_queue(), ^{
        [[SignalClient getInstance]sendMessage:weakRoom WithMsg:dict];
    });
    
}

#pragma  mark -SignalClientDelegate
- (void)answer:(nonnull NSString *)room Message:(nonnull NSDictionary *)dict {
    NSLog(@"have received a answer message %@",dict);
    NSString *sdp = dict[@"sdp"];
    RTCSessionDescription *remoteSdpDesc = [[RTCSessionDescription alloc]initWithType:RTCSdpTypeAnswer sdp:sdp];
    [peerConnetion setRemoteDescription:remoteSdpDesc completionHandler:^(NSError * _Nullable error) {
        if(!error){
            NSLog(@"Success to set remote Answer Sdp");
        }else{
            NSLog(@"Failed to set remote Answer Sdp, err=%@",error);
        }
    }];
}

- (void)byeFrom:(nonnull NSString *)room User:(nonnull NSString *)uid {
    NSLog(@"the user(%@) has leaved from room(%@) notify!", uid, room);
    [self addLogToScreen:@"the user(%@) has leaved from room(%@) notify!", uid, room];
    
    myState = @"joined_unbind";
    if(peerConnetion){
        [peerConnetion close];
        peerConnetion = nil;
    }
}

- (void)candidate:(nonnull NSString *)room Message:(nonnull NSDictionary *)dict {
    NSLog(@"have received a message %@",dict);
    
    NSString *candidateSdp = dict[@"candidate"];
    int sdpMLineIndex = [dict[@"label"] intValue];
    NSString *sdpMid = dict[@"id"];
    
    RTCIceCandidate *candidate = [[RTCIceCandidate alloc]initWithSdp:candidateSdp sdpMLineIndex:sdpMLineIndex sdpMid:sdpMid];
    [peerConnetion addIceCandidate:candidate];
    
}

- (void)connect_error {
    [self addLogToScreen: @"socket connect_error!"];
}

- (void)connect_timeout {
    [self addLogToScreen: @"socket connect_timeout!"];
}

- (void)connected {
    [[SignalClient getInstance] joinRoom:m_room];
    [self addLogToScreen: @"socket connect success!"];
    [self addLogToScreen: @"joinRoom: %@", m_room];
}

- (void)full:(nonnull NSString *)room {
    NSLog(@"the room(%@) is full notify!",room);
    [self addLogToScreen:@"the room(%@) is full notify!",room];
    
    if(peerConnetion){
        [peerConnetion close];
        peerConnetion = nil;
    }
    myState = @"leaved";
    
    
    MBProgressHUD *hub = [[MBProgressHUD alloc]initWithView:self.view];
    [hub setRemoveFromSuperViewOnHide:YES];
    hub.label.text = @"房间满了";
    UIView *view = [[UIView alloc]initWithFrame:CGRectMake(0, 0, 50, 50)];
    [hub setCustomView:view];
    [hub setMode:MBProgressHUDModeCustomView];
    [self.view addSubview:hub];
    [hub showAnimated:YES];
    [hub hideAnimated:YES afterDelay:1.0];
    
    if(capturer){
        [capturer stopCapture];
        capturer = nil;
    }
    
    if(factory){
        factory = nil;
    }
    
}

//成功加入房间 创建peerconnection 连接
- (void)joined:(nonnull NSString *)room {
    
    NSLog(@"joined room(%@) notify",m_room);
    [self addLogToScreen:@"joined room(%@) notify",m_room];
    
    myState = @"joined";
    
    //创建peerconnection
    if(!peerConnetion)
        peerConnetion = [self createPeerConnection];
    
}

- (void)leaved:(nonnull NSString *)room {
    NSLog(@"leaved room (%@) notify",room);
    [self addLogToScreen:@"leaved room (%@) notify",room];
}

- (void)offer:(nonnull NSString *)room Message:(nonnull NSDictionary *)dict {
    NSLog(@"have received a offer message %@",dict);
    
    NSString *offerSdp = dict[@"sdp"];
    RTCSessionDescription *localOfferDesc = [[RTCSessionDescription alloc]initWithType:RTCSdpTypeOffer sdp:offerSdp];
    if(!peerConnetion){//连接为空的时,需要在创建
        [self createPeerConnection];
    }
    __weak RTCPeerConnection *weakPeerConnection = peerConnetion;
    [peerConnetion setLocalDescription:localOfferDesc completionHandler:^(NSError * _Nullable error) {
        if(!error){
            [self getAnswer:weakPeerConnection];
            NSLog(@"Success to set local Offer Spd");
        }else{
            NSLog(@"Failed to set local Offer Spd! err=%@",error);
        }
    }];
    
}

- (void)otherjoin:(nonnull NSString *)room User:(nonnull NSString *)uid {
    NSLog(@"other user (%@) has been joined into room(%@) notify",uid, room);
    [self addLogToScreen:@"other user (%@) has been joined into room(%@) notify",uid, room];
    //创建peerconnection
    if([myState isEqualToString:@"joined_unbind"]){
        if(!peerConnetion)
            peerConnetion = [self createPeerConnection];
    }
    
    myState = @"joined_conn";
    //调用call 进行媒体协商
    [self doStartCall];
    
}

- (void)reconnectAttempt {
    [self addLogToScreen: @"socket reconnectAttempt!"];
}

#pragma RTCPeerConnectionDelegate
- (void)encodeWithCoder:(nonnull NSCoder *)coder {
    NSLog(@"%s",__func__);
}

- (void)traitCollectionDidChange:(nullable UITraitCollection *)previousTraitCollection {
    NSLog(@"%s",__func__);
}

- (void)preferredContentSizeDidChangeForChildContentContainer:(nonnull id<UIContentContainer>)container {
    NSLog(@"%s",__func__);
}

- (void)systemLayoutFittingSizeDidChangeForChildContentContainer:(nonnull id<UIContentContainer>)container {
    NSLog(@"%s",__func__);
}

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(nonnull id<UIViewControllerTransitionCoordinator>)coordinator {
    NSLog(@"%s",__func__);
}



- (void)peerConnection:(nonnull RTCPeerConnection *)peerConnection didAddStream:(nonnull RTCMediaStream *)stream {
    NSLog(@"%s",__func__);
}

- (void)peerConnection:(nonnull RTCPeerConnection *)peerConnection didChangeIceConnectionState:(RTCIceConnectionState)newState {
    NSLog(@"%s",__func__);
}

- (void)peerConnection:(nonnull RTCPeerConnection *)peerConnection didChangeSignalingState:(RTCSignalingState)stateChanged {
    NSLog(@"%s",__func__);
}

- (void)peerConnection:(nonnull RTCPeerConnection *)peerConnection didGenerateIceCandidate:(nonnull RTCIceCandidate *)candidate {
    NSLog(@"%s",__func__);
    
    __weak NSString *weakRoom = m_room;
    dispatch_async(dispatch_get_main_queue(), ^{
        NSDictionary *dict = [[NSDictionary alloc]initWithObjects:@[@"candidate",
                                                                  [NSString stringWithFormat:@"%d",candidate.sdpMLineIndex],
                                                                  candidate.sdpMid,
                                                                  candidate.sdp]
                                                          forKeys:@[@"type",@"label",@"id",@"candidate"]];
        [[SignalClient getInstance] sendMessage:weakRoom WithMsg:dict];
    });
    
}

- (void)peerConnection:(nonnull RTCPeerConnection *)peerConnection didOpenDataChannel:(nonnull RTCDataChannel *)dataChannel {
    NSLog(@"%s",__func__);
}

- (void)peerConnection:(nonnull RTCPeerConnection *)peerConnection didRemoveIceCandidates:(nonnull NSArray<RTCIceCandidate *> *)candidates {
    
}
- (void)peerConnection:(RTC_OBJC_TYPE(RTCPeerConnection) *)peerConnection
        didAddReceiver:(RTC_OBJC_TYPE(RTCRtpReceiver) *)rtpReceiver
               streams:(NSArray<RTC_OBJC_TYPE(RTCMediaStream) *> *)mediaStreams
{
    NSLog(@"%s",__func__);
    RTCMediaStreamTrack *streamTrack = rtpReceiver.track;
    if([streamTrack.kind isEqualToString:kRTCMediaStreamTrackKindVideo]){
        
        if(!self.remoteVideoView){
            NSLog(@"error:remotrVideoView have not been created");
            return;
        }
        remoteVideoTrack = (RTCVideoTrack *)streamTrack;
        [remoteVideoTrack addRenderer:self.remoteVideoView];
    }
    
}


/** Called when negotiation is needed, for example ICE has restarted. */
- (void)peerConnectionShouldNegotiate:(RTC_OBJC_TYPE(RTCPeerConnection) *)peerConnection
{
    NSLog(@"%s",__func__);
}

/** Called any time the IceGatheringState changes. */
- (void)peerConnection:(RTC_OBJC_TYPE(RTCPeerConnection) *)peerConnection
didChangeIceGatheringState:(RTCIceGatheringState)newState
{
    NSLog(@"%s",__func__);
    
}

#pragma mark - RTCEAGLVideoViewDelegate
- (void)videoView:(RTCEAGLVideoView*)videoView didChangeVideoSize:(CGSize)size {
    if (videoView == self.remoteVideoView) {
        remoteVideoSize = size;
    }
    [self layoutSubviews];
}

- (void)layoutSubviews {
    CGRect bounds = self.view.bounds;
    if (remoteVideoSize.width > 0 && remoteVideoSize.height > 0) {
        // Aspect fill remote video into bounds.
        CGRect remoteVideoFrame =
        AVMakeRectWithAspectRatioInsideRect(remoteVideoSize, bounds);
        CGFloat scale = 1;
        if (remoteVideoFrame.size.width > remoteVideoFrame.size.height) {
            // Scale by height.
            scale = bounds.size.height / remoteVideoFrame.size.height;
        } else {
            // Scale by width.
            scale = bounds.size.width / remoteVideoFrame.size.width;
        }
        remoteVideoFrame.size.height *= scale;
        remoteVideoFrame.size.width *= scale;
        self.remoteVideoView.frame = remoteVideoFrame;
        self.remoteVideoView.center =
        CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
    } else {
        self.remoteVideoView.frame = bounds;
    }

}
@end

编译时将会遇到"clang: error: linker command failed with exit code 1 (use -v to see invocation)" 问题

WebRTC 实现P2P音视频通话——原生IOS端使用WebRTC实现一对一音视频通话_第5张图片

这是因为webrtc库不能使用Enable Bitcode,将其设为NO

请添加图片描述

三、 效果

真机运行才能跑起来

WebRTC 实现P2P音视频通话——原生IOS端使用WebRTC实现一对一音视频通话_第6张图片

以上使用信令服务器进行管理房间以及媒体信息的交换, 使用stun/trun服务进行穿越检测,连通,转发,最后使用WebRTC实现音视频采集,媒体协商并传输,这样一个非常简单的IOS端与浏览器的一对一音视频通话就实现了。

你可能感兴趣的:(音视频,ios,webrtc,p2p,objective-c)