擺脫客戶端?網頁發起直播勢在必行!

背景

近幾年直播行業飛速發展,可是因爲Web端這方面功能的長時間缺失,使得直播端以客戶端爲主;WebRTC 的出現使得網頁也能夠成爲直播端。那麼究竟WebRTC是什麼呢?javascript

WebRTC,即Web Real-Time Communication,web實時通訊技術。簡單地說就是在web瀏覽器裏面引入實時通訊,包括音視頻通話等,它使得實時通訊變成一種標準功能,任何Web應用都無需藉助第三方插件和專有軟件,而是經過JavaScript API便可完成;並且WebRTC提供了視頻會議的核心技術,包括音視頻的採集、編解碼、網絡傳輸、展現等功能,還支持跨平臺,包括主流的PC和移動端設備。html

下面介紹下須要用到的幾個API:java

getUserMedia

咱們能夠經過調用navigator.mediaDevices.getUserMedia(constraints)去初始化一個本地的音視頻流,而後把直播流經過video標籤播放。代碼以下: web

html:chrome

<div id="container">
    <video id="gum-local" autoplay playsinline></video>
    <button id="showVideo">Open camera</button>
    <button id="switchVideo">switch camera</button>
</div>

js:json

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));

示例效果:
圖片描述canvas

固然,若是有多個設備,就須要考慮設備選擇和設備切換的問題。那就須要用到下面的這個API。windows

設備

咱們看看如何用原生的Web API去獲取設備(如下示例代碼可適用於Chrome,其餘瀏覽器暫未測試;具體瀏覽器兼容性可參考官方文檔,本文檔底部有連接)。數組

navigator.mediaDevices.enumerateDevices()

若是枚舉成功將會返回一個包含MediaDeviceInfo實例的數組,它包含了可用的多媒體輸入輸出設備的信息。瀏覽器

下面是調用代碼示例。

navigator.mediaDevices.enumerateDevices().then((devices) => {
  console.log(devices, '-----enumerateDevices------');
});

設備參數說明:

  1. deviceId:設備id,具備惟一性
  2. groupId:設備組id,不具備惟一性
  3. kind:設備類別(audioinput:音頻輸入設備,audiooutput:音頻輸出設備,videoinput:視頻輸入設備)
  4. label:設備名稱(未通過受權容許的設備,label值爲空,受權容許後可拿到label的值,以下兩圖所示)

獲取的全部設備截圖(未受權):
圖片描述

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.connectruntime.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的點對點鏈接的過程爲:

  1. 呼叫端給接收端發送一個offer信息。在發送給接收端以前先調用setLocalDescription存儲本地offer描述。
  2. 接收端收到offer消息後,先調用setRemoteDescription存儲遠端offer,再建立一個answer信息給呼叫端。

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);
    // 接收到answer
    localConnection.setRemoteDescription(answer);
  })
});

至此咱們已經介紹完畢瀏覽器設備檢測採集和屏幕分享的基本流程,可是光有這些可還遠遠不夠,一套完整的直播體系包括音視頻採集、處理、編碼和封裝、推流到服務器、服務器流分發、播放器流播放等等。若是想節省開發成本,可使用第三方SDK。下面簡單介紹下使用聲網SDK發起直播的流程。

瀏覽器要求:

  1. Chrome 58+
  2. Firefox 56+
  3. Safari 11+(屏幕共享不可用)
  4. Opera 45+(屏幕共享不可用)
  5. QQ 10+(屏幕共享不可用)
  6. 360 安全瀏覽器 9.1+(屏幕共享不可用)

設備檢測

調用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 插件id
    mediaSource: '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, // 視頻幀左上角的橫軸位置,默認爲0
    y: 0, // 視頻幀左上角的縱軸位置,默認爲0
    width: 1280, // 視頻幀寬度,默認爲640
    height: 720, // 視頻幀高度,默認爲360
    zOrder: 0, // 視頻幀所處層數;取值範圍爲 [0,100];默認值爲 0,表示該區域圖像位於最下層
    alpha: 1.0, // 視頻幀的透明度,默認值爲 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);

由於業務需求是攝像頭和屏幕共享能夠切換,攝像頭和屏幕共享的分辨率和碼率均不相同,屏幕共享須要更高的分辨率和碼率。可是開發中發現切換時設置碼率無效。SDK那邊給的答覆是:由於緩存問題,會以第一次推流設置的參數爲準,將會在下個版本中修復。

參考文獻:
MediaDevices.getUserMedia()
MedaiDevices.enumerateDevices()
HTMLMediaElement
MediaDevices/getDisplayMedia

相關文章
相關標籤/搜索