标签:
前面介绍了如何下载编译WebRTC,现在介绍如何利用WebRTC在iOS客户端上简单实现音视频通话。
对下载编译还有问题的,请先查看:WebRTC(iOS)下载编译。
不需要下载源码只需要库文件的也可以用CocoaPods下载编译好的库:pod ‘libjingle_peerconnection‘
开始编写之前,我们首先要搭建一个服务器,此服务器主要用于信令交互。我们这里采用github上的开源项目:SkyRTC项目。
完整Demo下载:FLWebRTCDemo。
开始iOS客户端的实现:
1、引入编译出的WebRTC库文件
2、引入必要的依赖库
AudioToolbox.framework
VideoToolbox.framework
QuartzCore.framework
OpenGLES.framework
CoreGraphics.framework
CoreVideo.framework
CoreMedia.framework
CoreAudio.framework
AVFoundation.framework
GLKit.framework
CFNetwork.framework
Security.framework
libsqlite3.tbd
libicucore.tbd
libc.tbd
libstdc++.6.0.9.tbd
3、代码实现音视频通话
// | |
// WebRTCHelper.h | |
// WebRTCDemo | |
// | |
#import <Foundation/Foundation.h> | |
#import <SocketRocket.h> | |
#import "RTCMediaStream.h" | |
#import "RTCPeerConnectionFactory.h" | |
#import "RTCPeerConnection.h" | |
#import "RTCPair.h" | |
#import "RTCMediaConstraints.h" | |
#import "RTCAudioTrack.h" | |
#import "RTCVideoTrack.h" | |
#import "RTCVideoCapturer.h" | |
#import "RTCSessionDescription.h" | |
#import "RTCSessionDescriptionDelegate.h" | |
#import "RTCEAGLVideoView.h" | |
#import "RTCICEServer.h" | |
#import "RTCVideoSource.h" | |
#import "RTCAVFoundationVideoSource.h" | |
#import "RTCICECandidate.h" | |
@protocol WebRTCHelperDelegate; | |
@interface WebRTCHelper : NSObject<SRWebSocketDelegate> | |
@property (nonatomic, assign)id<WebRTCHelperDelegate> delegate; | |
/** | |
* 与服务器建立连接 | |
* | |
* @param server 服务器地址 | |
* @param room 房间号 | |
*/ | |
- (void)connectServer:(NSString *)server room:(NSString *)room; | |
/** | |
* 退出房间 | |
*/ | |
- (void)exitRoom; | |
@end | |
@protocol WebRTCHelperDelegate <NSObject> | |
@optional | |
- (void)webRTCHelper:(WebRTCHelper *)webRTChelper setLocalStream:(RTCMediaStream *)stream userId:(NSString *)userId; | |
- (void)webRTCHelper:(WebRTCHelper *)webRTChelper addRemoteStream:(RTCMediaStream *)stream userId:(NSString *)userId; | |
- (void)webRTCHelper:(WebRTCHelper *)webRTChelper closeWithUserId:(NSString *)userId; | |
@end |
// | |
// WebRTCHelper.m | |
// WebRTCDemo | |
// | |
#import "WebRTCHelper.h" | |
#import <UIKit/UIKit.h> | |
#import <AVFoundation/AVFoundation.h> | |
typedef enum : NSUInteger { | |
RoleCaller, | |
RoleCallee, | |
} Role; | |
@interface WebRTCHelper ()<RTCPeerConnectionDelegate, RTCSessionDescriptionDelegate> | |
@end | |
@implementation WebRTCHelper | |
{ | |
SRWebSocket *_socket; | |
NSString *_server; | |
NSString *_room; | |
RTCPeerConnectionFactory *_factory; | |
RTCMediaStream *_localStream; | |
NSString *_myId; | |
NSMutableDictionary *_connectionDic; | |
NSMutableArray *_connectionIdArray; | |
NSString *_currentId; | |
Role _role; | |
} | |
- (instancetype)init | |
{ | |
self = [super init]; | |
if (self) | |
{ | |
_connectionDic = [NSMutableDictionary dictionary]; | |
_connectionIdArray = [NSMutableArray array]; | |
} | |
return self; | |
} | |
/** | |
* 与服务器建立连接 | |
* | |
* @param server 服务器地址 | |
* @param room 房间号 | |
*/ | |
- (void)connectServer:(NSString *)server room:(NSString *)room | |
{ | |
_server = server; | |
_room = room; | |
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"ws://%@:3000",server]] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10]; | |
_socket = [[SRWebSocket alloc] initWithURLRequest:request]; | |
_socket.delegate = self; | |
[_socket open]; | |
} | |
/** | |
* 加入房间 | |
* | |
* @param room 房间号 | |
*/ | |
- (void)joinRoom:(NSString *)room | |
{ | |
if (_socket.readyState == SR_OPEN) | |
{ | |
NSDictionary *dic = @{@"eventName": @"__join", @"data": @{@"room": room}}; | |
NSData *data = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:nil]; | |
[_socket send:data]; | |
} | |
} | |
/** | |
* 退出房间 | |
*/ | |
- (void)exitRoom | |
{ | |
_localStream = nil; | |
[_connectionIdArray enumerateObjectsUsingBlock:^(NSString *obj, NSUInteger idx, BOOL * _Nonnull stop) { | |
[self closePeerConnection:obj]; | |
}]; | |
[_socket close]; | |
} | |
/** | |
* 关闭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 ([_delegate respondsToSelector:@selector(webRTCHelper:closeWithUserId:)]) | |
{ | |
[_delegate webRTCHelper:self closeWithUserId:connectionId]; | |
} | |
}); | |
} | |
/** | |
* 创建本地流 | |
*/ | |
- (void)createLocalStream | |
{ | |
_localStream = [_factory mediaStreamWithLabel:@"ARDAMS"]; | |
//音频 | |
RTCAudioTrack *audioTrack = [_factory audioTrackWithID:@"ARDAMSa0"]; | |
[_localStream addAudioTrack:audioTrack]; | |
//视频 | |
NSArray *deviceArray = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; | |
AVCaptureDevice *device = [deviceArray lastObject]; | |
//检测摄像头权限 | |
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) | |
{ | |
RTCVideoCapturer *capturer = [RTCVideoCapturer capturerWithDeviceName:device.localizedName]; | |
RTCVideoSource *videoSource = [_factory videoSourceWithCapturer:capturer constraints:[self localVideoConstraints]]; | |
RTCVideoTrack *videoTrack = [_factory videoTrackWithID:@"ARDAMSv0" source:videoSource]; | |
[_localStream addVideoTrack:videoTrack]; | |
if ([_delegate respondsToSelector:@selector(webRTCHelper:setLocalStream:userId:)]) | |
{ | |
[_delegate webRTCHelper:self setLocalStream:_localStream userId:_myId]; | |
} | |
} | |
else | |
{ | |
NSLog(@"该设备不能打开摄像头"); | |
if ([_delegate respondsToSelector:@selector(webRTCHelper:setLocalStream:userId:)]) | |
{ | |
[_delegate webRTCHelper:self setLocalStream:nil userId:_myId]; | |
} | |
} | |
} | |
} | |
/** | |
* 视频的相关约束 | |
*/ | |
- (RTCMediaConstraints *)localVideoConstraints | |
{ | |
RTCPair *maxWidth = [[RTCPair alloc] initWithKey:@"maxWidth" value:@"640"]; | |
RTCPair *minWidth = [[RTCPair alloc] initWithKey:@"minWidth" value:@"640"]; | |
RTCPair *maxHeight = [[RTCPair alloc] initWithKey:@"maxHeight" value:@"480"]; | |
RTCPair *minHeight = [[RTCPair alloc] initWithKey:@"minHeight" value:@"480"]; | |
RTCPair *minFrameRate = [[RTCPair alloc] initWithKey:@"minFrameRate" value:@"15"]; | |
NSArray *mandatory = @[maxWidth, minWidth, maxHeight, minHeight, minFrameRate]; | |
RTCMediaConstraints *constraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatory optionalConstraints:nil]; | |
return constraints; | |
} | |
/** | |
* 为所有连接创建offer | |
*/ | |
- (void)createOffers | |
{ | |
[_connectionDic enumerateKeysAndObjectsUsingBlock:^(NSString *key, RTCPeerConnection *obj, BOOL * _Nonnull stop) { | |
_currentId = key; | |
_role = RoleCaller; | |
[obj createOfferWithDelegate:self constraints:[self offerOranswerConstraint]]; | |
}]; | |
} | |
/** | |
* 为所有连接添加流 | |
*/ | |
- (void)addStreams | |
{ | |
[_connectionDic enumerateKeysAndObjectsUsingBlock:^(NSString *key, RTCPeerConnection *obj, BOOL * _Nonnull stop) { | |
if (!_localStream) | |
{ | |
[self createLocalStream]; | |
} | |
[obj addStream:_localStream]; | |
}]; | |
} | |
/** | |
* 创建所有连接 | |
*/ | |
- (void)createPeerConnections | |
{ | |
[_connectionIdArray enumerateObjectsUsingBlock:^(NSString *obj, NSUInteger idx, BOOL * _Nonnull stop) { | |
RTCPeerConnection *connection = [self createPeerConnection:obj]; | |
[_connectionDic setObject:connection forKey:obj]; | |
}]; | |
} | |
/** | |
* 创建点对点连接 | |
* | |
* @param connectionId <#connectionId description#> | |
* | |
* @return <#return value description#> | |
*/ | |
- (RTCPeerConnection *)createPeerConnection:(NSString *)connectionId | |
{ | |
if (!_factory) | |
{ | |
[RTCPeerConnectionFactory initializeSSL]; | |
_factory = [[RTCPeerConnectionFactory alloc] init]; | |
} | |
RTCPeerConnection *connection = [_factory peerConnectionWithICEServers:nil constraints:[self peerConnectionConstraints] delegate:self]; | |
return connection; | |
} | |
/** | |
* peerConnection约束 | |
* | |
* @return <#return value description#> | |
*/ | |
- (RTCMediaConstraints *)peerConnectionConstraints | |
{ | |
RTCMediaConstraints *constraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:nil optionalConstraints:@[[[RTCPair alloc] initWithKey:@"DtlsSrtpKeyAgreement" value:@"true"]]]; | |
return constraints; | |
} | |
/** | |
* 设置offer/answer的约束 | |
*/ | |
- (RTCMediaConstraints *)offerOranswerConstraint | |
{ | |
NSMutableArray *array = [NSMutableArray array]; | |
RTCPair *receiveAudio = [[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" value:@"true"]; | |
[array addObject:receiveAudio]; | |
NSString *video = @"true"; | |
RTCPair *receiveVideo = [[RTCPair alloc] initWithKey:@"OfferToReceiveVideo" value:video]; | |
[array addObject:receiveVideo]; | |
RTCMediaConstraints *constraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:array optionalConstraints:nil]; | |
return constraints; | |
} | |
#pragma mark--RTCSessionDescriptionDelegate | |
// Called when creating a session. | |
- (void)peerConnection:(RTCPeerConnection *)peerConnection | |
didCreateSessionDescription:(RTCSessionDescription *)sdp | |
error:(NSError *)error | |
{ | |
NSLog(@"%s",__func__); | |
[peerConnection setLocalDescriptionWithDelegate:self sessionDescription:sdp]; | |
} | |
// Called when setting a local or remote description. | |
- (void)peerConnection:(RTCPeerConnection *)peerConnection | |
didSetSessionDescriptionWithError:(NSError *)error | |
{ | |
NSLog(@"%s",__func__); | |
if (peerConnection.signalingState == RTCSignalingHaveRemoteOffer) | |
{ | |
[peerConnection createAnswerWithDelegate:self constraints:[self offerOranswerConstraint]]; | |
} | |
else if (peerConnection.signalingState == RTCSignalingHaveLocalOffer) | |
{ | |
if (_role == RoleCallee) | |
{ | |
NSDictionary *dic = @{@"eventName": @"__answer", @"data": @{@"sdp": @{@"type": @"answer", @"sdp": peerConnection.localDescription.description}, @"socketId": _currentId}}; | |
NSData *data = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:nil]; | |
[_socket send:data]; | |
} | |
else if(_role == RoleCaller) | |
{ | |
NSDictionary *dic = @{@"eventName": @"__offer", @"data": @{@"sdp": @{@"type": @"offer", @"sdp": peerConnection.localDescription.description}, @"socketId": _currentId}}; | |
NSData *data = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:nil]; | |
[_socket send:data]; | |
} | |
} | |
else if (peerConnection.signalingState == RTCSignalingStable) | |
{ | |
if (_role == RoleCallee) | |
{ | |
NSDictionary *dic = @{@"eventName": @"__answer", @"data": @{@"sdp": @{@"type": @"answer", @"sdp": peerConnection.localDescription.description}, @"socketId": _currentId}}; | |
NSData *data = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:nil]; | |
[_socket send:data]; | |
} | |
} | |
} | |
#pragma mark--RTCPeerConnectionDelegate | |
// Triggered when the SignalingState changed. | |
- (void)peerConnection:(RTCPeerConnection *)peerConnection | |
signalingStateChanged:(RTCSignalingState)stateChanged | |
{ | |
NSLog(@"%s",__func__); | |
NSLog(@"%d", stateChanged); | |
} | |
// Triggered when media is received on a new stream from remote peer. | |
- (void)peerConnection:(RTCPeerConnection *)peerConnection | |
addedStream:(RTCMediaStream *)stream | |
{ | |
NSLog(@"%s",__func__); | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
if ([_delegate respondsToSelector:@selector(webRTCHelper:addRemoteStream:userId:)]) | |
{ | |
[_delegate webRTCHelper:self addRemoteStream:stream userId:_currentId]; | |
} | |
}); | |
} | |
// Triggered when a remote peer close a stream. | |
- (void)peerConnection:(RTCPeerConnection *)peerConnection | |
removedStream:(RTCMediaStream *)stream | |
{ | |
NSLog(@"%s",__func__); | |
} | |
// Triggered when renegotiation is needed, for example the ICE has restarted. | |
- (void)peerConnectionOnRenegotiationNeeded:(RTCPeerConnection *)peerConnection | |
{ | |
NSLog(@"%s",__func__); | |
} | |
// Called any time the ICEConnectionState changes. | |
- (void)peerConnection:(RTCPeerConnection *)peerConnection | |
iceConnectionChanged:(RTCICEConnectionState)newState | |
{ | |
NSLog(@"%s",__func__); | |
NSLog(@"%d", newState); | |
} | |
// Called any time the ICEGatheringState changes. | |
- (void)peerConnection:(RTCPeerConnection *)peerConnection | |
iceGatheringChanged:(RTCICEGatheringState)newState | |
{ | |
NSLog(@"%s",__func__); | |
NSLog(@"%d", newState); | |
} | |
// New Ice candidate have been found. | |
- (void)peerConnection:(RTCPeerConnection *)peerConnection | |
gotICECandidate:(RTCICECandidate *)candidate | |
{ | |
NSLog(@"%s",__func__); | |
NSDictionary *dic = @{@"eventName": @"__ice_candidate", @"data": @{@"label": [NSNumber numberWithInteger:candidate.sdpMLineIndex], @"candidate": candidate.sdp, @"socketId": _currentId}}; | |
NSData *data = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:nil]; | |
[_socket send:data]; | |
} | |
// New data channel has been opened. | |
- (void)peerConnection:(RTCPeerConnection*)peerConnection didOpenDataChannel:(RTCDataChannel*)dataChannel | |
{ | |
NSLog(@"%s",__func__); | |
} | |
#pragma mark--SRWebSocketDelegate | |
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message | |
{ | |
NSLog(@"收到服务器消息:%@",message); | |
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:[message dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingMutableContainers error:nil]; | |
NSString *eventName = dic[@"eventName"]; | |
if ([eventName isEqualToString:@"_peers"]) | |
{ | |
NSDictionary *dataDic = dic[@"data"]; | |
NSArray *connections = dataDic[@"connections"]; | |
[_connectionIdArray addObjectsFromArray:connections]; | |
_myId = dataDic[@"you"]; | |
if (!_factory) | |
{ | |
[RTCPeerConnectionFactory initializeSSL]; | |
_factory = [[RTCPeerConnectionFactory alloc] init]; | |
} | |
if (!_localStream) | |
{ | |
[self createLocalStream]; | |
} | |
[self createPeerConnections]; | |
[self addStreams]; | |
[self createOffers]; | |
} | |
else if ([eventName isEqualToString:@"_ice_candidate"]) | |
{ | |
NSDictionary *dataDic = dic[@"data"]; | |
NSString *socketId = dataDic[@"socketId"]; | |
NSInteger sdpMLineIndex = [dataDic[@"label"] integerValue]; | |
NSString *sdp = dataDic[@"candidate"]; | |
RTCICECandidate *candidate = [[RTCICECandidate alloc] initWithMid:nil index:sdpMLineIndex sdp:sdp]; | |
RTCPeerConnection *peerConnection = [_connectionDic objectForKey:socketId]; | |
[peerConnection addICECandidate:candidate]; | |
} | |
else if ([eventName isEqualToString:@"_new_peer"]) | |
{ | |
NSDictionary *dataDic = dic[@"data"]; | |
NSString *socketId = dataDic[@"socketId"]; | |
RTCPeerConnection *peerConnection = [self createPeerConnection:socketId]; | |
if (!_localStream) | |
{ | |
[self createLocalStream]; | |
} | |
[peerConnection addStream:_localStream]; | |
[_connectionIdArray addObject:socketId]; | |
[_connectionDic setObject:peerConnection forKey:socketId]; | |
} | |
else if ([eventName isEqualToString:@"_remove_peer"]) | |
{ | |
NSDictionary *dataDic = dic[@"data"]; | |
NSString *socketId = dataDic[@"socketId"]; | |
[self closePeerConnection:socketId]; | |
} | |
else if ([eventName isEqualToString:@"_offer"]) | |
{ | |
NSDictionary *dataDic = dic[@"data"]; | |
NSDictionary *sdpDic = dataDic[@"sdp"]; | |
NSString *sdp = sdpDic[@"sdp"]; | |
NSString *type = sdpDic[@"type"]; | |
NSString *socketId = dataDic[@"socketId"]; | |
RTCPeerConnection *peerConnection = [_connectionDic objectForKey:socketId]; | |
RTCSessionDescription *remoteSdp = [[RTCSessionDescription alloc] initWithType:type sdp:sdp]; | |
[peerConnection setRemoteDescriptionWithDelegate:self sessionDescription:remoteSdp]; | |
_currentId = socketId; | |
_role = RoleCallee; | |
} | |
else if ([eventName isEqualToString:@"_answer"]) | |
{ | |
NSDictionary *dataDic = dic[@"data"]; | |
NSDictionary *sdpDic = dataDic[@"sdp"]; | |
NSString *sdp = sdpDic[@"sdp"]; | |
NSString *type = sdpDic[@"type"]; | |
NSString *socketId = dataDic[@"socketId"]; | |
RTCPeerConnection *peerConnection = [_connectionDic objectForKey:socketId]; | |
RTCSessionDescription *remoteSdp = [[RTCSessionDescription alloc] initWithType:type sdp:sdp]; | |
[peerConnection setRemoteDescriptionWithDelegate:self sessionDescription:remoteSdp]; | |
} | |
} | |
- (void)webSocketDidOpen:(SRWebSocket *)webSocket | |
{ | |
NSLog(@"websocket建立成功"); | |
[self joinRoom:_room]; | |
} | |
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error | |
{ | |
NSLog(@"%s",__func__); | |
NSLog(@"%ld:%@",(long)error.code, error.localizedDescription); | |
// [[[UIAlertView alloc] initWithTitle:@"提示" message:[NSString stringWithFormat:@"%ld:%@",(long)error.code, error.localizedDescription] delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil, nil] show]; | |
} | |
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean | |
{ | |
NSLog(@"%s",__func__); | |
NSLog(@"%ld:%@",(long)code, reason); | |
// [[[UIAlertView alloc] initWithTitle:@"提示" message:[NSString stringWithFormat:@"%ld:%@",(long)code, reason] delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil, nil] show]; | |
} | |
@end |
标签:
原文地址:http://www.cnblogs.com/fulianga/p/5869208.html