摆脱客户端?网页发起直播势在必行!
背景
近幾年直播行業飛速發展,但是由于Web端這方面功能的長時間缺失,使得直播端以客戶端為主;WebRTC 的出現使得網頁也可以成為直播端。那么究竟WebRTC是什么呢?
WebRTC,即Web Real-Time Communication,web實時通信技術。簡單地說就是在web瀏覽器里面引入實時通信,包括音視頻通話等,它使得實時通信變成一種標準功能,任何Web應用都無需借助第三方插件和專有軟件,而是通過JavaScript API即可完成;而且WebRTC提供了視頻會議的核心技術,包括音視頻的采集、編解碼、網絡傳輸、展示等功能,還支持跨平臺,包括主流的PC和移動端設備。
下面介紹下需要用到的幾個API:
getUserMedia
我們可以通過調用navigator.mediaDevices.getUserMedia(constraints)去初始化一個本地的音視頻流,然后把直播流通過video標簽播放。代碼如下:
html:
<div id="container"><video id="gum-local" autoplay playsinline></video><button id="showVideo">Open camera</button><button id="switchVideo">switch camera</button> </div>js:
const constraints = {audio: false,video: true };async function init(e) {try {const stream = await navigator.mediaDevices.getUserMedia(constraints);const video = document.querySelector('video');video.srcObject = stream;} catch (e) {console.log(e, 'stream init error');} } document.querySelector('#showVideo').addEventListener('click', (e) => init(e));示例效果:
當然,如果有多個設備,就需要考慮設備選擇和設備切換的問題。那就需要用到下面的這個API。
設備
我們看看如何用原生的Web API去獲取設備(以下示例代碼可適用于Chrome,其他瀏覽器暫未測試;具體瀏覽器兼容性可參考官方文檔,本文檔底部有鏈接)。
navigator.mediaDevices.enumerateDevices()
如果枚舉成功將會返回一個包含MediaDeviceInfo實例的數組,它包含了可用的多媒體輸入輸出設備的信息。
下面是調用代碼示例。
navigator.mediaDevices.enumerateDevices().then((devices) => {console.log(devices, '-----enumerateDevices------'); });設備參數說明:
獲取的所有設備截圖(未授權):
deviceInfo--所有設備信息(未授權)
videoinput已授權截圖:
videoinput授權后截圖
獲取到設備列表后,可設置navigator.mediaDevices.getUserMedia(constraints)的constraints參數選擇所用設備。
const { audioList, videoList } = await getDevices(); const constraints = {audio: {deviceId: audioList[0].deviceId},video: {deviceId: videoList[0].deviceId} }; navigator.mediaDevices.getUserMedia(constraints); ...然而,我們在更換deviceId切換設備的時候發現一些異常情況。在某些deviceId之間切換時,攝像頭畫面或者是麥克風采集處并沒有發生變化。進一步調試發現,這些切換后沒有發生變化的deviceId都具有相同的groupId。因此,相同groupId下的設備,選擇一個用于切換即可。
篩選麥克風、攝像頭設備示例:
function getDevices() {return new Promise((resolve) => {navigator.mediaDevices.enumerateDevices().then((devices) => {const audioGroup = {};const videoGroup = {};const cameraList = [];const micList = [];devices.forEach((device, index) => {if ((!device.groupId || !audioGroup[device.groupId]) && device.kind === 'audioinput') {micList.push(device);audioGroup[device.groupId] = true;}if ((!device.groupId || !videoGroup[device.groupId]) && device.kind === 'videoinput') {cameraList.push(device);videoGroup[device.groupId] = true;}});resolve({ cameraList, micList });});}); }注意:在Chrome下,電腦外接攝像頭后拔出設備,此時還有可能獲取到拔出的設備信息,在進行切換的時候會有問題,可以采用在頁面進行友好提示處理這種情況。
屏幕共享
MediaDevices.getDisplayMedia
Chrome 72+、Firefox 66+版本已經實現了WebRTC規范中的MediaDevices.getDisplayMedia,具備屏幕共享功能。
navigator.mediaDevices.getDisplayMedia({video: true,audio: false }).then(stream => {video.srcObject = stream; }).catch(err => {console.error(err); });示例效果:
對于Chrome 72以下的版本,想要實現屏幕共享的功能需要借助Chrome插件去獲取screen(顯示器屏幕)、application windows(應用窗口)和browser tabs(瀏覽器標簽頁)。 Chrome插件:由manifest.json和script.js組成。
manifest.json 填入一些基本數據。
background中scripts傳入需執行的js文件。添加permissions: ['desktopCapture'],用來開啟屏幕共享的權限。externally_connectable用來聲明哪些應用和網頁可以通過`runtime.connect`和`runtime.sendMessage`連接到插件。 {"manifest_version": 2,"name": "Polyv Web Screensharing","permissions": [ "desktopCapture" ],"version": "0.0.1","background": {"persistent": false,"scripts": [ "script.js" ]},"externally_connectable": {"matches": ["*://localhost:*/*"]} }script.js
// script.js chrome.runtime.onMessageExternal.addListener(function(request, sender, sendResponse) {if (request.getStream) {// Gets chrome media stream token and returns it in the response.chrome.desktopCapture.chooseDesktopMedia(['screen', 'window', 'tab'], sender.tab,function(streamId) {sendResponse({ streamId: streamId });});return true; // Preserve sendResponse for future use}} );在頁面中開始屏幕共享。通過chrome.runtime.sendMessage發送消息到Chrome插件調起屏幕共享。獲取到streamId后,通過mediaDevices.getUserMedia得到stream。
const EXTENSION_ID = '<EXTENSION_ID>'; const video = $('#videoId'); chrome.runtime.sendMessage(EXTENSION_ID, { getStream: true }, res => {console.log('res: ', res);if (res.streamId) {navigator.mediaDevices.getUserMedia({video: {mandatory: {chromeMediaSource: 'desktop',chromeMediaSourceId: res.streamId}}}).then((stream) => {video.srcObject = stream;video.onloadedmetadata = function(e) {video.play();};})} else {// 取消選擇} });而Firefox 66版本以下,不需要像Chrome借助插件才能實現屏幕共享。Firefox 33之后可以直接通過使用mediaDevices.getUserMedia,指定約束對象mediaSource為screen、window、application來實現屏幕共享。不過在Firefox中,一次只能指定一種mediaSource。
navigator.mediaDevices.getUserMedia({video: {mediaSource: 'window' } }).then(stream => {video.srcObject = stream; });傳輸
WebRTC的RTCPeerConnection可以建立點對點連接通信,RTCDataChannel提供了數據通信的能力。
WebRTC的點對點連接的過程為:
RTCDataChannel提供了send方法和message事件。使用起來與WebSocket類似。
由于沒有服務器,以下代碼為呼叫端和接收端在同一頁面上,RTCPeerConnection對象之間是如何進行數據交互。
// 創建數據通道 sendChannel = localConnection.createDataChannel('通道名稱', options); sendChannel.binaryType = 'arraybuffer';sendChannel.onopen = function() {sendChannel.send('Hi there!'); }; sendChannel.onmessage = function(evt) {console.log('send channel onmessage: ', evt.data); };// 遠端接收實例 remoteConnection = new RTCPeerConnection(servers); remoteConnection.onicecandidate = function(evt) {if (evt.candidate) {localConnection.addIceCandidate(new RTCIceCandidate(evt.candidate));} }; // 當一個RTC數據通道已被遠端調用createDataChannel()添加到連接中時觸發 remoteConnection.ondatachannel = function() {const receiveChannel = event.channel;receiveChannel.binaryType = 'arraybuffer';//接收到數據時觸發receiveChannel.onmessage = function(evt) {console.log('onmessage', evt.data); // log: Hi there!};receiveChannel.send('Nice!'); };// 監聽是否有媒體流 remoteConnection.onaddstream = function(e) {peerVideo.srcObject = e.stream; };localConnection.addStream(stream);// 創建呼叫實例 localConnection.createOffer().then(offer => {localConnection.setLocalDescription(offer);remoteConnection.setRemoteDescription(offer);remoteConnection.createAnswer().then(answer => {remoteConnection.setLocalDescription(answer);// 接收到answerlocalConnection.setRemoteDescription(answer);}) });至此我們已經介紹完畢瀏覽器設備檢測采集和屏幕分享的基本流程,但是光有這些可還遠遠不夠,一套完整的直播體系包括音視頻采集、處理、編碼和封裝、推流到服務器、服務器流分發、播放器流播放等等。如果想節省開發成本,可以使用第三方SDK。下面簡單介紹下使用聲網SDK發起直播的流程。
瀏覽器要求:
設備檢測
調用AgoraRTC.getDevices獲取當前瀏覽器檢測到的所有可枚舉設備,kind為'videoinput'是攝像頭設備,kind為'audioinput'是麥克風設備,然后通過createStream初始化一個本地的流。 獲取設備:
AgoraRTC.getDevices((devices) => {const audioGroup = {};const videoGroup = {};const cameraList = [];const micList = [];devices.forEach((device, index) => {if ((!device.groupId || !audioGroup[device.groupId]) && device.kind === 'audioinput') {micList.push(device);audioGroup[device.groupId] = true;}if ((!device.groupId || !videoGroup[device.groupId]) && device.kind === 'videoinput') {cameraList.push(device);videoGroup[device.groupId] = true;}});return { cameraList, micList }; });初始化本地流:
// uid:自定義頻道號,cameraId設備Id const stream = AgoraRTC.createStream({streamID: uid,audio: false,video: true,cameraId: cameraId,microphoneId: microphoneId }); stream.init(() => {// clientCamera <div id="clientCamera" ></div>stream.play('clientCamera', { muted: true }); }, err => {console.error('AgoraRTC client init failed', err); });stream.init()初始化直播流;如果當前瀏覽器攝像頭權限為禁止,則調用失敗,可捕獲報錯Media access NotAllowedError: Permission denied; 若攝像頭權限為詢問,瀏覽器默認彈窗是否允許使用攝像頭,允許后調用play()可看到攝像頭捕獲的畫面。 如果不傳入cameraId,SDK會默認獲取到設備的deviceId,如果權限是允許,同樣會顯示攝像頭畫面。
采集
攝像頭
順利拿到cameraId和microphoneId后就可以進行直播。通過SDK提供的createStream創建一個音視頻流對象。執行init方法初始化成功之后,播放音視頻(見上文)。最后通過client發布流以及推流到CDN(見下文)。
屏幕共享
Web 端屏幕共享,通過創建一個屏幕共享的流來實現的。Chrome屏幕共享需要下載插件,在創建的流的時候還需要傳入插件的extensionId。
const screenStream = AgoraRTC.createStream({streamID: <uid>,audio: false,video: false,screen: true,extensionId: <extensionId>, // Chrome 插件idmediaSource: 'screen' // Firefox });傳輸
通過AgoraRTC.createStream創建的音視頻流,通過publish發送到第三方服務商的SD-RTN(軟件定義實時傳輸網絡)。
client.publish(screenStream, err => {console.error(err); });別的瀏覽器可以通過監聽到stream-added事件,通過subscribe訂閱遠端音視頻流。
client.on('stream-added', evt => {const stream = evt.stream;client.subscribe(stream, err => {console.error(err);}); });再通過startLiveStreaming推流到CDN。
// 編碼 client.setLiveTranscoding(<coding>); client.startLiveStreaming(<url>, true)在推攝像頭流的時候,關閉攝像頭,需要推一張占位圖。這個時候先用canvas畫圖,然后用WebRTC提供的captureStream捕獲靜態幀。再調用getVideoTracks,制定AgoraRTC.createStream的videoSource為該值。視頻源如來自 canvas,需要在 canvas 內容不變時,每隔 1 秒重新繪制 canvas 內容,以保持視頻流的正常發布。
const canvas = document.createElement('canvas'); renderCanvas(canvas); setInterval(() => {renderCanvas(canvas); }, 1000); canvasStream = canvas.captureStream();const picStream = AgoraRTC.createStream({streamID: <uid>,video: true,audio: false,videoSource: canvasStream.getVideoTracks()[0] });// 畫圖 function renderCanvas(canvas) {... }一個client只能推一個流,所以在進行屏幕共享的時候,需要創建兩個client,一個發送屏幕共享流,一個發送視頻流。屏幕共享流的video字段設為false。視頻流的video字段設為true。然后先通過setLiveTranscoding合圖再推流。
const users = [{x: 0, // 視頻幀左上角的橫軸位置,默認為0y: 0, // 視頻幀左上角的縱軸位置,默認為0width: 1280, // 視頻幀寬度,默認為640height: 720, // 視頻幀高度,默認為360zOrder: 0, // 視頻幀所處層數;取值范圍為 [0,100];默認值為 0,表示該區域圖像位于最下層alpha: 1.0, // 視頻幀的透明度,默認值為 1.0uid: 888888, // 旁路推流的用戶 ID},{x: 0,y: 0,width: 1280,height: 720,zOrder: 1,alpha: 1.0,uid: 999999} ];var liveTranscoding = {width: 640,height: 360,videoBitrate: 400,videoFramerate: 15,lowLatency: false,audioSampleRate: AgoraRTC.AUDIO_SAMPLE_RATE_48000,audioBitrate: 48,audioChannels: 1,videoGop: 30,videoCodecProfile: AgoraRTC.VIDEO_CODEC_PROFILE_HIGH,userCount: user.length,backgroundColor: 0x000000,transcodingUsers: users, }; client.setLiveTranscoding(liveTranscoding);因為業務需求是攝像頭和屏幕共享可以切換,攝像頭和屏幕共享的分辨率和碼率均不相同,屏幕共享需要更高的分辨率和碼率。但是開發中發現切換時設置碼率無效。SDK那邊給的答復是:因為緩存問題,會以第一次推流設置的參數為準,將會在下個版本中修復。
總結
以上是生活随笔為你收集整理的摆脱客户端?网页发起直播势在必行!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ubuntu 右键选单没有创建文档
- 下一篇: Delphi制作带图标的弹出式选单