WebRTC是Google公司的一款跨平臺的音視頻通話技術(shù)晨逝,它為我們提供了音視頻通信的核心技術(shù),包括音視頻的采集懦铺、編解碼捉貌、網(wǎng)絡(luò)傳輸、視頻顯示等功能冬念。
借助這款A(yù)PI趁窃,我們可以更容易地實(shí)現(xiàn)音視頻聊天功能。
準(zhǔn)備工作:
1急前、兩個客戶端棚菊。
2、一臺socket長連接服務(wù)器叔汁,用于交換客戶端兩端的通信信息(ip地址统求,端口等),以及管理通信狀態(tài)等据块。
3码邻、一臺STUN/TURN/ICE Server,用于獲取公網(wǎng)IP另假。
通信步驟:
1像屋、客戶端告知WebRTC獲取公網(wǎng)地址的服務(wù)器的IP,整個過程中WebRTC會不斷去獲取公網(wǎng)地址边篮,獲取公網(wǎng)地址后己莺,通過socket服務(wù)器發(fā)送給對方奏甫。
2、客戶端拿到對方發(fā)送的公網(wǎng)地址凌受,保存起來阵子,WebRTC會在通信的時候選擇最優(yōu)的公網(wǎng)地址。
3胜蛉、呼叫方生成一個類型為offer的會話描述挠进,并設(shè)置為本地回話描述,然后通過socket服務(wù)器發(fā)送給接聽方誊册。
4领突、接聽方收到offer的會話描述后,把offer設(shè)置為了遠(yuǎn)程回話描述案怯。并生成一個類型為answer的回話描述君旦,發(fā)送給呼叫方。
5嘲碱、呼叫方收到接收方的answer金砍,把a(bǔ)nswer設(shè)置為了遠(yuǎn)程回話描述。
6悍汛、至此捞魁,雙方P2P連接建立,開始音視頻通信离咐。
具體實(shí)現(xiàn):
@property(nonatomic, strong) RTCPeerConnection* peerConnection;
@property(nonatomic, strong) RTCPeerConnectionFactory* peerConnectionFactory;
@property(nonatomic, strong) RTCSessionDescription* localSdp;
@property(nonatomic, strong) RTCSessionDescription* remoteSdp;
@property(nonatomic, strong) RTCMediaConstraints* sdpMediaConstraints;
@property(nonatomic, strong) RTCAudioTrack* audioTrack;
@property(nonatomic, strong) RTCMediaStream* localMediaStream;
@property(nonatomic, strong) NSObject* candidateMutex;
@property(nonatomic, strong) NSMutableArray* queuedRemoteCandidates;
@property(nonatomic, strong) NSObject* sdpMutex;
@property(nonatomic, strong) NSMutableArray* queuedRemoteSdp;
@property(nonatomic, strong) RTCConfiguration *rtcConfig;
// video
@property(nonatomic, strong) RTCEAGLVideoView* localVideoView;
@property(nonatomic, strong) RTCEAGLVideoView* remoteVideoView;
@property(nonatomic, strong) RTCVideoTrack* localVideoTrack;
@property(nonatomic, strong) RTCVideoTrack* remoteVideoTrack;
- RTCPeerConnection:連接管理類谱俭。
- RTCPeerConnectionFactory:RTC連接工廠類,負(fù)責(zé)一些全局配置宵蛀,也負(fù)責(zé)RTC對象的- 實(shí)例化昆著。
- RTCSessionDescription:會話描述,呼叫方類型為offer术陶;接聽方為answer凑懂。
- RTCMediaConstraints:媒體信息約束,可以理解為對媒體信息進(jìn)行配置的類梧宫。
- RTCMediaStream: 媒體流接谨。
- RTCAudioTrack:音頻軌道,用于添加進(jìn)RTCMediaStream里塘匣,才會有聲音傳輸脓豪。
- RTCVideoTrack:視頻軌道,用于添加進(jìn)RTCMediaStream里忌卤,才會有視頻傳輸扫夜。
- RTCEAGLVideoView:RTCVideoTrack渲染之后,可顯示視頻畫面。
- RTCConfiguration:配置連接信息笤闯,配置連接時候的超時信息堕阔,ICE服務(wù)器的地址等。
接下來進(jìn)行整體的初始化:
創(chuàng)建ConnectionFactory颗味;
#如果想要更加安全就開啟SSL超陆。
[RTCPeerConnectionFactory initializeSSL];
self.peerConnectionFactory = [[RTCPeerConnectionFactory alloc] init];
創(chuàng)建媒體流,并且添加音頻軌道和視頻軌道
if (self.peerConnectionFactory) {
# 初始化媒體流脱衙,ARDAMSa0和ARDAMS為專業(yè)標(biāo)識符侥猬。
self.localMediaStream = [self.peerConnectionFactory mediaStreamWithLabel:@"ARDAMS"];
#添加音頻軌道
self.audioTrack = [self.peerConnectionFactory audioTrackWithID:@"ARDAMSa0"];
[self.localMediaStream addAudioTrack:self.audioTrack];
if (self.supportVideo) {
#添加視頻軌道
self.localVideoTrack = [self createVideoTrack:self.useFrontFacingCamera];
[self.localMediaStream addVideoTrack:self.localVideoTrack];
#把本地視頻軌道渲染到localVideoView
[self.localVideoTrack addRenderer:self.localVideoView];
}
}
創(chuàng)建媒體會話描述SDP:
#配置信息的基礎(chǔ)單元例驹,以鍵值對的方式
RTCPair* audio = [[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" value:@"true"];
NSArray* mandatory;
if (self.supportVideo) {
RTCPair* video = [[RTCPair alloc] initWithKey:@"OfferToReceiveVideo" value:@"true"];
mandatory = @[ audio, video ];
} else {
mandatory = @[ audio ];
}
self.sdpMediaConstraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatory
optionalConstraints:nil];
#此處將ICEServers的ip地址和登錄信息傳遞給RTC
NSMutableArray *iceServers = [[NSMutableArray alloc] init];
for(NSDictionary * dic in list)
{
NSString *strUsername = [dic objectForKey:@"userName"];
NSString *strPassword = [dic objectForKey:@"password"];
NSURL * strUri= [NSURL URLWithString:[dic objectForKey:@"uri"]];
RTCICEServer * iceServer = [[RTCICEServer alloc] initWithURI:strUri username:strUsername password:strPassword];
[iceServers addObject:iceServer];//jay for test
}
#config設(shè)置
self.rtcConfig = [[RTCConfiguration alloc] init];
self.rtcConfig.iceServers = iceServers;
self.rtcConfig.iceConnectionReceivingTimeout = 90000; // 90s
#沒有使用DtlsSrtp加密捐韩。
RTCPair *dtlssrtp = [[RTCPair alloc] initWithKey:@"DtlsSrtpKeyAgreement" value:@"false"]; //lianwei add for test
RTCMediaConstraints *constraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatory optionalConstraints:@[dtlssrtp]];
#創(chuàng)建peerConnection
if (self.peerConnectionFactory) {
self.peerConnection = [self.peerConnectionFactory peerConnectionWithConfiguration:self.rtcConfig
constraints:constraints
delegate:self];
}
#把媒體流添加進(jìn)peerConnection
if (self.peerConnection) {
[self.peerConnection addStream:self.localMediaStream];
}
當(dāng)我們將ICEServers告知RTC,且設(shè)置好peerConnection的delegate之后鹃锈,如果獲取到公網(wǎng)地址信息(ICECandidate)荤胁,則會觸發(fā)- (void)peerConnection:(RTCPeerConnection*)peerConnection gotICECandidate:(RTCICECandidate*)candidate
方法:
- (void)peerConnection:(RTCPeerConnection*)peerConnection gotICECandidate:(RTCICECandidate*)candidate
{
ZUSLOG(@"gotICECandidate,peerConnection=%@,canidate=%@",peerConnection,candidate);
NSDictionary *dicCandidate = @{
@"type" : @"candidate",
@"label" : @(candidate.sdpMLineIndex),
@"id" : candidate.sdpMid,
@"candidate" : candidate.sdp
};
NSString *message = [NSString stringWithDictionary:dicCandidate];
# 將獲取到ICECandidate通過socket服務(wù)器發(fā)送給其他客戶端。
[self sendRTCMessage:message withOfferType:NO];
}
而當(dāng)其他端收到發(fā)送過來的ICECandidate屎债,需要存入peerConnection備用:
NSString *type = dict[@"type"];
if ([type isEqualToString:@"candidate"]) {
NSString* mid = MessageDict[@"id"];
NSNumber* sdpLineIndex = MessageDict[@"label"];
NSString* sdp = MessageDict[@"candidate"];
//added by wafer for connection information,2017.06.15
_sdpCandidate = [NSString stringWithFormat:@"%@%@\r\n",_sdpCandidate,sdp];
RTCICECandidate* candidate = [[RTCICECandidate alloc] initWithMid:mid index:sdpLineIndex.intValue sdp:sdp];
#添加到peerConnection里
if (self.peerConnection) {
[self.peerConnection addICECandidate:candidate];
ZUSLOG(@"receiveRTCMessage,addICECandidate");
}
}
到這里仅政,通信之前的準(zhǔn)備工作就差不多了,接下來要開始撥打電話了:
呼叫方首先生成一個offer類型的SDP(會話描述):
#判斷是否是呼叫方
if (self.initiator) {
#呼叫方生成一個offer類型的SDP
[self.peerConnection createOfferWithDelegate:self constraints:self.sdpMediaConstraints];
}
這里又出現(xiàn)了delegate盆驹,于是我們需要在回調(diào)方法里處理事件圆丹。
這里的protocol一共有兩個方法:
1、生成SDP的回調(diào):
- (void)peerConnection:(RTCPeerConnection *)peerConnection didCreateSessionDescription:(RTCSessionDescription *)sdp error:(NSError *)error;
2躯喇、這個是設(shè)置本地或者遠(yuǎn)程sdp的時候會調(diào)用:
- (void)peerConnection:(RTCPeerConnection *)peerConnection didSetSessionDescriptionWithError:(NSError *)error;
我們先處理第一個方法:
首先我們在成功生成SDP后辫封,
- (void)peerConnection:(RTCPeerConnection*)peerConnection didCreateSessionDescription:(RTCSessionDescription*)origSdp error:(NSError*)error
{
ZUSLOG(@"didCreateSessionDescription,peerConnection=%@,origSdp=%@,error=%@",peerConnection,origSdp,error);
if (error) { #生成失敗
return;
}
#(不管是呼叫方還是接收方,處理都是一樣的)
self.localSdp = [[RTCSessionDescription alloc] initWithType:origSdp.type sdp:[NSString preferVoiceCodec:origSdp.description voiceCodecType:self.voiceCodecType]];
if (self.peerConnection) {
# 生成成功就把他設(shè)為本地SDP
[self.peerConnection setLocalDescriptionWithDelegate:self sessionDescription:self.localSdp];
}
# 然后再把生成的SDP發(fā)出去
NSDictionary *dicSdp = @{@"type":self.localSdp.type, @"sdp":self.localSdp.description};
NSString *message = [NSString stringWithDictionary:dicSdp];
[self sendRTCMessage:message withOfferType:YES];
});
}
然后就是接收方的處理(呼叫的信息是通過socket服務(wù)器告知的):
接收方在收到呼叫方的offer類型的SDP后廉丽,需要設(shè)置RemoteDescription為對方的SDP:
if ([type isEqualToString:@"offer"] ) {
NSString *sdpString = MessageDict[@"sdp"];
self.remoteSdp = [[RTCSessionDescription alloc] initWithType:type sdp:[NSString preferVoiceCodec:sdpString voiceCodecType:self.voiceCodecType]];
設(shè)置RemoteDescription為對方的SDP
if (self.peerConnection) {
[self.peerConnection setRemoteDescriptionWithDelegate:self
sessionDescription:self.remoteSdp];
}
設(shè)置成功之后會觸發(fā)代理方法:
- (void)peerConnection:(RTCPeerConnection *)peerConnection didSetSessionDescriptionWithError:(NSError *)error;
我們處理接收方設(shè)置完成之后的回調(diào)倦微,接收方需要生成類型為answer的SDP返回給呼叫方,表示同意接受通信正压。
- (void)peerConnection:(RTCPeerConnection*)peerConnection didSetSessionDescriptionWithError:(NSError*)error
{
if (error) {
return;
}
#判斷是接收方
if (!self.initiator) {
if (self.peerConnection && self.peerConnection.remoteDescription && !self.peerConnection.localDescription) {
#
[self.peerConnection createAnswerWithDelegate:self constraints:self.sdpMediaConstraints];
}
}
}
然后又觸發(fā)delegate方法
- (void)peerConnection:(RTCPeerConnection *)peerConnection didCreateSessionDescription:(RTCSessionDescription *)sdp error:(NSError *)error;
還是一模一樣的欣福,把生成的answer的SDP設(shè)為接收方自己的SDP,再把這個SDP再返回給呼叫方焦履。
最后再回到接收方:
接收方在收到回調(diào)之后拓劝,再把RemoteDescription設(shè)置好,至此嘉裤,連接正式建立
if ([type isEqualToString:@"answer"]) {
NSString *sdpString = MessageDict[@"sdp"];
self.remoteSdp = [[RTCSessionDescription alloc] initWithType:type sdp:[NSString preferVoiceCodec:sdpString voiceCodecType:self.voiceCodecType]];
if (self.peerConnection) {
[self.peerConnection setRemoteDescriptionWithDelegate:self
sessionDescription:self.remoteSdp];
}
}
從代碼的角度看郑临,呼叫方和接收方的邏輯公用還有回調(diào)之間的跳來跳去是有點(diǎn)暈。
但是總結(jié)一下:其實(shí)就是呼叫方生成offer的SDP价脾,接收方生成answer的SDP牧抵,雙方交換,然后設(shè)置好兩端的localSDP和RemoteSDP。
最后犀变,視頻流傳輸?shù)倪^程中會觸發(fā)回調(diào)方法妹孙,記得渲染到遠(yuǎn)端視頻界面remoteVideoView:
- (void)peerConnection:(RTCPeerConnection*)peerConnection addedStream:(RTCMediaStream*)stream
{
dispatch_async(dispatch_get_main_queue(), ^{
ZUSLOG(@"PCO onAddStream.");
if (self.supportVideo && stream.videoTracks.count) {
self.remoteVideoTrack = stream.videoTracks[0];
[self.remoteVideoTrack addRenderer:self.remoteVideoView]; //渲染
}
});
}
完 ~