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

概念
- MediaStreamTrack
- MediaStream
- source
- sink
- MediaTrackConstraints
1. MediaStreamTrack
MediaStreamTrack是WebRTC中的基本媒體單位,一個(gè)MediaStreamTrack包含一種媒體源(媒體設(shè)備或錄制內(nèi)容)返回的單一類型的媒體(如音頻,視頻)。單個(gè)軌道可包含多個(gè)頻道,如立體聲源盡管由多個(gè)音頻軌道構(gòu)成,但也可以看作是一個(gè)軌道。
WebRTC并不能直接訪問(wèn)或者控制源,對(duì)源的一切控制都可以通過(guò)軌道控制MediaTrackConstraints進(jìn)行實(shí)施。
MediaStreamTrack MDN文檔
MediaTrackConstraints MDN文檔
2. MediaStream
MediaStream是MediaStreamTrack的合集,可以包含 >=0 個(gè) MediaStreamTrack。MediaStream能夠確保它所包含的所有軌道都是是同時(shí)播放的,以及軌道的單一性。
3. source 與 sink
再M(fèi)ediaTrack的源碼中,MediaTrack都是由對(duì)應(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)采集的音頻,攝像頭采集的視頻等動(dòng)態(tài)資源。而sink則負(fù)責(zé)消費(fèi)source生產(chǎn)媒體資源,也就是通過(guò)<img>,<video>,<audio>等媒體標(biāo)簽進(jìn)行展示,或者是通過(guò)RTCPeerConnection將source通過(guò)網(wǎng)絡(luò)傳遞到遠(yuǎn)端。RTCPeerConnection可同時(shí)扮演source與sink的角色,作為sink,可以將獲取的source降低碼率,縮放,調(diào)整幀率等,然后傳遞到遠(yuǎn)端,作為source,將獲取的遠(yuǎn)端碼流傳遞到本地渲染。
source 與sink構(gòu)成一個(gè)MediaTrack,多個(gè)MeidaTrack構(gòu)成MediaStram。
4. MediaTrackConstraints
MediaTrackConstraints描述MediaTrack的功能以及每個(gè)功能可以采用的一個(gè)或多個(gè)值,從而達(dá)到選擇和控制源的目的。 MediaTrackConstraints 可作為參數(shù)傳遞給applyConstraints()以達(dá)到控制軌道屬性的目的,同時(shí)可以通過(guò)調(diào)getConstraints()用來(lái)查看最近應(yīng)用自定義約束。
const constraints = {
width: {min: 640, ideal: 1280},
height: {min: 480, ideal: 720},
advanced: [
{width: 1920, height: 1280},
{aspectRatio: 1.333}
]
};
//{ video: true }也是一個(gè)MediaTrackConstraints對(duì)象,用于指定請(qǐng)求的媒體類型和相對(duì)應(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對(duì)象直接賦值給HTMLMediaElement 接口的 srcObject屬性。
video.srcObject = stream;
如何獲取MediaStream
- 本地設(shè)備
可通過(guò)調(diào)用MediaDevices.getUserMedia()來(lái)訪問(wèn)本地媒體,調(diào)用該方法后瀏覽器會(huì)提示用戶給予使用媒體輸入的許可,媒體輸入會(huì)產(chǎn)生一個(gè)MediaStream,里面包含了請(qǐng)求的媒體類型的軌道。此流可以包含一個(gè)視頻軌道(來(lái)自硬件或者虛擬視頻源,比如相機(jī)、視頻采集設(shè)備和屏幕共享服務(wù)等等)、一個(gè)音頻軌道(同樣來(lái)自硬件或虛擬音頻源,比如麥克風(fēng)、A/D轉(zhuǎn)換器等等),也可能是其它軌道類型。
navigator.mediaDevices.getUserMedia(constraints)
.then(function(stream) {
/* 使用這個(gè)stream*/
video.srcObject = stream;
})
.catch(function(err) {
/* 處理error */
});
通過(guò)MediaDevices.enumerateDevices()我們可以得到一個(gè)本機(jī)可用的媒體輸入和輸出設(shè)備的列表,例如麥克風(fēng),攝像機(jī),耳機(jī)設(shè)備等。
//獲取媒體設(shè)備
navigator.mediaDevices.enumerateDevices().then(res => {
console.log(res);
});

列表中的每個(gè)媒體輸入都可作為MediaTrackConstraints中對(duì)應(yīng)類型的值,如一個(gè)音頻設(shè)備輸入audioDeviceInput可設(shè)置為MediaTrackConstraints中audio屬性的值
cosnt constraints = { audio : audioDeviceInput }
將該constraint值作為參數(shù)傳入到MediaDevices.getUserMedia(constraints)中,便可獲得該設(shè)備的MediaStream。
MediaDevices.enumerateDevices() MDN文檔
MediaDevices.getUserMedia() MDN文檔
- 捕獲屏幕
使用MediaDevices.getDisplayMedia()方法,可以提示用戶去選擇和授權(quán)捕獲展示的內(nèi)容或部分內(nèi)容(如一個(gè)窗口),并將錄制內(nèi)容在一個(gè)MediaStream 里。

MediaDevices.getDisplayMedia() MDN文檔
- HTMLCanvasElement.captureStream()
使用HTMLCanvasElement.captureStream() 方法返回的 CanvasCaptureMediaStream 是一個(gè)實(shí)時(shí)捕獲的canvas動(dòng)畫流。
//frameRate設(shè)置雙精準(zhǔn)度浮點(diǎn)值為每個(gè)幀的捕獲速率。
//如果未設(shè)置,則每次畫布更改時(shí)都會(huì)捕獲一個(gè)新幀。
//如果設(shè)置為0,則會(huì)捕獲單個(gè)幀。
cosnt canvasStream = canvas.captureStream(frameRate);
video.srcObject = canvasSream;
HTMLCanvasElement.captureStream() MDN文檔
CanvasCaptureMediaStream MDN文檔
RTCPeerConnection
從其他MediaStream中獲取
可通過(guò)構(gòu)造函數(shù)MediaStream() 返回新建的空白的 MediaStream 實(shí)例
newStream = new MediaStream();
- 傳入
MediaStream對(duì)象,該MediaStream對(duì)象的數(shù)據(jù)軌會(huì)被自動(dòng)添加到新建的流中。且這些數(shù)據(jù)軌不會(huì)從原流中移除,即變成了兩條流共享的數(shù)據(jù)。
newStream = new MediaStream(otherStream);
- 傳入
MediaStreamTrack對(duì)象的Array類型的成員,代表了每一個(gè)添加到流中的數(shù)據(jù)軌。
newStream = new MediaStream(tracks[]);
-
MediaStream.addTrack()方法會(huì)給流添加一個(gè)新軌道。 -
MediaStream.clone()方法復(fù)制一份副本MediaStream。這個(gè)新的MediaStream對(duì)象有一個(gè)新的id。
實(shí)現(xiàn)一個(gè)簡(jiǎ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;
- 通過(guò)
MediaRecorder對(duì)畫面進(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();