在前面的章节中,已经对WebRTC相关的重要知识点进行了介绍,包括涉及的网络协议、会话描述协议、如何进行网络穿透等,剩下的就是WebRTC的API了。
WebRTC通信相关的API非常多,主要完成了如下功能:
信令交换通信候选地址交换音视频采集音视频发送、接收相关API太多,为避免篇幅过长,文中部分采用了伪代码进行讲解。详细代码参考文章末尾,也可以在笔者的Github上找到,有问题欢迎留言交流。
信令交换
信令交换是WebRTC通信中的关键环节,交换的信息包括编解码器、网络协议、候选地址等。对于如何进行信令交换,WebRTC并没有明确说明,而是交给应用自己来决定,比如可以采用WebSocket。
发送方伪代码如下:
const pc = new RTCPeerConnection(iceConfig);const offer = await pc.createOffer();await pc.setLocalDescription(offer);sendtopeerViaSignalingServer(SIGNAliNG_OFFER,offer); // 发送方发送信令消息
接收方伪代码如下:
const pc = new RTCPeerConnection(iceConfig);await pc.setRemoteDescription(offer);const answer = await pc.createAnswer();await pc.setLocalDescription(answer);sendtopeerViaSignalingServer(SIGNAliNG_ANSWER,answer); // 接收方发送信令消息
候选地址交换服务
当本地设置了会话描述信息,并添加了媒体流的情况下,ICE框架就会开始收集候选地址。两边收集到候选地址后,需要交换候选地址,并从中知道合适的候选地址对。
候选地址的交换,同样采用前面提到的信令服务,伪代码如下:
// 设置本地会话描述信息const localPeer = new RTCPeerConnection(iceConfig);const offer = await pc.createOffer();await localPeer.setLocalDescription(offer);// 本地采集音视频const localVIDeo = document.getElementByID('local-vIDeo');const mediaStream = await navigator.mediaDevices.getUserMedia({ vIDeo: true,audio: true});localVIDeo.srcObject = mediaStream;// 添加音视频流mediaStream.getTracks().forEach(track => { localPeer.addTrack(track,mediaStream);});// 交换候选地址localPeer.onicecandIDate = function(evt) { if (evt.candIDate) { sendtopeerViaSignalingServer(SIGNAliNG_CANDIDATE,evt.candIDate); }}
音视频采集
可以使用浏览器提供的getUserMedia接口,采集本地的音视频。
const localVIDeo = document.getElementByID('local-vIDeo');const mediaStream = await navigator.mediaDevices.getUserMedia({ vIDeo: true,audio: true});localVIDeo.srcObject = mediaStream;
音视频发送、接收
将采集到的音视频轨道,通过addTrack进行添加,发送给远端。
mediaStream.getTracks().forEach(track => { localPeer.addTrack(track,mediaStream);});
远端可以通过监听ontrack来监听音视频的到达,并进行播放。
remotePeer.ontrack = function(evt) { const remoteVIDeo = document.getElementByID('remote-vIDeo'); remoteVIDeo.srcObject = evt.streams[0];}
完整代码
包含两部分:客户端代码、服务端代码。
1、客户端代码
const socket = io.connect('http://localhost:3000');const CLIENT_RTC_EVENT = 'CLIENT_RTC_EVENT';const SERVER_RTC_EVENT = 'SERVER_RTC_EVENT';const CLIENT_USER_EVENT = 'CLIENT_USER_EVENT';const SERVER_USER_EVENT = 'SERVER_USER_EVENT';const CLIENT_USER_EVENT_LOGIN = 'CLIENT_USER_EVENT_LOGIN'; // 登录const SERVER_USER_EVENT_UPDATE_USERS = 'SERVER_USER_EVENT_UPDATE_USERS';const SIGNAliNG_OFFER = 'SIGNAliNG_OFFER';const SIGNAliNG_ANSWER = 'SIGNAliNG_ANSWER';const SIGNAliNG_CANDIDATE = 'SIGNAliNG_CANDIDATE';let remoteUser = ''; // 远端用户let localUser = ''; // 本地登录用户function log(msg) { console.log(`[clIEnt] ${msg}`);}socket.on('connect',function() { log('ws connect.');});socket.on('connect_error',function() { log('ws connect_error.');});socket.on('error',function(errorMessage) { log('ws error,' + errorMessage);});socket.on(SERVER_USER_EVENT,function(msg) { const type = msg.type; const payload = msg.payload; switch(type) { case SERVER_USER_EVENT_UPDATE_USERS: updateUserList(payload); break; } log(`[${SERVER_USER_EVENT}] [${type}],${JsON.stringify(msg)}`);});socket.on(SERVER_RTC_EVENT,function(msg) { const {type} = msg; switch(type) { case SIGNAliNG_OFFER: handleReceiveOffer(msg); break; case SIGNAliNG_ANSWER: handleReceiveAnswer(msg); break; case SIGNAliNG_CANDIDATE: handleReceiveCandIDate(msg); break; }});async function handleReceiveOffer(msg) { log(`receive remote description from ${msg.payload.from}`); // 设置远端描述 const remoteDescription = new RTCSessionDescription(msg.payload.sdp); remoteUser = msg.payload.from; createPeerConnection(); await pc.setRemoteDescription(remoteDescription); // Todo 错误处理 // 本地音视频采集 const localVIDeo = document.getElementByID('local-vIDeo'); const mediaStream = await navigator.mediaDevices.getUserMedia({ vIDeo: true,audio: true }); localVIDeo.srcObject = mediaStream; mediaStream.getTracks().forEach(track => { pc.addTrack(track,mediaStream); // pc.addTransceiver(track,{streams: [mediaStream]}); // 这个也可以 }); // pc.addStream(mediaStream); // 目前这个也可以,不过接口后续会废弃 const answer = await pc.createAnswer(); // Todo 错误处理 await pc.setLocalDescription(answer); sendRTCEvent({ type: SIGNAliNG_ANSWER,payload: { sdp: answer,from: localUser,target: remoteUser } });}async function handleReceiveAnswer(msg) { log(`receive remote answer from ${msg.payload.from}`); const remoteDescription = new RTCSessionDescription(msg.payload.sdp); remoteUser = msg.payload.from; await pc.setRemoteDescription(remoteDescription); // Todo 错误处理}async function handleReceiveCandIDate(msg){ log(`receive candIDate from ${msg.payload.from}`); await pc.addIceCandIDate(msg.payload.candIDate); // Todo 错误处理}/** * 发送用户相关消息给服务器 * @param {Object} msg 格式如 { type: 'xx',payload: {} } */function sendUserEvent(msg) { socket.emit(CLIENT_USER_EVENT,JsON.stringify(msg));}/** * 发送RTC相关消息给服务器 * @param {Object} msg 格式如{ type: 'xx',payload: {} } */function sendRTCEvent(msg) { socket.emit(CLIENT_RTC_EVENT,JsON.stringify(msg));}let pc = null;/** * 邀请用户加入视频聊天 * 1、本地启动视频采集 * 2、交换信令 */async function startVIDeoTalk() { // 开启本地视频 const localVIDeo = document.getElementByID('local-vIDeo'); const mediaStream = await navigator.mediaDevices.getUserMedia({ vIDeo: true,audio: true }); localVIDeo.srcObject = mediaStream; // 创建 peerConnection createPeerConnection(); // 将媒体流添加到webrtc的音视频收发器 mediaStream.getTracks().forEach(track => { pc.addTrack(track,{streams: [mediaStream]}); }); // pc.addStream(mediaStream); // 目前这个也可以,不过接口后续会废弃}function createPeerConnection() { const iceConfig = {"iceServers": [ {url: 'stun:stun.ekiga.net'},{url: 'turn:turnserver.com',username: 'user',credential: 'pass'} ]}; pc = new RTCPeerConnection(iceConfig); pc.onnegotiationneeded = onnegotiationneeded; pc.onicecandIDate = onicecandIDate; pc.onicegatheringstatechange = onicegatheringstatechange; pc.oniceconnectionstatechange = oniceconnectionstatechange; pc.onsignalingstatechange = onsignalingstatechange; pc.ontrack = ontrack; return pc;}async function onnegotiationneeded() { log(`onnegotiationneeded.`); const offer = await pc.createOffer(); await pc.setLocalDescription(offer); // Todo 错误处理 sendRTCEvent({ type: SIGNAliNG_OFFER,payload: { from: localUser,target: remoteUser,sdp: pc.localDescription // Todo 直接用offer? } });}function onicecandIDate(evt) { if (evt.candIDate) { log(`onicecandIDate.`); sendRTCEvent({ type: SIGNAliNG_CANDIDATE,payload: { from: localUser,candIDate: evt.candIDate } }); }}function onicegatheringstatechange(evt) { log(`onicegatheringstatechange,pc.iceGatheringState is ${pc.iceGatheringState}.`);}function oniceconnectionstatechange(evt) { log(`oniceconnectionstatechange,pc.iceConnectionState is ${pc.iceConnectionState}.`);}function onsignalingstatechange(evt) { log(`onsignalingstatechange,pc.signalingstate is ${pc.signalingstate}.`);}// 调用 pc.addTrack(track,mediaStream),remote peer的 onTrack 会触发两次// 实际上两次触发时,evt.streams[0] 指向同一个mediaStream引用// 这个行为有点奇怪,github issue 也有提到 https://github.com/meetecho/janus-gateway/issues/1313let stream;function ontrack(evt) { // if (!stream) { // stream = evt.streams[0]; // } else { // console.log(`${stream === evt.streams[0]}`); // 这里为true // } log(`ontrack.`); const remoteVIDeo = document.getElementByID(
总结 以上是内存溢出为你收集整理的WebRTC:一个视频聊天的简单例子全部内容,希望文章能够帮你解决WebRTC:一个视频聊天的简单例子所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)