WebRTC学习笔记一简单示例
一、捕获本地媒体流getUserMedia1.index.html Document 开始录制 停止录制
部署本地服务器后,chrome中访问 192.168.11.129:9050 会报错: navigator.getUserMedia is not a function 。原因是,Chrome 47以后,getUserMedia API只能允许来自"安全可信"的客户端的视频音频请求,如HTTPS和本地的Localhost。所以将访问地址改成localhost:9050即可。 二、同网页示例
例子来源:https://codelabs.developers.google.com/codelabs/webrtc-web/#4 1.index.html Realtime communication with WebRTC Realtime communication with WebRTC 开始 调用 挂断 2.main.js
【腾讯文档】FFmpegWebRTCRTMPRTSPHLSRTP播放器-音视频流媒体高级开发-资料领取FFmpegWebRTCRTMPRTSPHLSRTP鎾 斁鍣 -闊宠 棰戞祦濯掍綋楂樼骇寮 鍙 -璧勬枡棰嗗彇"use strict"; //log function trace(text) { text = text.trim(); const now = (window.performance.now() / 1000).toFixed(3); console.log(now, text); } // 设置两个video,分别显示本地视频流和远端视频流 const localVideo = document.getElementById("localVideo"); const remoteVideo = document.getElementById("remoteVideo"); localVideo.addEventListener("loadedmetadata", logVideoLoaded); remoteVideo.addEventListener("loadedmetadata", logVideoLoaded); remoteVideo.addEventListener("onresize", logResizedVideo); function logVideoLoaded(event) { const video = event.target; trace(`${video.id} videoWidth: ${video.videoWidth}px, ` + `videoHeight: ${video.videoHeight}px.`); } function logResizedVideo(event) { logVideoLoaded(event); if (startTime) { const elapsedTime = window.performance.now() - startTime; startTime = null; trace(`Setup time: ${elapsedTime.toFixed(3)}ms.`); } } let startTime = null; let localStream; let remoteStream; // 建立两个对等连接对象,分表代表本地和远端 let localPeerConnection; let remotePeerConnection; const startButton = document.getElementById("startButton"); const callButton = document.getElementById("callButton"); const hangupButton = document.getElementById("hangupButton"); callButton.disabled = true; hangupButton.disabled = true; startButton.addEventListener("click", startAction); callButton.addEventListener("click", callAction); hangupButton.addEventListener("click", hangupAction); // 传输视频,不传输音频 const mediaStreamConstraints = { video: true, audio: false }; //开始事件,采集摄像头到本地 function startAction() { startButton.disabled = true; navigator.getUserMedia(mediaStreamConstraints, gotLocalMediaStream, handleLocalMediaStreamError) trace("Requesting local stream."); } function gotLocalMediaStream(mediaStream) { localVideo.srcObject = mediaStream; localStream = mediaStream; trace("Received local stream."); callButton.disabled = false; } function handleLocalMediaStreamError(error) { trace(`navigator.getUserMedia error: ${error.toString()}.`); } // 设置只交换视频 const offerOptions = { offerToReceiveVideo: 1, }; // 创建对等连接 function callAction() { callButton.disabled = true; hangupButton.disabled = false; trace("Starting call."); startTime = window.performance.now(); const videoTracks = localStream.getVideoTracks(); const audioTracks = localStream.getAudioTracks(); if (videoTracks.length > 0) { trace(`Using video device: ${videoTracks[0].label}.`); } if (audioTracks.length > 0) { trace(`Using audio device: ${audioTracks[0].label}.`); } // 服务器配置 const servers = null; localPeerConnection = new RTCPeerConnection(servers); trace("Created local peer connection object localPeerConnection."); localPeerConnection.addEventListener("icecandidate", handleConnection); localPeerConnection.addEventListener("iceconnectionstatechange", handleConnectionChange); remotePeerConnection = new RTCPeerConnection(servers); trace("Created remote peer connection object remotePeerConnection."); remotePeerConnection.addEventListener("icecandidate", handleConnection); remotePeerConnection.addEventListener("iceconnectionstatechange", handleConnectionChange); remotePeerConnection.addEventListener("addstream", gotRemoteMediaStream); localPeerConnection.addStream(localStream); trace("Added local stream to localPeerConnection."); trace("localPeerConnection createOffer start."); localPeerConnection.createOffer(offerOptions) .then(createdOffer).catch(setSessionDescriptionError); } function getOtherPeer(peerConnection) { return (peerConnection === localPeerConnection) ? remotePeerConnection : localPeerConnection; } function getPeerName(peerConnection) { return (peerConnection === localPeerConnection) ? "localPeerConnection" : "remotePeerConnection"; } function handleConnection(event) { const peerConnection = event.target; const iceCandidate = event.candidate; if (iceCandidate) { const newIceCandidate = new RTCIceCandidate(iceCandidate); const otherPeer = getOtherPeer(peerConnection); otherPeer.addIceCandidate(newIceCandidate) .then(() => { handleConnectionSuccess(peerConnection); }).catch((error) => { handleConnectionFailure(peerConnection, error); }); trace(`${getPeerName(peerConnection)} ICE candidate: ` + `${event.candidate.candidate}.`); } } function handleConnectionSuccess(peerConnection) { trace(`${getPeerName(peerConnection)} addIceCandidate success.`); }; function handleConnectionFailure(peerConnection, error) { trace(`${getPeerName(peerConnection)} failed to add ICE Candidate: `+ `${error.toString()}.`); } function handleConnectionChange(event) { const peerConnection = event.target; console.log("ICE state change event: ", event); trace(`${getPeerName(peerConnection)} ICE state: ` + `${peerConnection.iceConnectionState}.`); } function gotRemoteMediaStream(event) { const mediaStream = event.stream; remoteVideo.srcObject = mediaStream; remoteStream = mediaStream; trace("Remote peer connection received remote stream."); } function createdOffer(description) { trace(`Offer from localPeerConnection: ${description.sdp}`); trace("localPeerConnection setLocalDescription start."); localPeerConnection.setLocalDescription(description) .then(() => { setLocalDescriptionSuccess(localPeerConnection); }).catch(setSessionDescriptionError); trace("remotePeerConnection setRemoteDescription start."); remotePeerConnection.setRemoteDescription(description) .then(() => { setRemoteDescriptionSuccess(remotePeerConnection); }).catch(setSessionDescriptionError); trace("remotePeerConnection createAnswer start."); remotePeerConnection.createAnswer() .then(createdAnswer) .catch(setSessionDescriptionError); } function createdAnswer(description) { trace(`Answer from remotePeerConnection: ${description.sdp}.`); trace("remotePeerConnection setLocalDescription start."); remotePeerConnection.setLocalDescription(description) .then(() => { setLocalDescriptionSuccess(remotePeerConnection); }).catch(setSessionDescriptionError); trace("localPeerConnection setRemoteDescription start."); localPeerConnection.setRemoteDescription(description) .then(() => { setRemoteDescriptionSuccess(localPeerConnection); }).catch(setSessionDescriptionError); } function setSessionDescriptionError(error) { trace(`Failed to create session description: ${error.toString()}.`); } function setLocalDescriptionSuccess(peerConnection) { setDescriptionSuccess(peerConnection, "setLocalDescription"); } function setRemoteDescriptionSuccess(peerConnection) { setDescriptionSuccess(peerConnection, "setRemoteDescription"); } function setDescriptionSuccess(peerConnection, functionName) { const peerName = getPeerName(peerConnection); trace(`${peerName} ${functionName} complete.`); } //断掉 function hangupAction() { localPeerConnection.close(); remotePeerConnection.close(); localPeerConnection = null; remotePeerConnection = null; hangupButton.disabled = true; callButton.disabled = false; trace("Ending call."); }3.源码分析
点击开始,触发startAction没什么好说的。点击调用,直接看callAction: (1)首先使用 new RTCPeerConnection 创建了两个connection const servers = null; localPeerConnection = new RTCPeerConnection(servers);
servers在这个例子中并没有用,是用来配置STUN and TURN s服务器的,先忽略。
(2)添加事件侦听,先忽略 //也可以使用onicecandidate这种写法 addEventListener("icecandidate", handleConnection); addEventListener("iceconnectionstatechange", handleConnectionChange);
(3)然后就是addStream和createOffer localPeerConnection.addStream(localStream); trace("Added local stream to localPeerConnection."); trace("localPeerConnection createOffer start."); localPeerConnection.createOffer(offerOptions) .then(createdOffer).catch(setSessionDescriptionError);
其中createOffer需要一个Options // 设置只交换视频 const offerOptions = { offerToReceiveVideo: 1, };
这里我的理解是,createOffer为了产生SDP描述,要先使用addStream把视频流加载进去才能解析。 A创建一个RTCPeerConnection对象。 A使用RTCPeerConnection .createOffer()方法产生一个offer(一个SDP会话描述)。 A用生成的offer调用setLocalDescription(),设置成自己的本地会话描述。 A将offer通过信令机制发送给B。 B用A的offer调用setRemoteDescription(),设置成自己的远端会话描述,以便他的RTCPeerConnection知道A的设置。 B调用createAnswer()生成answer B通过调用setLocalDescription()将其answer设置为本地会话描述。 B然后使用信令机制将他的answer发回给A。 A使用setRemoteDescription()将B的应答设置为远端会话描述。
上述过程可以在源码createOffer和createAnser中看到。
(4)icecandidate事件 参考https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection/icecandidate_event
当 RTCPeerConnection通过RTCPeerConnection.setLocalDescription() (en-US)方法更改本地描述之后,该RTCPeerConnection会抛出icecandidate事件。该事件的监听器需要将更改后的描述信息传送给远端RTCPeerConnection,以更新远端的备选源。
意思就是setLocalDescription被调用后,触发icecandidate事件,这一点可以在示例的console中得到验证。
image.png 4.来张流程图,转自https://segmentfault.com/a/1190000037513346
image.png
(5)addTrack,addTransceiver addStream() 已过时,官方不推荐使用.将一个MediaStream音频或视频的本地源,添加到WebRTC对等连接流对象中。官方推荐我们使用另外一个方法addTrack remotePeerConnection.ontrack = function(evt) { const mediaStream = evt.streams[0]; remoteVideo.srcObject = mediaStream; remoteStream = mediaStream; trace("Remote peer connection received remote stream."); } localStream.getTracks().forEach(track => { localPeerConnection.addTrack(track, localStream); // localPeerConnection.addTransceiver(track, {streams: [localStream]}); // 这个也可以 });
如果你是做音视频聊天相关的产品,那么addTrack 刚好能满足你的需求,毕竟需要使用到用户的摄像头、麦克风(浏览器会询问用户是否授权)。但是你只想建立音视频轨道,并不需要使用摄像头、麦克风,那我们应该怎么去做呢?
addTransceiver创建一个新的RTCRtpTransceiver并将其添加到与关联的收发器集中RTCPeerConnection。每个收发器都代表一个双向流,并带有RTCRtpSender和RTCRtpReceiver。 let rtcTransceiver = RTCPeerConnection .addTransceiver(trackOrKind,init);
(a)trackOrKind: MediaStreamTrack以与所述收发器相关联,或者一个DOMString被用作kind接收器的的track。这里视频轨道就传"video",音频轨道就传"audio" (b)init: 可选参数。如下: direction:收发器的首选方向性。此值用于初始化新RTCRtpTransceiver对象的*RTCRtpTransceiver.direction属性。 sendEncodings:从中发送RTP媒体时允许的编码列表RTCRtpSender。每个条目都是类型RTCRtpEncodingParameters。 streams: MediaStream要添加到收发器的对象列表RTCRtpReceiver;当远程对等方RTCPeerConnection的track事件发生时,这些是将由该事件指定的流。
举个例子: 添加一个单向的音视频流收发器 this.rtcPeerConnection.addTransceiver("video", { direction: "recvonly" }); this.rtcPeerConnection.addTransceiver("audio", { direction: "recvonly" });
上述代码只会接收对端发过来的音视频流,不会将自己的音视频流传输给对端。direction:
image.png 三、网络1V1示例
源码参见https://github.com/wuyawei/webrtc-stream
这个例子不再是同一个网页,所以需要借助socket.io通讯。 房间相关逻辑暂时忽略,看一下创建offer部分: socket.on("apply", data => { // 你点同意的地方 ... this.$confirm(data.self + " 向你请求视频通话, 是否同意?", "提示", { confirmButtonText: "同意", cancelButtonText: "拒绝", type: "warning" }).then(async () => { await this.createP2P(data); // 同意之后创建自己的 peer 等待对方的 offer ... // 这里不发 offer }) ... }); socket.on("reply", async data =>{ // 对方知道你点了同意的地方 switch (data.type) { case "1": // 只有这里发 offer await this.createP2P(data); // 对方同意之后创建自己的 peer this.createOffer(data); // 并给对方发送 offer break; ... } });
本例采取的是呼叫方发送 Offer,这个地方一定得注意,只要有一方创建 Offer 就可以了,因为一旦连接就是双向的。
和微信等视频通话一样,双方都需要进行媒体流输出,因为你们都要看见对方。所以这里和之前本地对等连接的区别就是都需要给自己的 RTCPeerConnection 实例添加媒体流,然后连接后各自都能拿到对方的视频流。在 初始化 RTCPeerConnection 时,记得加上 onicecandidate 函数,用以给对方发送 ICE 候选。 async createP2P(data) { this.loading = true; // loading动画 this.loadingText = "正在建立通话连接"; await this.createMedia(data); }, async createMedia(data) { ... // 获取并将本地流赋值给 video 同之前 this.initPeer(data); // 获取到媒体流后,调用函数初始化 RTCPeerConnection }, initPeer(data) { // 创建输出端 PeerConnection ... this.peer.addStream(this.localstream); // 都需要添加本地流 this.peer.onicecandidate = (event) => { // 监听ICE候选信息 如果收集到,就发送给对方 if (event.candidate) { // 发送 ICE 候选 socket.emit("1v1ICE", {account: data.self, self: this.account, sdp: event.candidate}); } }; this.peer.onaddstream = (event) => { // 监听是否有媒体流接入,如果有就赋值给 rtcB 的 src,改变相应loading状态,赋值省略 this.isToPeer = true; this.loading = false; ... }; }
createOffer 等信息交换和之前一样,只是需要通过 Socket 转发给对应的客户端。然后各自接收到消息后分别采取对应的措施。 socket.on("1v1answer", (data) =>{ // 接收到 answer this.onAnswer(data); }); socket.on("1v1ICE", (data) =>{ // 接收到 ICE this.onIce(data); }); socket.on("1v1offer", (data) =>{ // 接收到 offer this.onOffer(data); }); async createOffer(data) { // 创建并发送 offer try { // 创建offer let offer = await this.peer.createOffer(this.offerOption); // 呼叫端设置本地 offer 描述 await this.peer.setLocalDescription(offer); // 给对方发送 offer socket.emit("1v1offer", {account: data.self, self: this.account, sdp: offer}); } catch (e) { console.log("createOffer: ", e); } }, async onOffer(data) { // 接收offer并发送 answer try { // 接收端设置远程 offer 描述 await this.peer.setRemoteDescription(data.sdp); // 接收端创建 answer let answer = await this.peer.createAnswer(); // 接收端设置本地 answer 描述 await this.peer.setLocalDescription(answer); // 给对方发送 answer socket.emit("1v1answer", {account: data.self, self: this.account, sdp: answer}); } catch (e) { console.log("onOffer: ", e); } }, async onAnswer(data) { // 接收answer try { await this.peer.setRemoteDescription(data.sdp); // 呼叫端设置远程 answer 描述 } catch (e) { console.log("onAnswer: ", e); } }, async onIce(data) { // 接收 ICE 候选 try { await this.peer.addIceCandidate(data.sdp); // 设置远程 ICE } catch (e) { console.log("onAnswer: ", e); } }
挂断的思路依然是将各自的 peer 关闭,但是这里挂断方还需要借助 Socket 告诉对方,你已经挂电话了,不然对方还在痴痴地等。 hangup() { // 挂断通话 并做相应处理 对方收到消息后一样需要关闭连接 socket.emit("1v1hangup", {account: this.isCall, self: this.account}); this.peer.close(); this.peer = null; this.isToPeer = false; this.isCall = false; }