談?wù)凪ediaStream

MediaStream 是連接 WebRTC API 和底層物理流的中間層渣刷,webRTC將音視頻經(jīng)過Vocie / Video engine進(jìn)行處理后徐勃,再通過MediaStream API給暴露給上層使用笤喳。


image.png

概念

  • MediaStreamTrack
  • MediaStream
  • source
  • sink
  • MediaTrackConstraints

1. MediaStreamTrack

MediaStreamTrack是WebRTC中的基本媒體單位弯予,一個MediaStreamTrack包含一種媒體源(媒體設(shè)備或錄制內(nèi)容)返回的單一類型的媒體(如音頻肛跌,視頻)愉舔。單個軌道可包含多個頻道钢猛,如立體聲源盡管由多個音頻軌道構(gòu)成,但也可以看作是一個軌道轩缤。
WebRTC并不能直接訪問或者控制源命迈,對源的一切控制都可以通過軌道控制MediaTrackConstraints進(jìn)行實施。

MediaStreamTrack MDN文檔
MediaTrackConstraints MDN文檔

2. MediaStream

MediaStream是MediaStreamTrack的合集典奉,可以包含 >=0 個 MediaStreamTrack躺翻。MediaStream能夠確保它所包含的所有軌道都是是同時播放的,以及軌道的單一性卫玖。

MediaStream MDN文檔

3. source 與 sink

再MediaTrack的源碼中公你,MediaTrack都是由對應(yīng)的source和sink組成的。

//src\pc\video_track.cc
void VideoTrack::AddOrUpdateSink(rtc::VideoSinkInterface<VideoFrame>* sink, const rtc::VideoSinkWants& wants) {
  RTC_DCHECK(worker_thread_->IsCurrent());
  VideoSourceBase::AddOrUpdateSink(sink, wants);
  rtc::VideoSinkWants modified_wants = wants;
  modified_wants.black_frames = !enabled();
  video_source_->AddOrUpdateSink(sink, modified_wants);
}
 
void VideoTrack::RemoveSink(rtc::VideoSinkInterface<VideoFrame>* sink) {
  RTC_DCHECK(worker_thread_->IsCurrent());
  VideoSourceBase::RemoveSink(sink);
  video_source_->RemoveSink(sink);
}

瀏覽器中存在從source到sink的媒體管道假瞬,其中source負(fù)責(zé)生產(chǎn)媒體資源陕靠,包括多媒體文件,web資源等靜態(tài)資源以及麥克風(fēng)采集的音頻脱茉,攝像頭采集的視頻等動態(tài)資源剪芥。而sink則負(fù)責(zé)消費source生產(chǎn)媒體資源,也就是通過<img>琴许,<video>税肪,<audio>等媒體標(biāo)簽進(jìn)行展示,或者是通過RTCPeerConnection將source通過網(wǎng)絡(luò)傳遞到遠(yuǎn)端。RTCPeerConnection可同時扮演source與sink的角色益兄,作為sink锻梳,可以將獲取的source降低碼率,縮放净捅,調(diào)整幀率等疑枯,然后傳遞到遠(yuǎn)端,作為source蛔六,將獲取的遠(yuǎn)端碼流傳遞到本地渲染荆永。

source 與sink構(gòu)成一個MediaTrack,多個MeidaTrack構(gòu)成MediaStram国章。

4. MediaTrackConstraints

MediaTrackConstraints描述MediaTrack的功能以及每個功能可以采用的一個或多個值具钥,從而達(dá)到選擇和控制源的目的。 MediaTrackConstraints 可作為參數(shù)傳遞給applyConstraints()以達(dá)到控制軌道屬性的目的捉腥,同時可以通過調(diào)getConstraints()用來查看最近應(yīng)用自定義約束氓拼。

const constraints = {
  width: {min: 640, ideal: 1280},
  height: {min: 480, ideal: 720},
  advanced: [
    {width: 1920, height: 1280},
    {aspectRatio: 1.333}
  ]
};

