摘要:背景近幾年直播行業(yè)飛速發(fā)展,但是由于端這方面功能的長時間缺失,使得直播端以客戶端為主的出現(xiàn)使得網(wǎng)頁也可以成為直播端。通過發(fā)送消息到插件調(diào)起屏幕共享。的點(diǎn)對點(diǎn)連接的過程為呼叫端給接收端發(fā)送一個信息。下面簡單介紹下使用聲網(wǎng)發(fā)起直播的流程。
背景
近幾年直播行業(yè)飛速發(fā)展,但是由于Web端這方面功能的長時間缺失,使得直播端以客戶端為主;WebRTC 的出現(xiàn)使得網(wǎng)頁也可以成為直播端。那么究竟WebRTC是什么呢?
WebRTC,即Web Real-Time Communication,web實(shí)時通信技術(shù)。簡單地說就是在web瀏覽器里面引入實(shí)時通信,包括音視頻通話等,它使得實(shí)時通信變成一種標(biāo)準(zhǔn)功能,任何Web應(yīng)用都無需借助第三方插件和專有軟件,而是通過JavaScript API即可完成;而且WebRTC提供了視頻會議的核心技術(shù),包括音視頻的采集、編解碼、網(wǎng)絡(luò)傳輸、展示等功能,還支持跨平臺,包括主流的PC和移動端設(shè)備。
下面介紹下需要用到的幾個API:
getUserMedia我們可以通過調(diào)用navigator.mediaDevices.getUserMedia(constraints)去初始化一個本地的音視頻流,然后把直播流通過video標(biāo)簽播放。代碼如下:
html:
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));
示例效果:
當(dāng)然,如果有多個設(shè)備,就需要考慮設(shè)備選擇和設(shè)備切換的問題。那就需要用到下面的這個API。
設(shè)備我們看看如何用原生的Web API去獲取設(shè)備(以下示例代碼可適用于Chrome,其他瀏覽器暫未測試;具體瀏覽器兼容性可參考官方文檔,本文檔底部有鏈接)。
navigator.mediaDevices.enumerateDevices()如果枚舉成功將會返回一個包含MediaDeviceInfo實(shí)例的數(shù)組,它包含了可用的多媒體輸入輸出設(shè)備的信息。
下面是調(diào)用代碼示例。
navigator.mediaDevices.enumerateDevices().then((devices) => { console.log(devices, "-----enumerateDevices------"); });
設(shè)備參數(shù)說明:
deviceId:設(shè)備id,具有唯一性
groupId:設(shè)備組id,不具有唯一性
kind:設(shè)備類別(audioinput:音頻輸入設(shè)備,audiooutput:音頻輸出設(shè)備,videoinput:視頻輸入設(shè)備)
label:設(shè)備名稱(未經(jīng)過授權(quán)允許的設(shè)備,label值為空,授權(quán)允許后可拿到label的值,如下兩圖所示)
獲取的所有設(shè)備截圖(未授權(quán)):
videoinput已授權(quán)截圖:
獲取到設(shè)備列表后,可設(shè)置navigator.mediaDevices.getUserMedia(constraints)的constraints參數(shù)選擇所用設(shè)備。
const { audioList, videoList } = await getDevices(); const constraints = { audio: { deviceId: audioList[0].deviceId }, video: { deviceId: videoList[0].deviceId } }; navigator.mediaDevices.getUserMedia(constraints);
然而,我們在更換deviceId切換設(shè)備的時候發(fā)現(xiàn)一些異常情況。在某些deviceId之間切換時,攝像頭畫面或者是麥克風(fēng)采集處并沒有發(fā)生變化。進(jìn)一步調(diào)試發(fā)現(xiàn),這些切換后沒有發(fā)生變化的deviceId都具有相同的groupId。因此,相同groupId下的設(shè)備,選擇一個用于切換即可。
篩選麥克風(fēng)、攝像頭設(shè)備示例:
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下,電腦外接攝像頭后拔出設(shè)備,此時還有可能獲取到拔出的設(shè)備信息,在進(jìn)行切換的時候會有問題,可以采用在頁面進(jìn)行友好提示處理這種情況。
屏幕共享 MediaDevices.getDisplayMediaChrome 72+、Firefox 66+版本已經(jīng)實(shí)現(xiàn)了WebRTC規(guī)范中的MediaDevices.getDisplayMedia,具備屏幕共享功能。
navigator.mediaDevices.getDisplayMedia({ video: true, audio: false }).then(stream => { video.srcObject = stream; }).catch(err => { console.error(err); });
示例效果:
對于Chrome 72以下的版本,想要實(shí)現(xiàn)屏幕共享的功能需要借助Chrome插件去獲取screen(顯示器屏幕)、application windows(應(yīng)用窗口)和browser tabs(瀏覽器標(biāo)簽頁)。 Chrome插件:由manifest.json和script.js組成。
manifest.json 填入一些基本數(shù)據(jù)。
background中scripts傳入需執(zhí)行的js文件。
添加permissions: ["desktopCapture"],用來開啟屏幕共享的權(quán)限。
externally_connectable用來聲明哪些應(yīng)用和網(wǎng)頁可以通過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發(fā)送消息到Chrome插件調(diào)起屏幕共享。獲取到streamId后,通過mediaDevices.getUserMedia得到stream。
const 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借助插件才能實(shí)現(xiàn)屏幕共享。Firefox 33之后可以直接通過使用mediaDevices.getUserMedia,指定約束對象mediaSource為screen、window、application來實(shí)現(xiàn)屏幕共享。不過在Firefox中,一次只能指定一種mediaSource。
navigator.mediaDevices.getUserMedia({ video: { mediaSource: "window" } }).then(stream => { video.srcObject = stream; });傳輸
WebRTC的RTCPeerConnection可以建立點(diǎn)對點(diǎn)連接通信,RTCDataChannel提供了數(shù)據(jù)通信的能力。
WebRTC的點(diǎn)對點(diǎn)連接的過程為:
呼叫端給接收端發(fā)送一個offer信息。在發(fā)送給接收端之前先調(diào)用setLocalDescription存儲本地offer描述。
接收端收到offer消息后,先調(diào)用setRemoteDescription存儲遠(yuǎn)端offer,再創(chuàng)建一個answer信息給呼叫端。
RTCDataChannel提供了send方法和message事件。使用起來與WebSocket類似。
由于沒有服務(wù)器,以下代碼為呼叫端和接收端在同一頁面上,RTCPeerConnection對象之間是如何進(jìn)行數(shù)據(jù)交互。
// 創(chuàng)建數(shù)據(jù)通道 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); }; // 遠(yuǎn)端接收實(shí)例 remoteConnection = new RTCPeerConnection(servers); remoteConnection.onicecandidate = function(evt) { if (evt.candidate) { localConnection.addIceCandidate(new RTCIceCandidate(evt.candidate)); } }; // 當(dāng)一個RTC數(shù)據(jù)通道已被遠(yuǎn)端調(diào)用createDataChannel()添加到連接中時觸發(fā) remoteConnection.ondatachannel = function() { const receiveChannel = event.channel; receiveChannel.binaryType = "arraybuffer"; //接收到數(shù)據(jù)時觸發(fā) receiveChannel.onmessage = function(evt) { console.log("onmessage", evt.data); // log: Hi there! }; receiveChannel.send("Nice!"); }; // 監(jiān)聽是否有媒體流 remoteConnection.onaddstream = function(e) { peerVideo.srcObject = e.stream; }; localConnection.addStream(stream); // 創(chuàng)建呼叫實(shí)例 localConnection.createOffer().then(offer => { localConnection.setLocalDescription(offer); remoteConnection.setRemoteDescription(offer); remoteConnection.createAnswer().then(answer => { remoteConnection.setLocalDescription(answer); // 接收到answer localConnection.setRemoteDescription(answer); }) });
至此我們已經(jīng)介紹完畢瀏覽器設(shè)備檢測采集和屏幕分享的基本流程,但是光有這些可還遠(yuǎn)遠(yuǎn)不夠,一套完整的直播體系包括音視頻采集、處理、編碼和封裝、推流到服務(wù)器、服務(wù)器流分發(fā)、播放器流播放等等。如果想節(jié)省開發(fā)成本,可以使用第三方SDK。下面簡單介紹下使用聲網(wǎng)SDK發(fā)起直播的流程。
瀏覽器要求:
Chrome 58+
Firefox 56+
Safari 11+(屏幕共享不可用)
Opera 45+(屏幕共享不可用)
QQ 10+(屏幕共享不可用)
360 安全瀏覽器 9.1+(屏幕共享不可用)
設(shè)備檢測調(diào)用AgoraRTC.getDevices獲取當(dāng)前瀏覽器檢測到的所有可枚舉設(shè)備,kind為"videoinput"是攝像頭設(shè)備,kind為"audioinput"是麥克風(fēng)設(shè)備,然后通過createStream初始化一個本地的流。 獲取設(shè)備:
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設(shè)備Id const stream = AgoraRTC.createStream({ streamID: uid, audio: false, video: true, cameraId: cameraId, microphoneId: microphoneId }); stream.init(() => { // clientCamera stream.play("clientCamera", { muted: true }); }, err => { console.error("AgoraRTC client init failed", err); });
stream.init()初始化直播流;如果當(dāng)前瀏覽器攝像頭權(quán)限為禁止,則調(diào)用失敗,可捕獲報錯Media access NotAllowedError: Permission denied; 若攝像頭權(quán)限為詢問,瀏覽器默認(rèn)彈窗是否允許使用攝像頭,允許后調(diào)用play()可看到攝像頭捕獲的畫面。 如果不傳入cameraId,SDK會默認(rèn)獲取到設(shè)備的deviceId,如果權(quán)限是允許,同樣會顯示攝像頭畫面。
采集 攝像頭順利拿到cameraId和microphoneId后就可以進(jìn)行直播。通過SDK提供的createStream創(chuàng)建一個音視頻流對象。執(zhí)行init方法初始化成功之后,播放音視頻(見上文)。最后通過client發(fā)布流以及推流到CDN(見下文)。
屏幕共享Web 端屏幕共享,通過創(chuàng)建一個屏幕共享的流來實(shí)現(xiàn)的。Chrome屏幕共享需要下載插件,在創(chuàng)建的流的時候還需要傳入插件的extensionId。
const screenStream = AgoraRTC.createStream({ streamID:傳輸, audio: false, video: false, screen: true, extensionId: , // Chrome 插件id mediaSource: "screen" // Firefox });
通過AgoraRTC.createStream創(chuàng)建的音視頻流,通過publish發(fā)送到第三方服務(wù)商的SD-RTN(軟件定義實(shí)時傳輸網(wǎng)絡(luò))。
client.publish(screenStream, err => { console.error(err); });
別的瀏覽器可以通過監(jiān)聽到stream-added事件,通過subscribe訂閱遠(yuǎn)端音視頻流。
client.on("stream-added", evt => { const stream = evt.stream; client.subscribe(stream, err => { console.error(err); }); });
再通過startLiveStreaming推流到CDN。
// 編碼 client.setLiveTranscoding(); client.startLiveStreaming( , true)
在推攝像頭流的時候,關(guān)閉攝像頭,需要推一張占位圖。這個時候先用canvas畫圖,然后用WebRTC提供的captureStream捕獲靜態(tài)幀。再調(diào)用getVideoTracks,制定AgoraRTC.createStream的videoSource為該值。視頻源如來自 canvas,需要在 canvas 內(nèi)容不變時,每隔 1 秒重新繪制 canvas 內(nèi)容,以保持視頻流的正常發(fā)布。
const canvas = document.createElement("canvas"); renderCanvas(canvas); setInterval(() => { renderCanvas(canvas); }, 1000); canvasStream = canvas.captureStream(); const picStream = AgoraRTC.createStream({ streamID:, video: true, audio: false, videoSource: canvasStream.getVideoTracks()[0] }); // 畫圖 function renderCanvas(canvas) { ... }
一個client只能推一個流,所以在進(jìn)行屏幕共享的時候,需要創(chuàng)建兩個client,一個發(fā)送屏幕共享流,一個發(fā)送視頻流。屏幕共享流的video字段設(shè)為false。視頻流的video字段設(shè)為true。然后先通過setLiveTranscoding合圖再推流。
const users = [ { x: 0, // 視頻幀左上角的橫軸位置,默認(rèn)為0 y: 0, // 視頻幀左上角的縱軸位置,默認(rèn)為0 width: 1280, // 視頻幀寬度,默認(rèn)為640 height: 720, // 視頻幀高度,默認(rèn)為360 zOrder: 0, // 視頻幀所處層數(shù);取值范圍為 [0,100];默認(rèn)值為 0,表示該區(qū)域圖像位于最下層 alpha: 1.0, // 視頻幀的透明度,默認(rèn)值為 1.0 uid: 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);
因?yàn)闃I(yè)務(wù)需求是攝像頭和屏幕共享可以切換,攝像頭和屏幕共享的分辨率和碼率均不相同,屏幕共享需要更高的分辨率和碼率。但是開發(fā)中發(fā)現(xiàn)切換時設(shè)置碼率無效。SDK那邊給的答復(fù)是:因?yàn)榫彺鎲栴},會以第一次推流設(shè)置的參數(shù)為準(zhǔn),將會在下個版本中修復(fù)。
參考文獻(xiàn):
MediaDevices.getUserMedia()
MedaiDevices.enumerateDevices()
HTMLMediaElement
MediaDevices/getDisplayMedia
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/104674.html
摘要:低代碼技術(shù)近期受到了廣泛的關(guān)注。談到低代碼,答案有兩部分當(dāng)今組織敏捷性勢在必行敏捷和快速有效地響應(yīng)內(nèi)外部變化的能力通常是現(xiàn)代企業(yè)成功最關(guān)的鍵因素。 低代碼技術(shù)近期受到了廣泛的關(guān)注。即使是領(lǐng)先的分析公司也對這一趨勢表達(dá)了自己的看法;事實(shí)上,F(xiàn)orrester預(yù)測低代碼收入增長將超過68%,到2020年整體市場規(guī)模達(dá)到155億美元。 那么為什么低代碼解決方案最近受到如此多的關(guān)注呢? 它有保...
摘要:保利威無延遲直播可以在手機(jī)電腦上實(shí)現(xiàn)高參與度的互動,讓課堂體驗(yàn)再上一層,對于提升公開課轉(zhuǎn)化率非常有幫助。無延遲體驗(yàn)?zāi)茏屩辈ジ咏€下體驗(yàn)。 ? ? 衡量一場直播是否成功,用戶互動體驗(yàn)必然是關(guān)鍵一環(huán)。 ? 今年疫情影響下,云辦公、云上課、云會展、云購物紛紛興起。帶貨直播、空中課堂、會展直播等多樣化的場景讓用戶對直播實(shí)時性、流暢性有了更高要求。 ? ...
摘要:視頻云聯(lián)合大促活動對象新老用戶均可參加,各規(guī)則限購次。云直播特惠套餐無需開發(fā)提供超項(xiàng)功能全終端低延遲開箱即用的直播服務(wù)。功能完善超項(xiàng)直播功能,可實(shí)現(xiàn)企業(yè)各類直播的互動營銷定制大數(shù)據(jù)運(yùn)營等需求。 UCloud+保利威=?UCloud最近新上線了【CDN&視頻云聯(lián)合大促】活動:CDN低至0.01元/GB 9.9元搶實(shí)時音視頻10萬分鐘時長包,除了CDN流量包、URTC實(shí)時音視頻時長包和云直播U...
閱讀 1692·2023-04-25 20:16
閱讀 3874·2021-10-09 09:54
閱讀 2708·2021-09-04 16:40
閱讀 2525·2019-08-30 15:55
閱讀 842·2019-08-29 12:37
閱讀 2745·2019-08-26 13:55
閱讀 2914·2019-08-26 11:42
閱讀 3158·2019-08-23 18:26