目錄css
git clone https://github.com/googlecodelabs/webrtc-web
源碼的Step01跑一下,瀏覽器獲取前置攝像頭就能成功,不展現具體效果了,看看源碼和一些其餘的應用html
源碼項目所給的代碼結構,可能是以下圖,因此常會看到js/main.js
css/main.css
這種src前端
<!-- core src code of index.html --> <head> <title>Realtime communication with WebRTC</title> <link rel="stylesheet" href="css/main.css" /> </head> <body> <h1>Realtime communication with WebRTC</h1> <!-- add video and script element in this .html file --> <video autoplay playsinline></video> <script src="js/main.js"></script> </body>
/* core src code of main.css */ body { font-family: sans-serif; } video { max-width: 100%; width: 800px; }
html
css
做爲標記型語言,瞭解其基本語法特徵與調用(我是經過閱讀DOM Sripting的前三章後比較清楚的,閱讀這部分還有一個好處是,把我不理解的簡潔代碼到頁面奇幻效果的轉化,推鍋給了DOM和瀏覽器廠商~),上面的兩個代碼就不難理解,着重分析下面js
代碼'use strict'; // On this codelab, you will be streaming only video (video: true). const mediaStreamConstraints = { video: true, }; // Video element where stream will be placed. const localVideo = document.querySelector('video'); // Local stream that will be reproduced on the video. let localStream; // Handles success by adding the MediaStream to the video element. function gotLocalMediaStream(mediaStream) { localStream = mediaStream; localVideo.srcObject = mediaStream; } // Handles error by logging a message to the console with the error message. function handleLocalMediaStreamError(error) { console.log('navigator.getUserMedia error: ', error); } // Initializes media stream. navigator.mediaDevices.getUserMedia(mediaStreamConstraints) .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);
gotLocalMediaStream()
處理視頻流函數handleLocalMediaStreamError()
異常處理函數getUserMedia()
調用navigator.mediaDevices.getUserMedia(constraints) /* produces a MediaStream */ .then(function(stream) { /* use the stream */ }) .catch(function(err) { /* handle the error */ });
先看mediaStream
的相關APIhtml5
The MediaStream interface represents a stream of media content. A stream consists of several tracks such as video or audio tracks. Each track is specified as an instance of MediaStreamTracknode
看代碼,從整個main.js
文件中,我沒有看出let localStream
有什麼特殊的用途,這一行註釋掉對網頁也沒有什麼影響(也許在以後的源碼中有用)git
但17行的代碼就至關關鍵了(能夠把這一行的代碼註釋看看是個什麼效果~獲取了媒體流,可是網頁上沒有視頻顯示)github
const localVideo = document.querySelector('video')
提及
const
只讀變量querySelector(selectors)
也正是基於樹形數據結構,來對document
中的 object
進行深度優先的前序遍歷,來獲取document
中符合selectors
的HTMLElement
並返回web
The matching is done using depth-first pre-order traversal of the document's nodes starting with the first element in the document's markup and iterating through sequential nodes by order of the number of child nodes.chrome
17行的HTMLMediaElement.srcObject 則是對'video'
流媒體的賦值,使頁面顯示videoshell
video: true
)getUserMedia()
的理解<body> <h1>Realtime communication with WebRTC</h1> <video id="localVideo" autoplay playsinline></video> <video id="remoteVideo" autoplay playsinline></video> <div> <button id="startButton">Start</button> <button id="callButton">Call</button> <button id="hangupButton">Hang Up</button> </div> <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script> <script src="js/main.js"></script> </body>
<head>
以及調用main.css
的部分和Step-01相比幾乎沒有改變video
button
script
其id
& src
命名都有很好的解釋說明效果,在下文對main.js
的分析中,相關內容會有更清楚的解釋button
開始,這是代碼183-192行// Define and add behavior to buttons. // Define action buttons. const startButton = document.getElementById('startButton'); const callButton = document.getElementById('callButton'); const hangupButton = document.getElementById('hangupButton'); // Set up initial action buttons status: disable call and hangup. callButton.disabled = true; hangupButton.disabled = true;
querySelector()
有相似功能,比較清晰// Add click event handlers for buttons. startButton.addEventListener('click', startAction); callButton.addEventListener('click', callAction); hangupButton.addEventListener('click', hangupAction);
startAction()
callAction()
hangupAction()
這三個函數startAction
開始// Handles start button action: creates local MediaStream. function startAction() { startButton.disabled = true; navigator.mediaDevices.getUserMedia(mediaStreamConstraints) .then(gotLocalMediaStream).catch(handleLocalMediaStreamError); trace('Requesting local stream.'); }
startButton
只能click一次,以後獲取getUserMedia()
mediaStreamConstraints()
函數幾乎沒有變化,gotLocalMediaStream()
& handleLocalMediaStreamError()
有些許變化,在19-43部分行// Define peer connections, streams and video elements. const localVideo = document.getElementById('localVideo'); const remoteVideo = document.getElementById('remoteVideo'); let localStream; let remoteStream; // Define MediaStreams callbacks. // Sets the MediaStream as the video element src. function gotLocalMediaStream(mediaStream) { localVideo.srcObject = mediaStream; localStream = mediaStream; trace('Received local stream.'); callButton.disabled = false; // Enable call button. } // Handles error by logging a message to the console. function handleLocalMediaStreamError(error) { trace(`navigator.getUserMedia error: ${error.toString()}.`); }
trace()
函數比較新穎,看看// Logs an action (text) and the time when it happened on the console. function trace(text) { text = text.trim(); const now = (window.performance.now() / 1000).toFixed(3); console.log(now, text); }
callAction()
,代碼203-246行// Code from line 16-17 // Define initial start time of the call (defined as connection between peers). let startTime = null; // Code from line19-27 // Define peer connections let localPeerConnection; let remotePeerConnection; // Handles call button action: creates peer connection. function callAction() { callButton.disabled = true; // disenable call button hangupButton.disabled = false; // enable hangup button trace('Starting call.'); startTime = window.performance.now(); // assign startTime with concrete time // Get local media stream tracks. 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; // Allows for RTC server configuration. // Create peer connections and add behavior. 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); // Add local stream to connection and create offer to connect. localPeerConnection.addStream(localStream); trace('Added local stream to localPeerConnection.'); trace('localPeerConnection createOffer start.'); localPeerConnection.createOffer(offerOptions) .then(createdOffer).catch(setSessionDescriptionError); }
RTCPeerConnection
,解析下面所說的三個步驟,以創建鏈接時序展開Setting up a call between WebRTC peers involves three tasks:
- Create a RTCPeerConnection for each end of the call and, at each end, add the local stream from getUserMedia().
- Get and share network information: potential connection endpoints are known as ICE candidates.
- Get and share local and remote descriptions: metadata about local media in SDP format.
getUserMedia()
部分,再也不贅述let localPeerConnection; const servers = null; // Allows for RTC server configuration. This is where you could specify STUN and TURN servers. // Create peer connections and add behavior. localPeerConnection = new RTCPeerConnection(servers); remotePeerConnection = new RTCPeerConnection(servers);
// Add local stream to connection and create offer to connect. localPeerConnection.addStream(localStream); trace('Added local stream to localPeerConnection.');
addStream()
以後,能夠認爲Local & Remote Peer已經所有建好(RTCPeerConnection實例化成功,media傳輸也能夠開始進行)localPeerConnection.addEventListener('icecandidate', handleConnection); localPeerConnection.addEventListener( 'iceconnectionstatechange', handleConnectionChange);
addEventListener()
method在button相關中已經瞭解,關於'icecandidate'
Event,看RTCPeerConnection: icecandidate event,而其中的setLocalDescription()
在下面一個section中有介紹remotePeerConnection.addEventListener('icecandidate', handleConnection); remotePeerConnection.addEventListener( 'iceconnectionstatechange', handleConnectionChange); remotePeerConnection.addEventListener('addstream', gotRemoteMediaStream);
// Connects with new peer candidate. 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:\n` + `${event.candidate.candidate}.`); } } // Logs changes to the connection state. function handleConnectionChange(event) { const peerConnection = event.target; console.log('ICE state change event: ', event); trace(`${getPeerName(peerConnection)} ICE state: ` + `${peerConnection.iceConnectionState}.`); } // Handles remote MediaStream success by adding it as the remoteVideo src. function gotRemoteMediaStream(event) { const mediaStream = event.stream; remoteVideo.srcObject = mediaStream; remoteStream = mediaStream; trace('Remote peer connection received remote stream.'); }
gotRemoteMediaStream()
函數,最終將Local Peer的addStream()
顯示trace('localPeerConnection createOffer start.'); localPeerConnection.createOffer(offerOptions) .then(createdOffer).catch(setSessionDescriptionError);
offerOptions
createdOffer()
setSessionDescriptionError()
這三個對應內容// Set up to exchange only video. const offerOptions = { offerToReceiveVideo: 1, }; // Logs offer creation and sets peer connection session descriptions. function createdOffer(description) { trace(`Offer from localPeerConnection:\n${description.sdp}`); trace('localPeerConnection setLocalDescription start.'); localPeerConnection.setLocalDescription(description) .then(() => { // The parameter list for a function with no parameters should be written with a pair of parentheses. setLocalDescriptionSuccess(localPeerConnection); // just logs successful info on the console }).catch(setSessionDescriptionError); } // Logs error when setting session description fails. function setSessionDescriptionError(error) { trace(`Failed to create session description: ${error.toString()}.`); }
createOffer()
函數setLocalDescription
,API中也沒有講的特別清楚,簡單的說,能夠認爲這個函數通過調用後,Local Peer的offer就發送成功(可參見RTCPeerConnection.signalingState),但實際上發送的信息是什麼、向誰發...等一系列問題,都是在官方教程中的源碼裏面未涉及的,這部分我寫在了WebRTC的RTCPeerConnection()原理探析中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:\n${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); }
setRemoteDescription
的API,Local Peer與Remote Peer之間的互相通訊基本創建了hangup
button對應什麼函數// Handles hangup action: ends up call, closes connections and resets peers. function hangupAction() { localPeerConnection.close(); remotePeerConnection.close(); localPeerConnection = null; remotePeerConnection = null; hangupButton.disabled = true; callButton.disabled = false; trace('Ending call.'); }
<textarea id="dataChannelSend" disabled placeholder="Press Start, enter some text, then press Send."></textarea> <textarea id="dataChannelReceive" disabled></textarea>
var startButton = document.querySelector('button#startButton'); var sendButton = document.querySelector('button#sendButton'); var closeButton = document.querySelector('button#closeButton'); startButton.onclick = createConnection; sendButton.onclick = sendData; closeButton.onclick = closeDataChannels;
var localConnection; var remoteConnection; var sendChannel; var dataConstraint; var dataChannelSend = document.querySelector('textarea#dataChannelSend'); // Offerer side function createConnection() { dataChannelSend.placeholder = ''; var servers = null; pcConstraint = null; dataConstraint = null; trace('Using SCTP based data channels'); // For SCTP, reliable and ordered delivery is true by default. // Add localConnection to global scope to make it visible // from the browser console. window.localConnection = localConnection = new RTCPeerConnection(servers, pcConstraint); // constructor trace('Created local peer connection object localConnection'); sendChannel = localConnection.createDataChannel('sendDataChannel', dataConstraint); trace('Created send data channel'); localConnection.onicecandidate = iceCallback1; sendChannel.onopen = onSendChannelStateChange; sendChannel.onclose = onSendChannelStateChange; // Add remoteConnection to global scope to make it visible // from the browser console. window.remoteConnection = remoteConnection = new RTCPeerConnection(servers, pcConstraint); trace('Created remote peer connection object remoteConnection'); remoteConnection.onicecandidate = iceCallback2; remoteConnection.ondatachannel = receiveChannelCallback; localConnection.createOffer().then( gotDescription1, onCreateSessionDescriptionError ); startButton.disabled = true; closeButton.disabled = false; } function iceCallback1(event) { trace('local ice callback'); if (event.candidate) { remoteConnection.addIceCandidate( event.candidate ).then( onAddIceCandidateSuccess, onAddIceCandidateError ); trace('Local ICE candidate: \n' + event.candidate.candidate); } } function iceCallback2(event) { trace('remote ice callback'); if (event.candidate) { localConnection.addIceCandidate( event.candidate ).then( // print out info on the console onAddIceCandidateSuccess, onAddIceCandidateError ); trace('Remote ICE candidate: \n ' + event.candidate.candidate); } } function onSendChannelStateChange() { var readyState = sendChannel.readyState; trace('Send channel state is: ' + readyState); if (readyState === 'open') { dataChannelSend.disabled = false; dataChannelSend.focus(); sendButton.disabled = false; closeButton.disabled = false; } else { dataChannelSend.disabled = true; sendButton.disabled = true; closeButton.disabled = true; } }
先看幾個API,RTCPeerConnection.createDataChannel()
The createDataChannel() method on the RTCPeerConnection interface creates a new channel linked with the remote peer, over which any kind of data may be transmitted. This can be useful for back-channel content such as images, file transfer, text chat, game update packets, and so forth.
RTCPeerConnection.onicecandidate
This happens whenever the local ICE agent needs to deliver a message to the other peer through the signaling server. This lets the ICE agent perform negotiation with the remote peer without the browser itself needing to know any specifics about the technology being used for signaling; simply implement this method to use whatever messaging technology you choose to send the ICE candidate to the remote peer.
The read-only property candidate on the RTCIceCandidate interface returns a DOMString describing the candidate in detail. Most of the other properties of RTCIceCandidate are actually extracted from this string.
When a web site or app using RTCPeerConnection receives a new ICE candidate from the remote peer over its signaling channel, it delivers the newly-received candidate to the browser's ICE agent by calling RTCPeerConnection.addIceCandidate(). This adds this new remote candidate to the RTCPeerConnection's remote description, which describes the state of the remote end of the connection.
The RTCDataChannel.onopen property is an EventHandler which specifies a function to be called when the open event is fired; this is a simple Event which is sent when the data channel's underlying data transport—the link over which the RTCDataChannel's messages flow—is established or re-established.
The HTMLElement.focus() method sets focus on the specified element, if it can be focused. The focused element is the element which will receive keyboard and similar events by default.
createOffer()
createAnswer()
部分,在學習完RTCPeerConnection以後,是很是容易的,因此不贅述function sendData() { var data = dataChannelSend.value; sendChannel.send(data); trace('Sent Data: ' + data); }
function closeDataChannels() { trace('Closing data channels'); sendChannel.close(); trace('Closed data channel with label: ' + sendChannel.label); receiveChannel.close(); trace('Closed data channel with label: ' + receiveChannel.label); localConnection.close(); remoteConnection.close(); localConnection = null; remoteConnection = null; trace('Closed peer connections'); startButton.disabled = false; sendButton.disabled = true; closeButton.disabled = true; dataChannelSend.value = ''; dataChannelReceive.value = ''; dataChannelSend.disabled = true; disableSendButton(); enableStartButton(); } function enableStartButton() { startButton.disabled = false; } function disableSendButton() { sendButton.disabled = true; }