//{ video: true }也是一個MediaTrackConstraints對象,用于指定請求的媒體類型和相對應(yīng)的參數(shù)抵碟。
navigator.mediaDevices.getUserMedia({ video: true })
.then(mediaStream => {
  const track = mediaStream.getVideoTracks()[0];
  track.applyConstraints(constraints)
  .then(() => {
    // Do something with the track such as using the Image Capture API.
  })
  .catch(e => {
    // The constraints could not be satisfied by the available devices.
  });
});

如何播放MediaStream

可將MediaStream對象直接賦值給HTMLMediaElement 接口的 srcObject屬性桃漾。

video.srcObject = stream;

srcObject MDN文檔

如何獲取MediaStream

  1. 本地設(shè)備

可通過調(diào)用MediaDevices.getUserMedia()來訪問本地媒體,調(diào)用該方法后瀏覽器會提示用戶給予使用媒體輸入的許可拟逮,媒體輸入會產(chǎn)生一個MediaStream撬统,里面包含了請求的媒體類型的軌道。此流可以包含一個視頻軌道(來自硬件或者虛擬視頻源敦迄,比如相機(jī)恋追、視頻采集設(shè)備和屏幕共享服務(wù)等等)、一個音頻軌道(同樣來自硬件或虛擬音頻源罚屋,比如麥克風(fēng)苦囱、A/D轉(zhuǎn)換器等等),也可能是其它軌道類型脾猛。

navigator.mediaDevices.getUserMedia(constraints)
  .then(function(stream) {
    /* 使用這個stream*/
    video.srcObject = stream;
  })
  .catch(function(err) {
    /* 處理error */
  });

通過MediaDevices.enumerateDevices()我們可以得到一個本機(jī)可用的媒體輸入和輸出設(shè)備的列表撕彤,例如麥克風(fēng),攝像機(jī)猛拴,耳機(jī)設(shè)備等羹铅。

//獲取媒體設(shè)備
navigator.mediaDevices.enumerateDevices().then(res => {
    console.log(res);
});
image.png

列表中的每個媒體輸入都可作為MediaTrackConstraints中對應(yīng)類型的值,如一個音頻設(shè)備輸入audioDeviceInput可設(shè)置為MediaTrackConstraints中audio屬性的值

cosnt constraints = { audio : audioDeviceInput }

將該constraint值作為參數(shù)傳入到MediaDevices.getUserMedia(constraints)中愉昆,便可獲得該設(shè)備的MediaStream职员。

MediaDevices.enumerateDevices() MDN文檔
MediaDevices.getUserMedia() MDN文檔

  1. 捕獲屏幕

使用MediaDevices.getDisplayMedia()方法,可以提示用戶去選擇和授權(quán)捕獲展示的內(nèi)容或部分內(nèi)容(如一個窗口)跛溉,并將錄制內(nèi)容在一個MediaStream 里焊切。

image.png

MediaDevices.getDisplayMedia() MDN文檔

  1. HTMLCanvasElement.captureStream()

使用HTMLCanvasElement.captureStream() 方法返回的 CanvasCaptureMediaStream 是一個實時捕獲的canvas動畫流扮授。

//frameRate設(shè)置雙精準(zhǔn)度浮點值為每個幀的捕獲速率。
//如果未設(shè)置蛛蒙,則每次畫布更改時都會捕獲一個新幀糙箍。
//如果設(shè)置為0,則會捕獲單個幀牵祟。
cosnt canvasStream = canvas.captureStream(frameRate);
video.srcObject = canvasSream;

HTMLCanvasElement.captureStream() MDN文檔
CanvasCaptureMediaStream MDN文檔

  1. RTCPeerConnection

  2. 從其他MediaStream中獲取

可通過構(gòu)造函數(shù)MediaStream() 返回新建的空白的 MediaStream 實例

newStream = new MediaStream();
  • 傳入 MediaStream 對象,該 MediaStream 對象的數(shù)據(jù)軌會被自動添加到新建的流中抖格。且這些數(shù)據(jù)軌不會從原流中移除诺苹,即變成了兩條流共享的數(shù)據(jù)。
