谈谈MediaStream

MediaStream 是连接 WebRTC API 和底层物理流的中间层,webRTC将音视频经过Vocie / Video engine进行处理后,再通过MediaStream API给暴露给上层使用。


image.png

概念

  • MediaStreamTrack
  • MediaStream
  • source
  • sink
  • MediaTrackConstraints

1. MediaStreamTrack

MediaStreamTrack是WebRTC中的基本媒体单位,一个MediaStreamTrack包含一种媒体源(媒体设备或录制内容)返回的单一类型的媒体(如音频,视频)。单个轨道可包含多个频道,如立体声源尽管由多个音频轨道构成,但也可以看作是一个轨道。
WebRTC并不能直接访问或者控制源,对源的一切控制都可以通过轨道控制MediaTrackConstraints进行实施。

MediaStreamTrack MDN文档
MediaTrackConstraints MDN文档

2. MediaStream

MediaStream是MediaStreamTrack的合集,可以包含 >=0 个 MediaStreamTrack。MediaStream能够确保它所包含的所有轨道都是是同时播放的,以及轨道的单一性。

MediaStream MDN文档

3. source 与 sink

再MediaTrack的源码中,MediaTrack都是由对应的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负责生产媒体资源,包括多媒体文件,web资源等静态资源以及麦克风采集的音频,摄像头采集的视频等动态资源。而sink则负责消费source生产媒体资源,也就是通过<img>,<video>,<audio>等媒体标签进行展示,或者是通过RTCPeerConnection将source通过网络传递到远端。RTCPeerConnection可同时扮演source与sink的角色,作为sink,可以将获取的source降低码率,缩放,调整帧率等,然后传递到远端,作为source,将获取的远端码流传递到本地渲染。

source 与sink构成一个MediaTrack,多个MeidaTrack构成MediaStram。

4. MediaTrackConstraints

MediaTrackConstraints描述MediaTrack的功能以及每个功能可以采用的一个或多个值,从而达到选择和控制源的目的。 MediaTrackConstraints 可作为参数传递给applyConstraints()以达到控制轨道属性的目的,同时可以通过调getConstraints()用来查看最近应用自定义约束。

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

//{ video: true }也是一个MediaTrackConstraints对象,用于指定请求的媒体类型和相对应的参数。
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. 本地设备

可通过调用MediaDevices.getUserMedia()来访问本地媒体,调用该方法后浏览器会提示用户给予使用媒体输入的许可,媒体输入会产生一个MediaStream,里面包含了请求的媒体类型的轨道。此流可以包含一个视频轨道(来自硬件或者虚拟视频源,比如相机、视频采集设备和屏幕共享服务等等)、一个音频轨道(同样来自硬件或虚拟音频源,比如麦克风、A/D转换器等等),也可能是其它轨道类型。

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

通过MediaDevices.enumerateDevices()我们可以得到一个本机可用的媒体输入和输出设备的列表,例如麦克风,摄像机,耳机设备等。

//获取媒体设备
navigator.mediaDevices.enumerateDevices().then(res => {
    console.log(res);
});
image.png

列表中的每个媒体输入都可作为MediaTrackConstraints中对应类型的值,如一个音频设备输入audioDeviceInput可设置为MediaTrackConstraints中audio属性的值

cosnt constraints = { audio : audioDeviceInput }

将该constraint值作为参数传入到MediaDevices.getUserMedia(constraints)中,便可获得该设备的MediaStream。

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

  1. 捕获屏幕

使用MediaDevices.getDisplayMedia()方法,可以提示用户去选择和授权捕获展示的内容或部分内容(如一个窗口),并将录制内容在一个MediaStream 里。

image.png

MediaDevices.getDisplayMedia() MDN文档

  1. HTMLCanvasElement.captureStream()

使用HTMLCanvasElement.captureStream() 方法返回的 CanvasCaptureMediaStream 是一个实时捕获的canvas动画流。

//frameRate设置双精准度浮点值为每个帧的捕获速率。
//如果未设置,则每次画布更改时都会捕获一个新帧。
//如果设置为0,则会捕获单个帧。
cosnt canvasStream = canvas.captureStream(frameRate);
video.srcObject = canvasSream;

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

  1. RTCPeerConnection

  2. 从其他MediaStream中获取

可通过构造函数MediaStream() 返回新建的空白的 MediaStream 实例

newStream = new MediaStream();
  • 传入 MediaStream 对象,该 MediaStream 对象的数据轨会被自动添加到新建的流中。且这些数据轨不会从原流中移除,即变成了两条流共享的数据。
newStream = new MediaStream(otherStream);
  • 传入 MediaStreamTrack 对象的 Array 类型的成员,代表了每一个添加到流中的数据轨。
newStream = new MediaStream(tracks[]);
  • MediaStream.addTrack()方法会给流添加一个新轨道。
  • MediaStream.clone()方法复制一份副本 MediaStream。这个新的MediaStream对象有一个新的id

实现一个简易的录屏工具

  • 获取捕获屏幕的MeidaStream
const screenStream = await navigator.mediaDevices.getDisplayMedia();
  • 获取本地音视频数据
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对画面进行录制
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文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容