效果圖:
iPod.gif
iPone.gif
webRTC 通過 RTCPeerConnection 來建立連接高效,穩(wěn)定的音視頻流傳輸它依然還需要利用服務器來做一些準備工作。比如STUN服務器、TURN服務器、ICE(NAT和防火墻穿透)、信令傳輸,相互之間的信令交換完畢,就會發(fā)送實時音視頻留給對方。
一.設置好 STUN 服務器
- (RTCICEServer *)defaultSTUNServer:(NSString *)stunURL {
NSURL *defaultSTUNServerURL = [NSURL URLWithString:stunURL];
return [[RTCICEServer alloc] initWithURI:defaultSTUNServerURL
username:@""
password:@""];
}
二.創(chuàng)建 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;
}
三.由視頻發(fā)起方來觸發(fā)信令的發(fā)送 ,
- 通過SocketManager來發(fā)送offer信令
- 發(fā)起方發(fā)送 offer 信令給接受方
- 接受方收到 offer 初始化自己的 RTCPeerConnection 發(fā)出自己的offer 并保存接收到的消息
- 接收方點擊接聽視頻時 -> 遍歷保存的信息 -> 有offer信息保存sdp 并創(chuàng)建 answer -> 有 answer 保存 sdp
- 發(fā)起方接收到 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];
// 如果是發(fā)起者,創(chuàng)建一個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-創(chuàng)建信令的delegate回調 并發(fā)送 offer 信令 及sdp
#pragma mark - RTCSessionDescriptionDelegate
// Called when creating a session.
- (void)peerConnection:(RTCPeerConnection *)peerConnection
didCreateSessionDescription:(RTCSessionDescription *)sdp
error:(NSError *)error
{
if (error) {
NSLog(@"創(chuàng)建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 被發(fā)現 及發(fā)送
#pragma mark - RTCPeerConnectionDelegate
// Triggered when the SignalingState changed.
- (void)peerConnection:(RTCPeerConnection *)peerConnection
signalingStateChanged:(RTCSignalingState)stateChanged
{
NSLog(@"信令狀態(tài)改變");
}
// 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__);
}