效果图:
webRTC 通过 RTCPeerConnection 来建立连接高效,稳定的音视频流传输它依然还需要利用服务器来做一些准备工作。比如STUN服务器、TURN服务器、ICE(NAT和防火墙穿透)、信令传输,相互之间的信令交换完毕,就会发送实时音视频留给对方。
一.设置好 STUN 服务器
- (RTCICEServer *)defaultSTUNServer:(NSString *)stunURL {
NSURL *defaultSTUNServerURL = [NSURL URLWithString:stunURL];
return [[RTCICEServer alloc] initWithURI:defaultSTUNServerURL
username:@""
password:@""];
}
二.创建 RTCPeerConnection 并设置好 本地视频/对方视频位置
- (void)setUPinitRTC
{
self.peerConnection = [self.peerConnectionFactory peerConnectionWithICEServers:_ICEServers constraints:self.pcConstraints delegate:self];
//设置 local media stream
RTCMediaStream *mediaStream = [self.peerConnectionFactory mediaStreamWithLabel:@"ARDAMS"];
// 添加 local video track
RTCAVFoundationVideoSource *source = [[RTCAVFoundationVideoSource alloc] initWithFactory:self.peerConnectionFactory constraints:self.videoConstraints];
RTCVideoTrack *localVideoTrack = [[RTCVideoTrack alloc] initWithFactory:self.peerConnectionFactory source:source trackId:@"AVAMSv0"];
[mediaStream addVideoTrack:localVideoTrack];
self.localVideoTrack = localVideoTrack;
// 添加 local audio track
RTCAudioTrack *localAudioTrack = [self.peerConnectionFactory audioTrackWithID:@"ARDAMSa0"];
[mediaStream addAudioTrack:localAudioTrack];
// 添加 mediaStream
[self.peerConnection addStream:mediaStream];
RTCEAGLVideoView *localVideoView = [[RTCEAGLVideoView alloc] initWithFrame:self.callView.meVideoView.bounds];
localVideoView.transform = CGAffineTransformMakeScale(-1, 1);
localVideoView.delegate = self;
[self.callView.meVideoView addSubview:localVideoView];
self.localVideoView = localVideoView;
[self.localVideoTrack addRenderer:self.localVideoView];
RTCEAGLVideoView *remoteVideoView = [[RTCEAGLVideoView alloc] initWithFrame:self.callView.friendVideoView.bounds];
remoteVideoView.transform = CGAffineTransformMakeScale(-1, 1);
remoteVideoView.delegate = self;
[self.callView.friendVideoView addSubview:remoteVideoView];
self.remoteVideoView = remoteVideoView;
}
三.由视频发起方来触发信令的发送 ,
- 通过SocketManager来发送offer信令
- 发起方发送 offer 信令给接受方
- 接受方收到 offer 初始化自己的 RTCPeerConnection 发出自己的offer 并保存接收到的消息
- 接收方点击接听视频时 -> 遍历保存的信息 -> 有offer信息保存sdp 并创建 answer -> 有 answer 保存 sdp
- 发起方接收到 answer 时保存
3.1-弹出视频对话view
- (void)showRTCViewByRemoteName:(NSString *)remoteName isVideo:(BOOL)isVideo isCaller:(BOOL)isCaller
{
// 1.显示视图
WS(weakSelf);
self.callView = [VideoOrAudioCallView callViewWithUserName:remoteName isVideo:isVideo role:isCaller ? RoleCaller : RoleCallee];
self.callView.acceptHandle = ^{
[weakSelf acceptAction];
};
self.callView.closeHandle = ^{
[weakSelf hangupEvent];
};
// 拨打时,禁止黑屏
[UIApplication sharedApplication].idleTimerDisabled = YES;
// 做RTC必要设置
if (isCaller) {
[self setUPinitRTC];
// 如果是发起者,创建一个offer信令
[self.peerConnection createOfferWithDelegate:self constraints:self.sdpConstraints];
}
}
3.2-收到消息的处理
- (void)hangupEvent
{
NSDictionary *dict = @{@"type":@"bye"};
[self processMessageDict:dict];
}
- (void)receiveSignalingMessage:(NSNotification *)notification
{
NSDictionary *dict = notification.userInfo;
NSString *type = dict[@"type"];
if ([type isEqualToString:@"offer"]) {
[self showRTCViewByRemoteName:CURRENT_FRIENDNAME isVideo:YES isCaller:NO];
[self.messages insertObject:dict atIndex:0];
} else if ([type isEqualToString:@"answer"]) {
RTCSessionDescription *sdp = [[RTCSessionDescription alloc] initWithType:type sdp:dict[@"sdp"]];
[self.peerConnection setRemoteDescriptionWithDelegate:self sessionDescription:sdp];
} else if ([type isEqualToString:@"candidate"]) {
[self.messages addObject:dict];
} else if ([type isEqualToString:@"bye"]) {
[self processMessageDict:dict];
}
}
- (void)acceptAction
{
[self initRTCSetting];
for (NSDictionary *dict in self.messages) {
[self processMessageDict:dict];
}
[self.messages removeAllObjects];
}
- (void)processMessageDict:(NSDictionary *)dict
{
NSString *type = dict[@"type"];
if ([type isEqualToString:@"offer"]) {
RTCSessionDescription *remoteSdp = [[RTCSessionDescription alloc] initWithType:type sdp:dict[@"sdp"]];
[self.peerConnection setRemoteDescriptionWithDelegate:self sessionDescription:remoteSdp];
[self.peerConnection createAnswerWithDelegate:self constraints:self.sdpConstraints];
} else if ([type isEqualToString:@"answer"]) {
RTCSessionDescription *remoteSdp = [[RTCSessionDescription alloc] initWithType:type sdp:dict[@"sdp"]];
[self.peerConnection setRemoteDescriptionWithDelegate:self sessionDescription:remoteSdp];
} else if ([type isEqualToString:@"candidate"]) {
NSString *mid = [dict objectForKey:@"id"];
NSNumber *sdpLineIndex = [dict objectForKey:@"label"];
NSString *sdp = [dict objectForKey:@"sdp"];
RTCICECandidate *candidate = [[RTCICECandidate alloc] initWithMid:mid index:sdpLineIndex.intValue sdp:sdp];
[self.peerConnection addICECandidate:candidate];
} else if ([type isEqualToString:@"bye"]) {
if (self.callView) {
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil];
NSString *jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
if (jsonStr.length > 0) {
[[SocketManager shareSockManager] RTCMessageSendWithData:jsonData withTag:-100];
}
WS(weakSelf);
[self.callView closeWithCompletion:^(BOOL finished) {
weakSelf.callView = nil;
}];
[self cleanCache];
}
}
}
3.3-创建信令的delegate回调 并发送 offer 信令 及sdp
#pragma mark - RTCSessionDescriptionDelegate
// Called when creating a session.
- (void)peerConnection:(RTCPeerConnection *)peerConnection
didCreateSessionDescription:(RTCSessionDescription *)sdp
error:(NSError *)error
{
if (error) {
NSLog(@"创建SessionDescription 失败");
} else {
[self.peerConnection setLocalDescriptionWithDelegate:self sessionDescription:sdp];
NSDictionary *jsonDict = @{ @"type" : sdp.type, @"sdp" : sdp.description};
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonDict options:0 error:nil];
[[SocketManager shareSockManager] RTCMessageSendWithData:jsonData withTag:-100];
}
}
// Called when setting a local or remote description.
- (void)peerConnection:(RTCPeerConnection *)peerConnection
didSetSessionDescriptionWithError:(NSError *)error
{
NSLog(@"%s",__func__);
}
3.4- RTCPeerConnectionDelegate 添加视频流 新的 Ice candidate 被发现 及发送
#pragma mark - RTCPeerConnectionDelegate
// Triggered when the SignalingState changed.
- (void)peerConnection:(RTCPeerConnection *)peerConnection
signalingStateChanged:(RTCSignalingState)stateChanged
{
NSLog(@"信令状态改变");
}
// Triggered when media is received on a new stream from remote peer.
- (void)peerConnection:(RTCPeerConnection *)peerConnection
addedStream:(RTCMediaStream *)stream
{
dispatch_async(dispatch_get_main_queue(), ^{
if ([stream.videoTracks count]) {
self.remoteVideoTrack = nil;
[self.remoteVideoView renderFrame:nil];
self.remoteVideoTrack = stream.videoTracks[0];
[self.remoteVideoTrack addRenderer:self.remoteVideoView];
// 连接成功后的UI操作
[self.callView connectFinshHandle];
}
[self videoView:self.remoteVideoView didChangeVideoSize:self.callView.friendVideoView.bounds.size];
[self videoView:self.localVideoView didChangeVideoSize:self.callView.meVideoView.bounds.size];
});
}
// 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__);
}
// Called any time the ICEGatheringState changes.
- (void)peerConnection:(RTCPeerConnection *)peerConnection
iceGatheringChanged:(RTCICEGatheringState)newState
{
NSLog(@"%s",__func__);
switch (newState) {
case RTCICEGatheringNew:
{
NSLog(@"newState = RTCICEGatheringNew");
}
break;
case RTCICEGatheringGathering:
{
NSLog(@"newState = RTCICEGatheringGathering");
}
break;
case RTCICEGatheringComplete:
{
NSLog(@"newState = RTCICEGatheringComplete");
}
break;
}
}
// New Ice candidate have been found.
- (void)peerConnection:(RTCPeerConnection *)peerConnection
gotICECandidate:(RTCICECandidate *)candidate
{
if (self.HaveSentCandidate) {
return;
}
NSDictionary *jsonDict = @{@"type":@"candidate",
@"label":[NSNumber numberWithInteger:candidate.sdpMLineIndex],
@"id":candidate.sdpMid,
@"sdp":candidate.sdp
};
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonDict options:0 error:nil];
if (jsonData.length > 0) {
[[SocketManager shareSockManager] RTCMessageSendWithData:jsonData withTag:-100];
self.HaveSentCandidate = YES;
}
}
// New data channel has been opened.
- (void)peerConnection:(RTCPeerConnection*)peerConnection
didOpenDataChannel:(RTCDataChannel*)dataChannel
{
NSLog(@"%s",__func__);
}