newStream = new MediaStream(otherStream);
  • 傳入 MediaStreamTrack 對象的 Array 類型的成員雹拄,代表了每一個添加到流中的數(shù)據(jù)軌收奔。
newStream = new MediaStream(tracks[]);
  • MediaStream.addTrack()方法會給流添加一個新軌道。
  • MediaStream.clone()方法復(fù)制一份副本 MediaStream滓玖。這個新的MediaStream對象有一個新的id坪哄。

實現(xiàn)一個簡易的錄屏工具

  • 獲取捕獲屏幕的MeidaStream
const screenStream = await navigator.mediaDevices.getDisplayMedia();
  • 獲取本地音視頻數(shù)據(jù)
const cameraStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
const audioStream = await navigator.mediaDevices.getUserMedia({ video: false, audio: true });
  • 將屏幕畫面與攝像頭畫面繪制canvas中
function createStreamVideo(stream) {
    const video = document.createElement("video");
    video.srcObject = stream;
    video.autoplay = true;

    return video;
}

const cameraVideo = createStreamVideo(cameraStream);
const screenVideo = createStreamVideo(screenStream);
animationFrameHandler() {
      if (screenVideo) {
          canvas.drawImage(screenVideo, 0, 0, canvasWidth, canvasHeight);
      }

      if (cameraVideo) {
          canvas.drawImage(
              cameraVideo,
              canvasWidth - CAMERA_VIDEO_WIDTH,
              canvasHeight - CAMERA_VIDEO_HEIGHT,
              CAMERA_VIDEO_WIDTH,
              CAMERA_VIDEO_HEIGHT
          )
      }

      requestAnimationFrame(animationFrameHandler.bind(this));
  }
  • 獲取canvas的MediaStream
const recorderVideoStream = await canvas.captureStream();
  • 合并本地的音頻軌道和canvasStream的視頻軌道,獲得最終畫面的MediaStream
const stream = new MediaStream();
audioStream.getAudioTracks().forEach(track => stream.addTrack(track));
recorderVideoStream.getVideoTracks().forEach(track => stream.addTrack(track));

video.srcObject = stream;
  • 通過MediaRecorder對畫面進(jìn)行錄制
const recorder = new MediaRecorder(stream, { mineType: "video/webm;codecs=h264" });
recorder.ondataavailable = e => {
    recorderVideo.src = URL.createObjectURL(e.data);
};

//開始錄制
recorder.start();

//停止錄制
recorder.stop();

MediaRecorder MDN文檔

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末势篡,一起剝皮案震驚了整個濱河市翩肌,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌禁悠,老刑警劉巖念祭,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異碍侦,居然都是意外死亡粱坤,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門瓷产,熙熙樓的掌柜王于貴愁眉苦臉地迎上來站玄,“玉大人,你說我怎么就攤上這事濒旦≈昕酰” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵疤估,是天一觀的道長灾常。 經(jīng)常有香客問我,道長铃拇,這世上最難降的妖魔是什么钞瀑? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮慷荔,結(jié)果婚禮上雕什,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好贷岸,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布壹士。 她就那樣靜靜地躺著,像睡著了一般偿警。 火紅的嫁衣襯著肌膚如雪躏救。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天螟蒸,我揣著相機(jī)與錄音盒使,去河邊找鬼。 笑死七嫌,一個胖子當(dāng)著我的面吹牛少办,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播诵原,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼英妓,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了绍赛?” 一聲冷哼從身側(cè)響起蔓纠,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎惹资,沒想到半個月后缕允,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體婆誓,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了抖拴。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片佣谐。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡规伐,死狀恐怖做盅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情分扎,我是刑警寧澤澄成,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站畏吓,受9級特大地震影響墨状,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜菲饼,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一肾砂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧宏悦,春花似錦镐确、人聲如沸包吝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽诗越。三九已至,卻和暖如春息堂,著一層夾襖步出監(jiān)牢的瞬間嚷狞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工储矩, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留感耙,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓持隧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親逃片。 傳聞我的和親對象是個殘疾皇子屡拨,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容