前言
WebRTC 实现P2P音视频通话——原生IOS端使用WebRTC实现一对一音视频通话将基于前两篇博客<<信令服务器>><
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.新建项目
2.配置摄像头,麦克风访问权限
3.CocoaPods安装第三方依赖库
platform :ios, '11.0'
use_frameworks!
target 'WebRTC_IOS' do
pod 'GoogleWebRTC'
pod 'Socket.IO-Client-Swift', '~>13.3.0'
pod 'MBProgressHUD'
end
执行pod install 安装依赖库前打开项目,设置Build Settings中的SWIFT_VIERSION为4.2(因为socket.io库是由swift编写的,所以需要配置一下swift的版本,不然安装时会遇到swift版本问题)
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
#import
@interface CallViewController ()
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
[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
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
NSLog(@"%s",__func__);
}
- (void)systemLayoutFittingSizeDidChangeForChildContentContainer:(nonnull id
NSLog(@"%s",__func__);
}
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(nonnull id
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
}
- (void)peerConnection:(RTC_OBJC_TYPE(RTCPeerConnection) *)peerConnection
didAddReceiver:(RTC_OBJC_TYPE(RTCRtpReceiver) *)rtpReceiver
streams:(NSArray
{
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库不能使用Enable Bitcode,将其设为NO
以上使用信令服务器进行管理房间以及媒体信息的交换, 使用stun/trun服务进行穿越检测,连通,转发,最后使用WebRTC实现音视频采集,媒体协商并传输,这样一个非常简单的IOS端与浏览器的一对一音视频通话就实现了。
参考文章:WebRTC 实现P2P音视频通话——原生IOS端使用WebRTC实现一对一音视频通话_F小志的博客-CSDN博客_ios webrtc