RTCPeerConnection 接口代表一個由本地計算機到遠端的WebRTC連接。該接口提供了創建,保持,監控,關閉連接的方法的實現。
一對一視頻對話的時候,連接過程如下所示:
這個過程是不是看起來還挺清晰的?但是這只是表述了媒體信息交換的過程,別忘了還有網絡信息~
接下來我們就先好好捋一捋PC建立的過程吧!
-
A打開本地視頻流,創建PeerConnection對象,將本地音視頻流封裝成MediaStream添加到PeerConnection中;
-
A通過
CreateOffer
創建offer信息,調用setLocalDescription
儲存本地offer,然後通過信令服務器將offer發送給B; -
A在調用
setLocalDescription
的同時,向服務器發送了iceCandidate消息; -
B打開本地視頻流,創建PeerConnection對象,將本地音視頻流封裝成MediaStream添加到PeerConnection中;
-
B收到offer後,通過
setRemoteDescription
儲存遠端offer,然後通過CreateAnswer
創建answer信息,同樣調用setLocalDescription
儲存本地answer描述,再將answer發送給A; -
B在調用
setLocalDescription
的同時,向服務器發送了iceCandidate消息; -
A收到B的answer後,再次調用
setRemoteDescription
設置遠端的answer描述。
那接下來就讓我們從頭看一看,一對一視頻對話究竟是怎麼實現的吧~ 這裏我會按照事件的發送順序,將客戶端和服務器端的代碼拆分開說,雖然從邏輯上來講是對的,但是代碼會比較亂,只用於理解,不能直接用哦。
當用戶訪問對話頁面時,會向服務器發送’create or join’消息
服務器收到’create or join’,並計算當前房間裏的人數
若 numClients === 0,則向客戶端發送’created’消息;客戶端收到’created’消息,則將當前用戶作爲會話發起人,令
isInitiator = true
。
若 numClients === 1, 則向會話發起人發送’join’消息,發起人端isChannelReady = true
; 向新用戶發送’joined’消息,新用戶端isChannelReady = true
,表示參與對話的兩方都準備好了。
上面這一部分的代碼在服務器的搭建中都講過了。
客戶端進入網頁後會自動打開攝像頭:
navigator.mediaDevices.getUserMedia({
audio: false,
video: true
})
.then(gotLocalMediaStream)
.catch(function (e) {
alert('getUserMedia() error: ' + e.name);
});
function gotLocalMediaStream(mediastream) {
console.log('Adding local stream.');
localStream = mediastream;
localVideo.srcObject = mediastream;
sendMessage('got user media');
if (isInitiator) {
maybeStart();
}
}
對於會話發起人,在打開攝像頭的時候,會發起
maybeStart()
方法,進而實例化一個RTCPeerConnection,然後將本地的視頻流加入pc中。
function maybeStart() {
console.log('>>>>>>> maybeStart() ', isStarted, localStream, isChannelReady);
if (!isStarted && typeof localStream !== 'undefined' && isChannelReady) {
console.log('>>>>>> creating peer connection');
createPeerConnection();
pc.addStream(localStream);
isStarted = true;
console.log('isInitiator', isInitiator);
if (isInitiator) {
doCall();
}
}
}
function createPeerConnection() {
try {
pc = new RTCPeerConnection(null);
pc.onicecandidate = handleIceCandidate;
pc.onaddstream = handleRemoteStreamAdded;
pc.onremovestream = handleRemoteStreamRemoved;
console.log('Created RTCPeerConnnection');
} catch (e) {
console.log('Failed to create PeerConnection, exception: ' + e.message);
alert('Cannot create RTCPeerConnection object.');
return;
}
}
function handleIceCandidate(event) {
console.log('icecandidate event: ', event);
if (event.candidate) {
sendMessage({
type: 'candidate',
label: event.candidate.sdpMLineIndex,
id: event.candidate.sdpMid,
candidate: event.candidate.candidate
});
} else {
console.log('End of candidates.');
}
}
function handleRemoteStreamAdded(event) {
console.log('Remote stream added.');
remoteStream = event.stream;
remoteVideo.srcObject = event.stream;
}
function handleRemoteStreamRemoved(event) {
console.log('Remote stream removed. Event: ', event);
}
這裏用到了一個實現定義的方法sendMessage
,這個方法可以讓一個用戶端將信息發送給服務器,之後通過服務器,將這個消息轉發給其他的用戶端,在整個webRTC通訊過程中發揮了非常大的作用。
//客戶端代碼
function sendMessage(message) {
console.log('Client sending message: ', message);
socket.emit('message', message);
}
//服務器端相應處理的代碼
socket.on('message', function (message) {
socket.broadcast.emit('message', message);
});
作爲會話發起人,要率先執行
docall
操作,也就是向會話對方發送offer,這個過程中,發起人端會通過setLocalDescription
自動生成保存本地音視頻相關參數的SDP對象。注意,發起人端在調用setLocalDescription
的時候,還觸發了handleIceCandidate
,向服務器發送了iceCandidate信息。
function doCall() {
console.log('Sending offer to peer');
pc.createOffer(setLocalAndSendMessage, handleCreateOfferError);
}
function setLocalAndSendMessage(sessionDescription) {
pc.setLocalDescription(sessionDescription);
console.log('setLocalAndSendMessage sending message', sessionDescription);
sendMessage(sessionDescription);
}
function handleCreateOfferError(event) {
console.log('createOffer() error: ', event);
發起人端通過sendMessage
將offer信息和iceCandidate信息經由服務器發送給接收端,那麼相應的接收端會怎麼反應呢?接收端,其實大部分時間都是“被動反應”的。它根據接收到的消息,進行相對應的操作。
當接收端接受到來自發起人端的offer類型的message後,即開始初始化自己的peerConnection,
除了將本地視頻流添加到pc中,還會將收到的offer信息和candidate信息通過setRemoteDescription
和addIceCandidate
保存起來。
之後,接收端通過doAnswer()
向發起人端發送對話描述和candidate消息。
// This client receives a message
socket.on('message', function (message) {
console.log('Client received message:', message);
if (message === 'got user media') {
maybeStart();
} else if (message.type === 'offer') {
console.log("收到offer!"); //接收端
if (!isInitiator && !isStarted) {
maybeStart();
}
pc.setRemoteDescription(new RTCSessionDescription(message));
doAnswer();
} else if (message.type === 'answer' && isStarted) {
pc.setRemoteDescription(new RTCSessionDescription(message)); //發起人端
} else if (message.type === 'candidate' && isStarted) {
console.log("收到candidate!"); //接收端
var candidate = new RTCIceCandidate({
sdpMLineIndex: message.label,
candidate: message.candidate
});
pc.addIceCandidate(candidate);
} else if (message === 'bye' && isStarted) {
handleRemoteHangup();
}
});
function doAnswer() {
console.log('Sending answer to peer.');
pc.createAnswer().then(
setLocalAndSendMessage,
onCreateSessionDescriptionError
);
發起人端接收到接收端發來的answer和candidate,同樣通過
setRemoteDescription
和addIceCandidate
保存,這樣,參與對話的兩方交換了會話信息和ice信息,建立了音視頻傳輸的P2P通道。
建立連接之後,兩端的通過上面的
handleRemoteStreamAdded
方法將接受到的視頻流顯示到對應的視頻標籤內。
1v1的視頻通話,就這麼搞定了。這裏展示的代碼主要是幫助大家理清RTCPeerConnection建立的流程,但是並不完善,但是網絡上完整的項目很多,大家可以多找找~