今天我们继续完成我们上节没有完成的任务,上节我们已经将连接和信令的处理逻辑都处理完成了,今天我们来完成媒体协商。那么首先我们要写一个媒体协商的call方法,这个call方法也不干什么特别重要的事,它就是创建一个Offer,然后最终这个Offer创建成功之后通过端对端消息发送给对端,如果我们想创建这个Offer,那首先我们要判断一下我们的状态,那必须是要在joinconnction这个状态下我们才能调用call;而这个call是必须在调用者,如果两端的话调用者,如果是两端的话,调用者才能调用这个call,就是作为发起端才能调用call,那么作为接收端它是不能调用这个call;所以我们要做一个判断,如果state等于joined_conn那处于这种状态下,我们才能去做这件事,那这件比较简单,就是说用PC调用createOffer,这是传入一个offerOptions,如果回调成功那么调用getOffer如果出错了就是 catch,第三个是iceStart,第四个是静音检测,那首先我们把音频和视频打开,这样我们就在 SDP里设置了 我要接收远端的视频和音频;作用这两个选型就是我是否能控制远端的视频和音频的;那么这样我们这里创建Offer就创建完成了。
function call(){
if(state === 'joined_conn'){
var offerOptions = {
offerToRecieveAudio: 1, // 表示是否接收视频
offerToRecieveVideo: 1 // 表示是否接收音频
}
pc.createOffer(offerOptions)
.then(getOffer)
.catch(handleOfferError);
}
}
它是一个异步操作,如果成功了会调用getOffer,那getOffer其实比较简单,它首先有个参数desc,首先我们要调用pc.setLocalDescription来触发收集candidate, 收集完了之后我们要sendMessage发送一个端对端消息给另一端,第一个参数就是roomid,第二个参数就是数据offerdesc,这样我们就把消息发送给对端了,让他知道我这个Offer已经创建好了,对端收到这个Offer之后它会创建一个Answer再给我们返回来。
function getOffer(desc){
pc.setLocalDescription(desc);
offer.value = desc.sdp;
offerdesc = desc;
//send offer sdp
sendMessage(roomid, offerdesc);
}
在发送之前我们还有这个sendMessage没实现, 它主要是传一个roomid和一个data,这里实际调用的是socket.emit,我们首先打印 一个console表示我们已经进入这个函数了,
function sendMessage(roomid, data){
console.log('send message to other end', roomid, data);
if(!socket){
console.log('socket is null');
}
socket.emit('message', roomid, data);
}
还有一个是错误处理, 当出现这个错误的时候 我们就打印一下错误信息 。
function handleOfferError(err){
console.error('Failed to create offer:', err);
}
当我们收到Offer之后,先setLocalDescription然后紧接着是sendMessage,那当发送到服务器端进行转发,转发完了又会回到信令处理这里,所以在message要处理很多的逻辑了。
那首先我们要判断一下这里传过来的数据是不是对的,如果数据不对我们也就不用在做了,如果data是OK的,我们要接着做下面的处理,data.type有好几种,第一种就是offer,如果遇到offer应该怎么处理,否则data.type等于answer会怎么样,还有一个是data.type等于candidate会如何,如果都不是,这里就要打印一个错误信息了。
那我们来一个个处理,如果收到的是Offer,那首先对端的pc已经创建好了,那它要调用setRemoteDescription,并且将这个desc设置进去,这个完了之后它紧接着要调用createAnswer,这个函数里如果成功就要getAnswer,如果出错了就调用handleAnswerError;
稍后我们再来实现getAnswer,在这里这个data,比如说它在通过这个信令发过来的时候,它就已经不再是一个对象了,那在我们在getOffer的是它是一个对象,getOffer的时候它拿到这个desc是一个对象,但是我们把这个sendMessage在发送过来的时候实际已经转化成文本了,这时候我们还要给它生成一个对象,所以这里我们不能就简单的desc传进去,我们要new RTCSessionDescription(desc),把它当作一个钩子函数设置进去,这样就OK了,
如果类型是Answer,那就是pc.setRemoteDescription(new RTCSessionDescription(desc)); 那么这时候我们就将这个远端设置好了,
那再接下来就是candidate,我们设置了setLocalDescription之后,双方都可以进行收集candidate了,每当收到一个candidate都是触发了这个pc的onIceCandidate这个事件,在这个事件触发之后它会触发sendMessage,那发送过来之后,当我们处理这个candidate,那我们也有new RTCIceCandidate,那在这里我们要给它传一些参数,第一个是sdpMLineIndex: data.label, 也就是说我们媒体行的行号是多少,第二个是candidate: data.candidate,这样我们生成一个新的candidate,那有了这个新的candidate之后我们要加入本端,也就是pc.addIceCandidate,这样就将candidate加入到我们的PeerConnection当中去了,那它下面就会进行连接性检测,
socket.on('message', (roomid, data) => {
console.log('receive message!', roomid, data);
if(data === null || data === undefined){
console.error('the message is invalid!');
return;
}
if(data.hasOwnProperty('type') && data.type === 'offer') {
offer.value = data.sdp;
pc.setRemoteDescription(new RTCSessionDescription(desc));
//create answer
pc.createAnswer()
.then(getAnswer)
.catch(handleAnswerError);
}else if(data.hasOwnProperty('type') && data.type == 'answer'){
answer.value = data.sdp;
pc.setRemoteDescription(new RTCSessionDescription(desc));
}else if (data.hasOwnProperty('type') && data.type === 'candidate'){
var candidate = new RTCIceCandidate({
sdpMLineIndex: data.label,
candidate: data.candidate
});
pc.addIceCandidate(candidate);
}else{
console.log('the message is invalid!', data);
}
});
下面我们来实现getAnswer函数,它也是要传过来一个desc,所以在我们获取到本地的Answer之后,我们要做setLocalDescription,就是通知本地要收集这个candidate了,也就是我们要发送消息也就是sendMessage(roomid, desc);
也就是说被调用方首先收到这个Offer之后,要设置setRemoteDescription才能调用这个getAnswer,这个逻辑是没问题的。
function getAnswer(desc){
pc.setLocalDescription(desc);
answer.value = desc.sdp;
//send answer sdp
sendMessage(roomid, desc);
}
我们再实现一下handleAnswerError,那创建Answer失败之后我们要打印一个console,那这个错误信息就写完了,
function handleAnswerError(err){
console.error('Failed to create answer:', err);
}
在实现一下createPeerConnection,也就是说当我们发现candidate的时候也就是说我们找到了一个candidate,这个时候我们要sendMessage,将这个candidata要要发送出去,发送出去这个candidata应该是一个什么样的格式呢?这里其实它包括了几个类型,
首先它要输入一个roomid,然后第一个就是说它肯定要有一个type,那这个 type就是candidate, 然后就是label, label的值就是event.candidate.sdpMLineIndex,那么再下一个是id,这个id就是event.candidate.sdpMid,最后一个candidate就是event.candidate.candidate,
对candidate来说,那它发送一个消息,要我们组装,那我们实际要用的时候主要是用label:event.candidate.sdpMLineIndex, 和candidate: event.candidate.candidate,那这样我们candidate这个消息也构造好了,
function createPeerConnection(){
//如果是多人的话,在这里要创建一个新的连接.
//新创建好的要放到一个map表中。
//key=userid, value=peerconnection
console.log('create RTCPeerConnection!');
if(!pc){
pc = new RTCPeerConnection(pcConfig);
pc.onicecandidate = (e)=>{
if(e.candidate) {
sendMessage(roomid, {
type: 'candidate',
label:event.candidate.sdpMLineIndex,
id:event.candidate.sdpMid,
candidate: event.candidate.candidate
});
}else{
console.log('this is the end candidate');
}
}
pc.ontrack = getRemoteStream;
}else {
console.warning('the pc have be created!');
}
return;
}