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
,表示参与对话的两方都准备好了。
上面这一部分的代码在服务器的搭建中都讲过了。
客户端进入网页后会自动打开摄像头:
javascript">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中。
javascript">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通讯过程中发挥了非常大的作用。
javascript">//客户端代码
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信息。
javascript">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消息。
javascript">// 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建立的流程,但是并不完善,但是网络上完整的项目很多,大家可以多找找~