1.podfile 文件
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'
target 'iosWebRTCDemo' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
pod 'GoogleWebRTC','1.1.24595'
pod 'ReactiveObjC'
pod 'SocketRocket','0.5.1'
pod 'HGAlertViewController', '~> 1.0.1'
target 'iosWebRTCDemoTests' do
inherit! :search_paths
# Pods for testing
end
target 'iosWebRTCDemoUITests' do
# Pods for testing
end
end
[[WebRTCHelper shareInstance] connectServer:@"172.27.104.54" port:@"9090/SocketServer"];
- WebRTCHelper.h
//
// WebRTCHelper.h
// WebRTC_new
//
// Created by
// Copyright © 2020年
//
#import
#import
#import
#import
#import
//@import SocketIO;
typedef enum : NSUInteger {
WebSocketConnectSuccess = 0,
WebSocketConnectField,
WebSocketConnectClosed,
} WebSocketConnectState;
@protocol WebRTCHelperDelegate;
@protocol WebRTCHelperFrindDelegate;
@interface WebRTCHelper : NSObject
/**
* 单例
*/
+(instancetype)shareInstance;
/*注释*/
@property (nonatomic,weak) id delegate;
/*注释*/
@property (nonatomic,weak) id friendDelegate;
/**
* 与服务器建立连接
* @param server 服务器地址
* @param port 端口号
*/
-(void)connectServer:(NSString *)server port:(NSString *)port;
/**
* 切换摄像头
*/
- (void)swichCamera:(BOOL)_usingFrontCamera;
/**
* 是否显示本地视频
*/
- (void)showLocaolCamera:(BOOL)_usingCamera;
/**
* 退出房间
*/
-(void)exitRoom;
@end
@protocol WebRTCHelperDelegate
@optional
/**
* 获取到发送信令消息
* @param webRTCHelper 本类
* @param message 消息内容
*/
-(void)webRTCHelper:(WebRTCHelper *)webRTCHelper receiveMessage:(NSString *)message;
/**
* 获取本地的localVideoStream数据
* @param webRTCHelper 本类
* @param steam 视频流
* @param userId 用户标识
*/
-(void)webRTCHelper:(WebRTCHelper *)webRTCHelper setLocalStream:(RTCMediaStream *)steam userId:(NSString *)userId;
/**
* 获取远程的remoteVideoStream数据
* @param webRTCHelper 本类
* @param stream 视频流
* @param userId 用户标识
*/
-(void)webRTCHelper:(WebRTCHelper *)webRTCHelper addRemoteStream:(RTCMediaStream *)stream userId:(NSString *)userId;
/**
* 某个用户退出后,关闭用户的连接
* @param webRTCHelper 本类
* @param userId 用户标识
*/
- (void)webRTCHelper:(WebRTCHelper *)webRTCHelper closeWithUserId:(NSString *)userId;
/**
* 获取socket连接状态
* @param webRTCHelper 本类
* @param connectState 连接状态,分为
WebSocketConnectSuccess 成功,
WebSocketConnectField, 失败
WebSocketConnectClosed 关闭
*/
-(void)webRTCHelper:(WebRTCHelper *)webRTCHelper socketConnectState:(WebSocketConnectState)connectState;
-(void)webRTCHelper:(WebRTCHelper *)webRTCHelper capturerSession:(AVCaptureSession *)captureSession;
@end
@protocol WebRTCHelperFrindDelegate
@optional
/**
* 获取房间内所有的用户(除了自己)
* @param friendList 用户列表
*/
-(void)webRTCHelper:(WebRTCHelper *)webRTCHelper gotFriendList:(NSArray *)friendList;
/**
* 获取新加入的用户信息
* @param friendId 新用户的id
*/
-(void)webRTCHelper:(WebRTCHelper *)webRTCHelper gotNewFriend:(NSString *)friendId;
/**
* 获取离开房间用户的信息
* @param friendId 离开用户的ID
*/
-(void)webRTCHelper:(WebRTCHelper *)webRTCHelper removeFriend:(NSString *)friendId;
@end
3 .WebRTCHelper.m
//
// WebRTCHelper.m
// WebRTC_new
//
// Created by
// Copyright © 2020年
//
#import "WebRTCHelper.h"
#define kAPPID @"1234567890abcdefg"
#define kDeviceUUID [[[UIDevice currentDevice] identifierForVendor] UUIDString]
//google提供的
static NSString *const RTCSTUNServerURL = @"stun:stun.l.google.com:19302";
//static NSString *const RTCSTUNServerURL2 = @"stun:23.21.150.121";
static NSString *const RTCSTUNServerURL2 =@"stun:global.stun.twilio.com:3478?transport=udp";
@interface WebRTCHelper()
{
SRWebSocket *_socket;
NSString *_server;
RTCPeerConnectionFactory *_factory;
RTCMediaStream *_localStream;
NSString *_myId;
NSMutableDictionary *_connectionDic; // 存放 所有用户 对应的链接集合
NSMutableArray *_connectionIdArray; // 存放 房间人数的 id
NSMutableArray *ICEServers;
//是否显示我的视频流(默认为yes,显示;no为不显示)
BOOL _usingCamera;
RTCCameraVideoCapturer * _capture;
}
@end
@implementation WebRTCHelper
static WebRTCHelper * instance = nil;
+(instancetype)shareInstance{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[[self class] alloc] init];
[instance initData];
});
return instance;
}
-(void)initData{
_connectionDic = [NSMutableDictionary dictionary];
_connectionIdArray = [NSMutableArray array];
_usingCamera = YES;
}
#pragma mark -提供给外部的方法
/**
* 与服务器进行连接
*/
- (void)connectServer:(NSString *)server port:(NSString *)port{
_server = server;
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"ws://%@:%@",server,port]] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:20];
_socket = [[SRWebSocket alloc] initWithURLRequest:request];
_socket.delegate = self;
[_socket open];
}
/**
* 退出房间
*/
- (void)exitRoom
{
_localStream = nil;
[_capture stopCapture];
_capture = nil;
NSArray *arr =_connectionIdArray.copy;
[arr enumerateObjectsUsingBlock:^(NSString *obj, NSUInteger idx, BOOL * _Nonnull stop) {
[self closePeerConnection:obj];
}];
[_socket close];
}
/**
* 切换摄像头
*/
- (void)swichCamera:(BOOL)_usingFrontCamera{
[self switchFrontBackCamera:_usingFrontCamera];
}
/**
* 是否显示本地摄像头
*/
- (void)showLocaolCamera:(BOOL)_usingCamera{
_usingCamera = _usingCamera;
//如果为空,则创建点对点工厂
if (!_factory)
{
//设置SSL传输
[RTCPeerConnectionFactory initialize];
[self create_factory];
}
//如果本地视频流为空
if (!_localStream)
{
//创建本地流
[self createLocalStream];
}
// [_capture stopCapture];
//创建连接
[self createPeerConnections];
//添加
[self addStreams];
[self createOffers];
}
#pragma mark -内部方法
/**
* 关闭peerConnection
*
* @param connectionId <#connectionId description#>
*/
- (void)closePeerConnection:(NSString *)connectionId
{
RTCPeerConnection *peerConnection = [_connectionDic objectForKey:connectionId];
if (peerConnection)
{
[peerConnection close];
}
[_connectionIdArray removeObject:connectionId];
[_connectionDic removeObjectForKey:connectionId];
dispatch_async(dispatch_get_main_queue(), ^{
if ([self->_delegate respondsToSelector:@selector(webRTCHelper:closeWithUserId:)])
{
[self->_delegate webRTCHelper:self closeWithUserId:connectionId];
}
});
}
/**
* 创建点对点连接
*
* @param connectionId connectionId description
*
* @return <#return value description#>
*/
- (RTCPeerConnection *)createPeerConnection:(NSString *)connectionId
{
//如果点对点工厂为空
if (!_factory)
{
[self create_factory];
}
//得到ICEServer
if (!ICEServers) {
ICEServers = [NSMutableArray array];
[ICEServers addObject:[self defaultSTUNServer]];
}
//用工厂来创建连接
RTCConfiguration *configuration = [[RTCConfiguration alloc] init];
configuration.iceServers = ICEServers;
RTCPeerConnection *connection = [_factory peerConnectionWithConfiguration:configuration constraints:[self creatPeerConnectionConstraint] delegate:self];
return connection;
}
- (RTCMediaConstraints *)creatPeerConnectionConstraint
{
RTCMediaConstraints *constraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:@{kRTCMediaConstraintsOfferToReceiveAudio:kRTCMediaConstraintsValueTrue,kRTCMediaConstraintsOfferToReceiveVideo:kRTCMediaConstraintsValueTrue} optionalConstraints:@{@"DtlsSrtpKeyAgreement":@"true"}];
return constraints;
}
//初始化STUN Server (ICE Server)
- (RTCIceServer *)defaultSTUNServer{
return [[RTCIceServer alloc] initWithURLStrings:@[RTCSTUNServerURL,RTCSTUNServerURL2]];
}
/**
* 为所有连接添加流
*/
- (void)addStreams
{
//给每一个点对点连接,都加上本地流
NSDictionary *dic = [_connectionDic copy];
[dic enumerateKeysAndObjectsUsingBlock:^(NSString *key, RTCPeerConnection *obj, BOOL * _Nonnull stop) {
if (!self->_localStream)
{
[self createLocalStream];
}
[obj addStream:self->_localStream];
}];
}
/**
* 创建所有连接
*/
- (void)createPeerConnections
{
//从我们的连接数组里快速遍历
NSArray *arr = _connectionIdArray.copy;
[arr enumerateObjectsUsingBlock:^(NSString *obj, NSUInteger idx, BOOL * _Nonnull stop) {
//根据连接ID去初始化 RTCPeerConnection 连接对象
RTCPeerConnection *connection = [self createPeerConnection:obj];
//设置这个ID对应的 RTCPeerConnection对象
[self->_connectionDic setObject:connection forKey:obj];
}];
}
/**
* 创建本地视频流
*/
-(void)createLocalStream{
_localStream = [_factory mediaStreamWithStreamId:@"ARDAMS"];
//音频
RTCAudioTrack * audioTrack = [_factory audioTrackWithTrackId:@"ARDAMSa0"];
[_localStream addAudioTrack:audioTrack];
NSArray *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 ([_delegate respondsToSelector:@selector(webRTCHelper:setLocalStream:userId:)])
{
[_delegate webRTCHelper:self setLocalStream:nil userId:_myId];
}
}
else
{
if (device)
{
RTCVideoSource *videoSource = [_factory videoSource];
[videoSource adaptOutputFormatToWidth:640 height:480 fps:20];
_capture = [[RTCCameraVideoCapturer alloc] initWithDelegate:videoSource];
// AVCaptureDeviceFormat * format = [[RTCCameraVideoCapturer supportedFormatsForDevice:device] lastObject];
AVCaptureDeviceFormat* format = device.activeFormat;
RTCVideoTrack *videoTrack = [_factory videoTrackWithSource:videoSource trackId:@"ARDAMSv0"];
__weak RTCCameraVideoCapturer *weakCapture = _capture;
[_localStream addVideoTrack:videoTrack];
if ([self->_delegate respondsToSelector:@selector(webRTCHelper:capturerSession:)])
{
[self->_delegate webRTCHelper:self capturerSession:weakCapture.captureSession];
}
[weakCapture stopCapture];
[weakCapture startCaptureWithDevice:device format:format fps:20 completionHandler:^(NSError * error) {
NSLog(@"11111111");
}];
}
else
{
NSLog(@"该设备不能打开摄像头");
if ([_delegate respondsToSelector:@selector(webRTCHelper:setLocalStream:userId:)])
{
[_delegate webRTCHelper:self setLocalStream:nil userId:_myId];
}
}
}
}
// 切换摄像头
-(void)switchFrontBackCamera:(BOOL)usingFrontCamera
{
if (_localStream == nil)
return;
NSArray *captureDevices = [RTCCameraVideoCapturer captureDevices];
AVCaptureDevicePosition position = usingFrontCamera ? AVCaptureDevicePositionFront : AVCaptureDevicePositionBack;
AVCaptureDevice * device = nil;
for (AVCaptureDevice *obj in captureDevices) {
if (obj.position == position) {
device = obj;
break;
}
}
// AVCaptureDeviceFormat * format = [[RTCCameraVideoCapturer supportedFormatsForDevice:device] lastObject];
AVCaptureDeviceFormat* format = device.activeFormat;
[_capture stopCapture];
[_capture startCaptureWithDevice:device format:format fps:15 completionHandler:^(NSError * _Nonnull error)
{
}];
}
/**
* 视频的相关约束
*/
- (RTCMediaConstraints *)localVideoConstraints
{
NSDictionary *mandatory = @{kRTCMediaConstraintsMaxWidth:@640,kRTCMediaConstraintsMinWidth:@640,kRTCMediaConstraintsMaxHeight:@480,kRTCMediaConstraintsMinHeight:@480,kRTCMediaConstraintsMinFrameRate:@15};
RTCMediaConstraints *constraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatory optionalConstraints:@{@"DtlsSrtpKeyAgreement":@"true"}];
return constraints;
}
/**
* 创建offer
*/
-(void)createOffer:(RTCPeerConnection *)peerConnection{
if (peerConnection == nil) {
peerConnection = [self createPeerConnection:nil];
}
[peerConnection offerForConstraints:[self offerOranswerConstraint] completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) {
if (error == nil) {
// RTCSessionDescription *newSdp = [self descriptionForDescription:sdp preferredVideoCodec:@"H264"];
__weak RTCPeerConnection * weakPeerConnction = peerConnection;
[peerConnection setLocalDescription:sdp completionHandler:^(NSError * _Nullable error) {
if (error == nil) {
[self setSessionDescriptionWithPeerConnection:weakPeerConnction];
}
}];
}
}];
}
/**
* 为所有连接创建offer
*/
- (void)createOffers
{
//给每一个点对点连接,都去创建offer
NSDictionary *dic = [_connectionDic copy];
[dic enumerateKeysAndObjectsUsingBlock:^(NSString *key, RTCPeerConnection *obj, BOOL * _Nonnull stop) {
[self createOffer:obj];
}];
}
/**
* 设置offer/answer的约束d
*/
- (RTCMediaConstraints *)offerOranswerConstraint
{
NSMutableDictionary * dic = [@{kRTCMediaConstraintsOfferToReceiveAudio:kRTCMediaConstraintsValueTrue,kRTCMediaConstraintsOfferToReceiveVideo:kRTCMediaConstraintsValueTrue} mutableCopy];
[dic setObject:(_usingCamera ? kRTCMediaConstraintsValueTrue : kRTCMediaConstraintsValueFalse) forKey:kRTCMediaConstraintsOfferToReceiveVideo];
RTCMediaConstraints *constraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:dic optionalConstraints:@{@"DtlsSrtpKeyAgreement":@"true"}];
return constraints;
}
// Called when setting a local or remote description.
//当一个远程或者本地的SDP被设置就会调用
- (void)setSessionDescriptionWithPeerConnection:(RTCPeerConnection *)peerConnection
{
NSLog(@"%s",__func__);
NSString *currentId = [self getKeyFromConnectionDic:peerConnection];
if (currentId == nil)
{
NSLog(@"找不到用户");
return;
}
//判断,当前连接状态为,收到了远程点发来的offer,这个是进入房间的时候,尚且没人,来人就调到这里
if (peerConnection.signalingState == RTCSignalingStateHaveRemoteOffer)
{
//创建一个answer,会把自己的SDP信息返回出去
[peerConnection answerForConstraints:[self offerOranswerConstraint] completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) {
__weak RTCPeerConnection *obj = peerConnection;
[peerConnection setLocalDescription:sdp completionHandler:^(NSError * _Nullable error) {
[self setSessionDescriptionWithPeerConnection:obj];
}];
}];
}
//判断连接状态为本地发送offer
else if (peerConnection.signalingState == RTCSignalingStateHaveLocalOffer)
{
if (peerConnection.localDescription.type == RTCSdpTypeAnswer)
{
NSDictionary *dic = @{@"event": @"answer", @"data": @{@"sdp": peerConnection.localDescription.sdp}, @"sender": _myId, @"receiver":currentId};
NSData *data = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[_socket send:jsonString];
}
//发送者,发送自己的offer
else if(peerConnection.localDescription.type == RTCSdpTypeOffer)
{
NSDictionary *dic = @{@"event": @"offer", @"data": @{@"sdp": peerConnection.localDescription.sdp}, @"sender": _myId, @"receiver":currentId};
NSData *data = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[_socket send:jsonString];
}
}
else if (peerConnection.signalingState == RTCSignalingStateStable)
{
if (peerConnection.localDescription.type == RTCSdpTypeAnswer)
{
NSDictionary *dic = @{@"event": @"answer", @"data": @{@"sdp": peerConnection.localDescription.sdp}, @"sender": _myId, @"receiver":currentId};
NSData *data = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[_socket send:jsonString];
}
}
}
#pragma mark RTCPeerConnectionDelegate
/**获取远程视频流*/
- (void)peerConnection:( RTCPeerConnection *)peerConnection didAddStream:( RTCMediaStream *)stream {
NSString * userId = [self getKeyFromConnectionDic:peerConnection];
dispatch_async(dispatch_get_main_queue(), ^{
if ([self->_delegate respondsToSelector:@selector(webRTCHelper:addRemoteStream:userId:)]) {
[self->_delegate webRTCHelper:self addRemoteStream:stream userId:userId];
}
});
}
/**RTCIceConnectionState 状态变化*/
- (void)peerConnection:(nonnull RTCPeerConnection *)peerConnection didChangeIceConnectionState:(RTCIceConnectionState)newState {
NSLog(@"%s",__func__);
NSLog(@"newState=====%ld",(long)newState);
NSString * connectId = [self getKeyFromConnectionDic:peerConnection];
if (newState == RTCIceConnectionStateDisconnected) {
// [self createOffer:peerConnection];
//断开connection的连接
// dispatch_async(dispatch_get_main_queue(), ^{
// if ([self->_delegate respondsToSelector:@selector(webRTCHelper:closeWithUserId:)]) {
// [self->_delegate webRTCHelper:self closeWithUserId:connectId];
// }
// [self closePeerConnection:connectId];
// });
}
}
//[candidate.sdpMid isEqualToString:@"audio"]?@"audio":@"video"
/**获取到新的candidate*/
- (void)peerConnection:(RTCPeerConnection *)peerConnection didGenerateIceCandidate:(RTCIceCandidate *)candidate{
NSLog(@"+++%s",__func__);
NSString *currentId = [self getKeyFromConnectionDic: peerConnection];
NSDictionary *dic = @{
@"event": @"_ice_candidate",
@"data":@{
@"candidate":@{
@"sdpMid":candidate.sdpMid ,
@"sdpMLineIndex":[NSNumber numberWithInteger:candidate.sdpMLineIndex],
@"candidate": candidate.sdp
}
} ,
@"sender":_myId ,
@"receiver":currentId
};
NSLog(@"%d-------%@",candidate.sdpMLineIndex,candidate.sdpMid);
NSData *data = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[_socket send:jsonString];
}
/**删除某个视频流*/
- (void)peerConnection:(nonnull RTCPeerConnection *)peerConnection didRemoveStream:(nonnull RTCMediaStream *)stream {
NSLog(@"%s",__func__);
}
- (void)peerConnectionShouldNegotiate:(RTCPeerConnection *)peerConnection{
NSLog(@"%s,line = %d object = %@",__FUNCTION__,__LINE__,peerConnection);
}
- (void)peerConnection:(nonnull RTCPeerConnection *)peerConnection didRemoveIceCandidates:(nonnull NSArray *)candidates {
NSLog(@"%s,line = %d object = %@",__FUNCTION__,__LINE__,candidates);
}
- (void)peerConnection:(RTCPeerConnection *)peerConnection didChangeSignalingState:(RTCSignalingState)stateChanged{
NSString *currentId = [self getKeyFromConnectionDic: peerConnection];
NSLog(@"stateChanged = %ld=======currentId=====%@",(long)stateChanged,currentId);
}
- (void)peerConnection:(RTCPeerConnection *)peerConnection didChangeIceGatheringState:(RTCIceGatheringState)newState{
NSLog(@"newState = %ld",newState);
}
#pragma mark -消息相关
-(void)peerConnection:(RTCPeerConnection *)peerConnection didOpenDataChannel:(RTCDataChannel *)dataChannel{
}
#pragma mark -视频分辨率代理
- (void)capturer:(nonnull RTCVideoCapturer *)capturer didCaptureVideoFrame:(nonnull RTCVideoFrame *)frame {
}
-(void)create_factory
{
if(!_factory)
{
RTCInitializeSSL();
RTCDefaultVideoEncoderFactory *encodeFac = [[RTCDefaultVideoEncoderFactory alloc]init];
RTCDefaultVideoDecoderFactory *decodeFac = [[RTCDefaultVideoDecoderFactory alloc]init];
NSArray *arrCodecs = [encodeFac supportedCodecs];
// RTCVideoCodecInfo *info = arrCodecs[2];
// RTCVideoCodecInfo *info1 = arrCodecs[2];
// [encodeFac setPreferredCodec :info];
_factory = [[RTCPeerConnectionFactory alloc]initWithEncoderFactory:encodeFac decoderFactory:decodeFac];
// _factory = [[RTCPeerConnectionFactory alloc] init];
}
}
#pragma mark --open
-(void)openWith:(NSDictionary*)dic{
//得到data
//得到所有的连接
NSArray *connections = dic[@"remoteIds"];
//加到连接数组中去
[_connectionIdArray addObjectsFromArray:connections];
//拿到给自己分配的ID
_myId = dic[@"sender"];
//如果为空,则创建点对点工厂
if (!_factory)
{
//设置SSL传输
[RTCPeerConnectionFactory initialize];
[self create_factory];
}
//如果本地视频流为空
if (!_localStream)
{
//创建本地流
[self createLocalStream];
}
//创建连接
[self createPeerConnections];
//添加
[self addStreams];
// [self createOffers];
//获取房间内所有用户的代理回调
dispatch_async(dispatch_get_main_queue(), ^{
if ([self->_friendDelegate respondsToSelector:@selector(webRTCHelper:gotFriendList:)]) {
[self->_friendDelegate webRTCHelper:self gotFriendList:connections];
}
});
}
#pragma mark --_ice_candidate
-(void)_ice_candidateWith:(NSDictionary *)dic{
NSDictionary *dataDic = dic[@"data"];
NSString *socketId = dic[@"sender"];
NSDictionary *candidateDic =dataDic[@"candidate"];
NSString *sdpMid = candidateDic[@"sdpMid"];
int sdpMLineIndex = [candidateDic[@"sdpMLineIndex"] intValue];
NSString *sdp = candidateDic[@"candidate"];
//生成远端网络地址对象
RTCIceCandidate *candidate = [[RTCIceCandidate alloc] initWithSdp:sdp sdpMLineIndex:sdpMLineIndex sdpMid:sdpMid];
//拿到当前对应的点对点连接
RTCPeerConnection *peerConnection = [_connectionDic objectForKey:socketId];
//添加到点对点连接中
[peerConnection addIceCandidate:candidate];
}
#pragma mark ------join
-(void)joinWith:(NSDictionary *)dic{
//拿到新人的ID
NSString *socketId = dic[@"sender"];
//再去创建一个连接
RTCPeerConnection *peerConnection = [self createPeerConnection:socketId];
if (!_localStream)
{
[self createLocalStream];
}
//把本地流加到连接中去
[peerConnection addStream:_localStream];
//连接ID新加一个
[_connectionIdArray addObject:socketId];
//并且设置到Dic中去
[_connectionDic setObject:peerConnection forKey:socketId];
[self createOffer:peerConnection];
dispatch_async(dispatch_get_main_queue(), ^{
//设置新加入用户代理
if ([self->_friendDelegate respondsToSelector:@selector(webRTCHelper:gotNewFriend:)]) {
[self->_friendDelegate webRTCHelper:self gotNewFriend:socketId];
}
});
}
#pragma mark --remove
-(void)removeWith:(NSDictionary *)dic{
//得到socketId,关闭这个peerConnection
NSString *socketId = dic[@"sender"];
[self closePeerConnection:socketId];
//设置关闭某个用户聊天代理回调
dispatch_async(dispatch_get_main_queue(), ^{
// if ([self->_delegate respondsToSelector:@selector(webRTCHelper:closeWithUserId:)])
// {
// [self->_delegate webRTCHelper:self closeWithUserId:socketId];
// }
//设置退出房间用户代理回调
if ([self->_friendDelegate respondsToSelector:@selector(webRTCHelper:removeFriend:)]) {
[self->_friendDelegate webRTCHelper:self removeFriend:socketId];
}
});
}
#pragma mark --offer
-(void)offerWith:(NSDictionary *)dic{
NSDictionary *dataDic = dic[@"data"];
//拿到SDP
NSString *sdp = dataDic[@"sdp"];
NSString *socketId = dic[@"sender"];
//拿到这个点对点的连接
RTCPeerConnection *peerConnection = [_connectionDic objectForKey:socketId];
//根据类型和SDP 生成SDP描述对象
RTCSessionDescription *remoteSdp = [[RTCSessionDescription alloc] initWithType:RTCSdpTypeOffer sdp:sdp];
// RTCSessionDescription *newSdp = [self descriptionForDescription:remoteSdp preferredVideoCodec:@"H264"];
//设置给这个点对点连接
__weak RTCPeerConnection *weakPeerConnection = peerConnection;
[peerConnection setRemoteDescription:remoteSdp completionHandler:^(NSError * _Nullable error) {
[self setSessionDescriptionWithPeerConnection:weakPeerConnection];
}];
//设置当前角色状态为被呼叫,(被发offer)
// _role = RoleCallee;
}
#pragma mark --answer
-(void)answerWith:(NSDictionary *)dic{
NSDictionary *dataDic = dic[@"data"];
if([dataDic count]==0){
return;
}
NSString *sdp = dataDic[@"sdp"];
NSString *socketId = dic[@"sender"];
RTCPeerConnection *peerConnection = [_connectionDic objectForKey:socketId];
RTCSessionDescription *remoteSdp = [[RTCSessionDescription alloc] initWithType:RTCSdpTypeAnswer sdp:sdp];
// RTCSessionDescription *newSdp = [self descriptionForDescription:remoteSdp preferredVideoCodec:@"H264"];
__weak RTCPeerConnection * weakPeerConnection = peerConnection;
[peerConnection setRemoteDescription:remoteSdp completionHandler:^(NSError * _Nullable error) {
// [self setSessionDescriptionWithPeerConnection:weakPeerConnection];
}];
}
#pragma mark WebSocketDelegate
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message{
NSLog(@"收到服务器消息:%@",message);
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:[message dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingMutableContainers error:nil];
NSString *eventName = dic[@"event"];
NSLog(@"====================eventName===============%@",eventName);
//1.发送加入房间后的反馈
if ([eventName isEqualToString:@"open"])
{
[self openWith:dic];
}
//4.接收到新加入的人发了ICE候选,(即经过ICEServer而获取到的地址)
else if ([eventName isEqualToString:@"_ice_candidate"])
{
[self _ice_candidateWith:dic];
}
//2.其他新人加入房间的信息
else if ([eventName isEqualToString:@"join"])
{
[self joinWith:dic];
}
//有人离开房间的事件
else if ([eventName isEqualToString:@"remove"])
{
[self removeWith:dic];
}
//这个新加入的人发了个offer
else if ([eventName isEqualToString:@"offer"])
{
[self offerWith:dic];
}
//回应offer
else if ([eventName isEqualToString:@"answer"])
{
[self answerWith:dic];
}
}
- (void)webSocketDidOpen:(SRWebSocket *)webSocket{
NSLog(@"socket连接成功");
dispatch_async(dispatch_get_main_queue(), ^{
if ([self->_delegate respondsToSelector:@selector(webRTCHelper:socketConnectState:)]) {
[self->_delegate webRTCHelper:self socketConnectState:WebSocketConnectSuccess];
}
});
}
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error{
NSLog(@"socket连接失败");
dispatch_async(dispatch_get_main_queue(), ^{
if ([self->_delegate respondsToSelector:@selector(webRTCHelper:socketConnectState:)]) {
[self->_delegate webRTCHelper:self socketConnectState:WebSocketConnectSuccess];
}
});
}
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean{
NSLog(@"socket关闭。code = %ld,reason = %@",code,reason);
}
- (NSString *)getKeyFromConnectionDic:(RTCPeerConnection *)peerConnection
{
//find socketid by pc
static NSString *socketId;
[_connectionDic enumerateKeysAndObjectsUsingBlock:^(NSString *key, RTCPeerConnection *obj, BOOL * _Nonnull stop) {
if ([obj isEqual:peerConnection])
{
NSLog(@"%@",key);
socketId = key;
}
}];
return socketId;
}
- (RTCSessionDescription *)descriptionForDescription:(RTCSessionDescription *)description preferredVideoCodec:(NSString *)codec
{
NSString *sdpString = description.sdp;
NSString *lineSeparator = @"\n";
NSString *mLineSeparator = @" ";
// Copied from PeerConnectionClient.java.
// TODO(tkchin): Move this to a shared C++ file.
NSMutableArray *lines =
[NSMutableArray arrayWithArray:
[sdpString componentsSeparatedByString:lineSeparator]];
NSInteger mLineIndex = -1;
NSString *codecRtpMap = nil;
// a=rtpmap: /
// [/]
NSString *pattern =
[NSString stringWithFormat:@"^a=rtpmap:(\\d+) %@(/\\d+)+[\r]?$", codec];
NSRegularExpression *regex =
[NSRegularExpression regularExpressionWithPattern:pattern
options:0
error:nil];
for (NSInteger i = 0; (i < lines.count) && (mLineIndex == -1 || !codecRtpMap);
++i) {
NSString *line = lines[i];
if ([line hasPrefix:@"m=video"]) {
mLineIndex = i;
continue;
}
NSTextCheckingResult *codecMatches =
[regex firstMatchInString:line
options:0
range:NSMakeRange(0, line.length)];
if (codecMatches) {
codecRtpMap =
[line substringWithRange:[codecMatches rangeAtIndex:1]];
continue;
}
}
if (mLineIndex == -1) {
RTCLog(@"No m=video line, so can't prefer %@", codec);
return description;
}
if (!codecRtpMap) {
RTCLog(@"No rtpmap for %@", codec);
return description;
}
NSArray *origMLineParts =
[lines[mLineIndex] componentsSeparatedByString:mLineSeparator];
if (origMLineParts.count > 3) {
NSMutableArray *newMLineParts =
[NSMutableArray arrayWithCapacity:origMLineParts.count];
NSInteger origPartIndex = 0;
// Format is: m= ...
[newMLineParts addObject:origMLineParts[origPartIndex++]];
[newMLineParts addObject:origMLineParts[origPartIndex++]];
[newMLineParts addObject:origMLineParts[origPartIndex++]];
[newMLineParts addObject:codecRtpMap];
for (; origPartIndex < origMLineParts.count; ++origPartIndex) {
if (![codecRtpMap isEqualToString:origMLineParts[origPartIndex]]) {
[newMLineParts addObject:origMLineParts[origPartIndex]];
}
}
NSString *newMLine =
[newMLineParts componentsJoinedByString:mLineSeparator];
[lines replaceObjectAtIndex:mLineIndex
withObject:newMLine];
} else {
RTCLogWarning(@"Wrong SDP media description format: %@", lines[mLineIndex]);
}
NSString *mangledSdpString = [lines componentsJoinedByString:lineSeparator];
return [[RTCSessionDescription alloc] initWithType:description.type
sdp:mangledSdpString];
}
@end
- ChatViewController.h
#import
@interface ChatViewController : UIViewController
@end
- ChatViewController.m
//
// ChatViewController.m
// WebRTC_new
//
// Created by
// Copyright © 2020年
//
#import "ChatViewController.h"
#import
#import "WebRTCHelper.h"
#import
#define kWidth [UIScreen mainScreen].bounds.size.width
#define kHeight [UIScreen mainScreen].bounds.size.height
@interface ChatCell:UICollectionViewCell
/*注释*/
@property (nonatomic,strong) RTCEAGLVideoView *videoView;
/*注释*/
@property (nonatomic,strong) RTCVideoTrack *track;
/*注释*/
@property (nonatomic,strong) CALayer *baseLayer;
@end
@implementation ChatCell
- (instancetype)init
{
self = [super init];
if (self) {
self.videoView = [[RTCEAGLVideoView alloc] initWithFrame:CGRectMake(0, 0, (kWidth-40)/3, (kWidth-40)/3 + 50)];
[self.contentView addSubview:self.videoView];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
self.contentView.layer.borderColor = [UIColor whiteColor].CGColor;
self.contentView.layer.borderWidth = 1;
self.videoView = [[RTCEAGLVideoView alloc] initWithFrame:CGRectMake(0, 0, (kWidth-40)/3, (kWidth-40)/3 + 50)];
}
return self;
}
- (void)setTrack:(RTCVideoTrack *)track{
if (track != nil) {
self.contentView.layer.mask = nil;
[self.contentView addSubview:self.videoView];
[track addRenderer:self.videoView];
}else{
self.contentView.layer.mask = nil;
for (UIView * view in self.contentView.subviews) {
[view removeFromSuperview];
}
[self setShaperLayer];
}
}
-(void)setShaperLayer{
//高亮状态下的imageView
UIImageView * highlightImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)];
highlightImageView.center = self.contentView.center;
highlightImageView.image = [UIImage imageNamed:@"voice_ highlight"];
//默认状态下的imageView
UIImageView * defaultImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)];
defaultImageView.image = [UIImage imageNamed:@"voice_default"];
//先添加highlightImageView,在添加defaultImageview
defaultImageView.center = self.contentView.center;
[self.contentView addSubview:defaultImageView];
[self.contentView addSubview:highlightImageView];
self.baseLayer = nil;
if (self.baseLayer == nil) {
self.baseLayer = [CALayer layer];
self.baseLayer.frame = highlightImageView.bounds;
}
//创建左边layer
CAShapeLayer * leftLayer = [CAShapeLayer layer];
leftLayer.fillColor = [UIColor greenColor].CGColor;
leftLayer.position = CGPointMake(-25, 25);
leftLayer.bounds = highlightImageView.bounds;
leftLayer.path = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 50,50)].CGPath;
[self.baseLayer addSublayer:leftLayer];
//左边动画
CABasicAnimation * leftAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
leftAnimation.fromValue = [NSValue valueWithCGPoint:CGPointMake(-25, 25)];
leftAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(5, 25)];
leftAnimation.duration = 1.0;
leftAnimation.repeatCount = MAXFLOAT;
[leftLayer addAnimation:leftAnimation forKey:@"noVoiceLeftAnimation"];
//创建右边layer
CAShapeLayer * rightLayer = [CAShapeLayer layer];
// rightLayer.strokeColor = [UIColor greenColor].CGColor;
rightLayer.bounds = highlightImageView.bounds;
rightLayer.position = CGPointMake(75, 25);
rightLayer.fillColor = [UIColor greenColor].CGColor;
rightLayer.path = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 50, 50)].CGPath;
[self.baseLayer addSublayer:rightLayer];
//动画
CABasicAnimation * rightAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
rightAnimation.duration = 1.0;
rightAnimation.fromValue = [NSValue valueWithCGPoint:CGPointMake(75, 25)];
rightAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(45, 25)];
rightAnimation.repeatCount = MAXFLOAT;
[rightLayer addAnimation:rightAnimation forKey:@"noVoiceRightAnimation"];
highlightImageView.layer.mask = self.baseLayer;
}
@end
@interface ChatViewController ()
{
RTCMediaStream * _localSteam;
//判断是显示前摄像头还是显示后摄像头(yes为前摄像头。false为后摄像头)
BOOL _usingFrontCamera;
//是否显示我的视频流(默认为yes,显示;no为不显示)
BOOL _usingCamera;
}
@property (weak, nonatomic) IBOutlet UICollectionView *collectionView;
/*保存远端视频流*/
@property (nonatomic,strong) NSMutableDictionary *videoTracks;
/*房间内其他用户*/
@property (nonatomic,strong) NSMutableArray *members;
//显示本地视频的view
@property (weak, nonatomic) IBOutlet RTCCameraPreviewView *localVideoView;
@end
@implementation ChatViewController
/*注释*/
- (NSMutableArray *)members
{
if(!_members){
_members = [NSMutableArray array];
}
return _members;
}
/*注释*/
- (NSMutableDictionary *)videoTracks
{
if(!_videoTracks){
_videoTracks = [NSMutableDictionary dictionary];
}
return _videoTracks;
}
- (void)viewDidLoad {
[super viewDidLoad];
_usingFrontCamera = YES;
_usingCamera = YES;
NSLog(@"uid = %@",[[[UIDevice currentDevice] identifierForVendor] UUIDString]);
[WebRTCHelper shareInstance].delegate = self;
[WebRTCHelper shareInstance].friendDelegate = self;
UICollectionViewFlowLayout * layout = [[UICollectionViewFlowLayout alloc] init];
layout.minimumLineSpacing = 10;
layout.minimumInteritemSpacing = 10;
layout.itemSize = CGSizeMake((kWidth-40)/3, (kWidth-40)/3+50);
self.collectionView.collectionViewLayout = layout;
self.collectionView.delegate = self;
self.collectionView.dataSource = self;
[self.collectionView registerClass:[ChatCell class] forCellWithReuseIdentifier:@"chatCell"];
[self connect];
//设置屏幕常亮
[[UIApplication sharedApplication] setIdleTimerDisabled:YES];
if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}
self.navigationItem.hidesBackButton = YES;
}
-(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
self.navigationController.interactivePopGestureRecognizer.enabled = YES;
}
}
#pragma mark -按钮点击操作
/**
* 关闭按钮
*/
- (IBAction)closeChatBtnClick:(UIButton *)sender {
[[WebRTCHelper shareInstance] exitRoom];
[self.navigationController popViewControllerAnimated:YES];
}
/**
* 摄像头转换(前后摄像头)
*/
- (IBAction)swithVideoBtnClick:(UIButton *)sender {
_usingFrontCamera = !_usingFrontCamera;
[[WebRTCHelper shareInstance] swichCamera:_usingFrontCamera];
}
/**
* 语音是否开启
*/
- (IBAction)swichAudioBtnClick:(UIButton *)sender {
[sender setImage:[UIImage imageNamed:@"audioOn"] forState:UIControlStateNormal];
}
/**
是否显示本地摄像头
*/
- (IBAction)swichLocaolCameraBtnClick:(UIButton *)sender {
_usingCamera = !_usingCamera;
// [sender setImage:[UIImage imageNamed:_usingCamera?@"videoOn":@"videoOff"] forState:UIControlStateNormal];
// [[WebRTCHelper shareInstance] showLocaolCamera:_usingCamera];
}
/**
* 连接服务器
*/
-(void)connect{
[[WebRTCHelper shareInstance] connectServer:@"172.27.104.54" port:@"9090/SocketServer"];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
NSLog(@"self.members=====%@", self.members);
return self.members.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
ChatCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"chatCell" forIndexPath:indexPath];
NSString * userId = [self.members objectAtIndex:indexPath.item];
RTCVideoTrack * track = [self.videoTracks objectForKey:userId];
NSLog(@"%@==6666==%@",track,userId);
cell.track = track;
return cell;
}
#pragma mark -WebRTCHelperFrindDelegate
- (void)webRTCHelper:(WebRTCHelper *)webRTCHelper gotFriendList:(NSArray *)friendList{
[self.members removeAllObjects];
[self.members addObjectsFromArray:friendList];
[_collectionView reloadData];
}
- (void)webRTCHelper:(WebRTCHelper *)webRTCHelper gotNewFriend:(NSString *)friendId{
[self.members addObject:friendId];
[_collectionView reloadData];
}
- (void)webRTCHelper:(WebRTCHelper *)webRTCHelper removeFriend:(NSString *)friendId{
[self.members removeObject:friendId];
[_collectionView reloadData];
if (self.members.count == 0) {
// [[WebRTCHelper shareInstance] exitRoom];
// [self.navigationController popViewControllerAnimated:YES];
}
}
#pragma mark -WebRTCHelperDelegate
- (void)webRTCHelper:(WebRTCHelper *)webRTCHelper receiveMessage:(NSString *)message{
NSLog(@"messaga = %@",message);
}
/**
* 旧版本获取本地视频流的代理,在这个代理里面会获取到RTCVideoTrack类,然后添加到RTCEAGLVideoView类型的localVideoView上面
*/
- (void)webRTCHelper:(WebRTCHelper *)webRTCHelper setLocalStream:(RTCMediaStream *)steam userId:(NSString *)userId{
if (steam) {
_localSteam = steam;
RTCVideoTrack * track = [_localSteam.videoTracks lastObject];
[track addRenderer:self.localVideoView];
}
}
/**
* 新版获取本地视频流的方法
* @param captureSession RTCCameraPreviewView类的参数,通过设置这个,就可以达到显示本地视频的功能
*/
- (void)webRTCHelper:(WebRTCHelper *)webRTCHelper capturerSession:(AVCaptureSession *)captureSession{
self.localVideoView.captureSession = captureSession;
}
/**
* 获取远端视频流的方法,主要是获取到RTCVideoTrack类型的数据,然后保存起来,在刷新列表的时候,添加到对应item里面的RTCEAGLVideoView类型的view上面
*/
- (void)webRTCHelper:(WebRTCHelper *)webRTCHelper addRemoteStream:(RTCMediaStream *)stream userId:(NSString *)userId{
RTCVideoTrack * track = [stream.videoTracks lastObject];
NSLog(@"track====%@",track);
if (track != nil) {
[self.videoTracks setObject:track forKey:userId];
}
[self.collectionView reloadData];
}
- (void)webRTCHelper:(WebRTCHelper *)webRTCHelper closeWithUserId:(NSString *)userId{
[self.videoTracks removeObjectForKey:userId];
if (self.videoTracks.count >= self.members.count) {
[self.collectionView reloadData];
}
}
- (void)webRTCHelper:(WebRTCHelper *)webRTCHelper socketConnectState:(WebSocketConnectState)connectState{
if (connectState == WebSocketConnectField) {
HGAlertViewController * alert = [HGAlertViewController alertControllerWithTitle:@"提示" message:@"连接socket失败" preferredStyle:(UIAlertControllerStyleAlert)];
alert.addAction(@"取消",^(UIAlertAction *alertAction){
[[WebRTCHelper shareInstance] exitRoom];
[self.navigationController popViewControllerAnimated:YES];
}).addAction(@"确定",^(UIAlertAction *alertAction){
[[WebRTCHelper shareInstance] exitRoom];
[self.navigationController popViewControllerAnimated:YES];
});
[self presentViewController:alert animated:YES completion:nil];
}
}
-(void)dealloc{
NSLog(@"移除了");
//取消设置屏幕常亮
[[UIApplication sharedApplication] setIdleTimerDisabled:NO];
}
@end