web端直播技术初步研究(下)

引言

之前写了《web端直播技术初步研究(上)》,本篇继续介绍直播流播放原理。

flv.js 和 dash.js 的原理

在上篇中介绍了flv.jsdash.js可以分别播放不同协议的视频流,那么它们的原理是什么呢?

大概流程是:

对于无法直接播放的视频源,先通过异步获取视频源,得到二进制的视频流内容,通过转码成视频可以解析的二进制内容,然后“投喂”给video播放器。

需要涉及几项现代技术(但不是每种协议方式都会用到如下技术):

  • MSE(Media Source Extension)

这是一个w3c规范,不做媒体业务开发的同学可能不是非常熟悉这个规范。此规范允许JavaScript动态构建audiovideo的媒体流。它定义了一个MediaSource对象,可以作为HTMLMediaElement的媒体数据源,它是实现“投喂器”的主要技术,简单伪代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<script>
  (function (param) {
    var videoMp4 = document.querySelector('#videoEl');
    if (window.MediaSource) {
      var mediaSource = new MediaSource();
      videoMp4.src = URL.createObjectURL(mediaSource);
      mediaSource.addEventListener('sourceopen', sourceOpen);
    } else {
      console.log("The Media Source Extensions API is not supported.")
    }

    function sourceOpen(e) {
      URL.revokeObjectURL(videoMp4.src);
      // 设置 媒体的编码类型
      var mime = 'video/webm; codecs="vorbis, vp8"';
      var mediaSource = e.target;
      var sourceBuffer = mediaSource.addSourceBuffer(mime);
      var videoUrl = './video/v.webm';
      fetch(videoUrl)
        .then(function (response) {
          return response.arrayBuffer();
        })
        .then(function (arrayBuffer) {
          sourceBuffer.addEventListener('updateend', function (e) {
            if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
              mediaSource.endOfStream();
              videoMp4.play().then(function () {}).catch(function (err) {
                log('.js-log-mp4', err)
              });
            }
          });
          sourceBuffer.appendBuffer(arrayBuffer);
        });
    }
  })()
</script>
  • WebSocket

Websocket可以传输文本和二进制内容,主要用来持续接收视频流数据,伪代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
  (function () {
    const webSocket = new WebSocket('ws://server.example.com');
    webSocket.binaryType = 'arraybuffer';

    webSocket.addEventListener('open', (event) => {
      // 建立连接成功
    });

    // 通过message事件接收消息
    webSocket.addEventListener('message', function (event) {
      console.log('Message from server', event.data);
      let buffer = event.data;
      let data = parseBuffer(buffer);
    });

    // 关闭
    webSocket.addEventListener('close', (closeEvent) => {
      webSocket.close();
    });
  })()
</script>
  • fetch

Fetch api主要用于异步获取视频源的内容,以及对流的处理。

首先我们需要先获取流:

1
fetch('./demo.flv').then(response => response.body)

现在我们已经有了流媒体,我们需要一个“阅读器”来读取:

1
2
3
4
5
6
fetch('./ demo.flv ')
  .then(response => response.body)
  .then(body => {
    const reader = body.getReader()
    
  })

已经获取了“阅读器”,我们通过“阅读器”来读取流内容,并创建自定义可读流(自定义可读流并不是必需的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
return new ReadableStream({
    start(controller) {
      return pump();

      function pump() {
        return reader.read().then(({
          done,
          value
        }) => {

          if (done) {
            controller.close();
            return;
          }

          controller.enqueue(value);
          return pump();
        });
      }
    }
  })
  .then(stream => new Response(stream))
  .then(response => response.blob())
  .then(blob => URL.createObjectURL(blob))
  .then(url => console.log('处理并投喂给视频播放器'))
  .catch(err => console.error(err));
  • ArrayBuffer | Blob

一般与Fetch api配合使用,用来对流内容进行处理,比如转码等。在将其它格式,比如flv格式转换成mp4格式时十分有用,flv.js就是使用这个做法。

至于如何转码,又是另一课题,不是本文主要目标,有机会另作分享。

回头看WebRTC的推流

之前说过webRTC目前还不是一个成熟的可用于商业直播平台的采集方案。但是,如果不考虑前置处理,我们目前是可以尝试使用它结合其它辅助技术进行推流的,主要用到的技术有:

  • WebRTC
  • MediaRecorder
  • WebSocket
  • FileReader
  • Blob | ArrayBuffer

首先,我们需要使用WebRTC打开客户端媒体硬件(摄像头和麦克风)进行视频录制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
navigator.mediaDevices.getUserMedia({
    video: {
      mandatory: {
        minAspectRatio: 1.777,
        maxAspectRatio: 1.778,
      },
    },
    audio: true
  })
  .then(stream => {

    if (stream) {
      oStream = stream

      // 将获取的音视频信息实时显示在video 中 
      const _video = document.querySelector('#__video_show')
      if (_video) {
        _video.srcObject = stream

        // 或者(不被推荐的方式)
        // _video.src = URL.createObjectURL(stream)
      }

    }
  })
  .catch(error => {
    alert(`出现错误:${error.message}`)
  })

获取了视频流之后,进行流的切分录制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
function startRecording(stream) {
  log('Start recording...');
  if (typeof MediaRecorder.isTypeSupported == 'function') {
    /*
        MediaRecorder.isTypeSupported is a function announced in https://developers.google.com/web/updates/2016/01/mediarecorder and later introduced in the MediaRecorder API spec http://www.w3.org/TR/mediastream-recording/
    */
    //这里涉及到视频的容器以及编解码参数,这个与浏览器有密切的关系
    if (MediaRecorder.isTypeSupported('video/webm;codecs=vp9')) {
      var options = {
        mimeType: 'video/webm;codecs=h264'
      };
    } else if (MediaRecorder.isTypeSupported('video/webm;codecs=h264')) {
      var options = {
        mimeType: 'video/webm;codecs=h264'
      };
    } else if (MediaRecorder.isTypeSupported('video/webm;codecs=vp8')) {
      var options = {
        mimeType: 'video/webm;codecs=vp8'
      };
    }
    log('Using ' + options.mimeType);
    mediaRecorder = new MediaRecorder(stream, options);
  } else {
    log('isTypeSupported is not supported, using default codecs for browser');
    mediaRecorder = new MediaRecorder(stream);
  }

  pauseResBtn.textContent = "Pause";

  mediaRecorder.start(10);

  var url = window.URL || window.webkitURL;
  videoElement.src = url ? url.createObjectURL(stream) : stream;
  videoElement.play();

  //这个地方,是视频数据捕获好了后,会触发MediaRecorder一个dataavailable的Event,在这里做视频数据的采集工作,主要是基于Blob进行转写,利用FileReader进行读取。FileReader一定
  //要注册loadend的监听器,或者写onload的函数。在loadend的监听函数里面,进行格式转换,方便websocket进行数据传输,因为websocket的数据类型支持blob以及arrayBuffer,我们这里用
  //的是arrayBuffer,所以,将视频数据的Blob转写为Unit8Buffer,便于websocket的后台服务用ByteBuffer接收。
  mediaRecorder.ondataavailable = function (e) {
    //log('Data available...');
    //console.log(e.data);
    //console.log(e.data.type);
    //console.log(e);
    chunks.push(e.data);
    var reader = new FileReader();
    reader.addEventListener("loadend", function () {
      // 一个块录制完成之后通过websocket进行上传
    });
    reader.readAsArrayBuffer(e.data);
  };

录制完成一个块之后就开始上传:

1
2
3
4
5
  var buf = new Uint8Array(reader.result);
  console.log(reader.result);
  if (reader.result.byteLength > 0) { //加这个判断,是因为有很多数据是空的,这个没有必要发到后台服务器,减轻网络开销,提升性能吧。
    ws.send(buf);
  }

服务端通过ws接收数据,并执行解编码后向拉流端传送数据。

服务端

之前我们讲的都是客户端拉流,那服务端呢?

一般大型的应用都使用云平台提供服务,更加稳定高效。也可以自己进行搭建服务,可以使用srs ,livegonginx-http-flv-module等进行搭建。由于服务端搭建不是本文重点阐述目标,下面只是简单介绍一下常用服务端技术:

  • Srs(Simple-RTMP-Server)

SRS流媒体服务器定位是运营级的互联网直播服务器集群,追求更好的概念完整性和最简单实现的代码。Srs是跨平台流媒体服务器,支持rtmp/hls/http/hds的协议

  • Livego

是使用 Go语言开发的直播服务器,跨平台,支持常用的传输协议,如RTMP, HLS,HTTP-FLV等

  • nginx-http-flv-module

是在nginx-rtmp-module基础上实现的一个音视频传输模块,将RTMP转为FLV封装格式,再通过HTTP协议下发。支持HTTP-FLV方式直播,添加了GOP(group of picture)缓存功能,减少了首屏等待时间,对RTMP和HTTP-FLV都有效,添加了VHOST(单IP地址多域名)功能并支持类似Nginx的HTTP模块的通配符配置;修复了nginx-rtmp-module中已知的bug。

小结

以上粗略地描述了如何通过现代web技术搭建直播和点播视频流的功能原理,实际商业操作过程其实比本文描述的更加复杂,比如:如何在基础功能上保证视频流的稳定和性能,webSocket的连接的异常中断如何处理,断点如何续播等。

本人才学有限,如有错误,敬请斧正。

参考文档

调研参考了大量的文章,无法一一列出。列出一部分,表示深深的感谢: