前端视频渐近式分片播放(m3u8)

4,376 阅读7分钟

前端视频渐近式分片播放(m3u8)

前言:这事还要从后端说起,因为后端需要处理m3u8数据,再把它还原成mp4音频文件,结果发现还需要使用工具对视频进行转码。浏览器只支持H264格式的mp4文件,对AVC的编码格式的视频不支持。其实,如果其它ogg还是webm文件都是avc格式的文件,你依旧无法在浏览器播放,所以就考虑前端怎么获取分段的ts数据,而不是全让后端处理。 现在,小编来讲解使用Mux.js处理数据流。

movie-6565990_960_720

至于具体浏览器支持什么格式,还请看下面的文章

最佳视频文件格式有8种,你对它们真的了解吗

一、分段原由?

首先,我们都会考虑到前端一般是会使用 HTML5的video和audio 支播放音视频。并且,前端还有许多插件和组件可以使用,比如 AplayerDplayer 都非常不错,但有一点就是,我们不一定经常可以使用完整的视频链接。比如说一个xxx. mp4 这种类型的文件。

image-20211126134229343

直播功能那就更不可能是使用这种方式去加载视频的,其中 DplayerDPlayer)插件也不错,有直播功能,可以和后端进行数据交互,今天就来讲一下,使用 mux.js 组件去获取后端的数据流。

二、使用方法

不急,先让你们看看 mux.js 文档

mux.js/ at main · …

因为这里官方文档是在GitHub, 所以自行解决哈!!!看到这儿,会有点儿感觉不正规的样子,没错,这是一个小团队开发的组件库,并不是什么大公司。 很大部分的组件,都是全球千千万的开发者开发出来的,因此我们目前只需要学会怎么使用就可以。

- 安装模块

因为是组件库,所以可以使用npm 或者 yarn去安装模块

npm install mux.js@5.14.1

其它方法?什么,你还想要CDN服务,很可惜,还真没有。 现各大cdn提供商没有收录这个组件。

因为目前的版本没有打包成dist文件,所以在官网还在开发的状态。所以使用 拼接 https://cdn.jsdelivr.net/gh 也没有效果。

- 实现流程

在这里,我们要明白,一般前端是只有引入文件,况且你也无法在video标签上的src加入 .ts后缀名的文件,浏览器本身不支持查看 .ts后缀名的文件。当然,我们可以使用框架去获取这个文件数据。但你获取要文件还不行, 这个实现的源头是数据流,而不是文件。

javascript中的方法

  1. 使用Ajax获取 .ts后缀名的文件中数据流。

  2. 引入mux.js,解析数据流

  3. 然后把数据流再放到video的src中

    image-20211126140019660

- 后端实现

首先,在Html文件中使用Ajax是获取不到你当前目录下的文件的数据流,因此会存在跨域的问题。如果有兴趣的小伙伴,可以去考虑怎么在 流行的 Vue 和 React框架中获取数据流。

所以,我们需要一个中间代理,让它去帮助我们去获取获取文件的数据流。然后返回数据,而我们前端只需要再访问这个代理就可以。注意:如果是在同一个服务器中,不存在跨域,直接在后端使用 跨域资源共享(CORS) , 开启域名访问权限就可以.

当前考虑怎么读取静态文件,并让前端获取数据流。 小编首选 使用 nginx代理 也可以说是php代理吧。至于怎么安装,请看教程 wampserver的安装与配置的详细过程:_晒太阳的鱼il的博客-CSDN博客_wampserver

在www目录下建立 ajaxdemo文件夹, 里面再新建立 getTsFile.php文件,复制如下内容

<?php

$localfile = "./static/segment-0.ts";

$size = filesize($localfile);

$start = 0;
$end = $size - 1;
$length = $size;

header("Accept-Ranges: 0-$size");
header("Content-Type: video/mp4");
header('Access-Control-Allow-Origin: *');

$ranges_arr = array();
if (isset($_SERVER['HTTP_RANGE'])) {
    if (!preg_match('/^bytes=\d*-\d*(,\d*-\d*)*$/i', $_SERVER['HTTP_RANGE'])) {
        header('HTTP/1.1 416 Requested Range Not Satisfiable');
    }
    $ranges = explode(',', substr($_SERVER['HTTP_RANGE'], 6));
    foreach ($ranges as $range) {
        $parts = explode('-', $range);
        $ranges_arr[] = array($parts[0],$parts[1]);
    }

    $ranges = $ranges_arr[0];
    if($ranges[0]==''){
        if($ranges[1]!=''){
            //Range: bytes=-n 表示取文件末尾的n个字节
            $length = (int)$ranges[1];
            $start = $size - $length;
        }else{
            //Range: bytes=- 这种形式不合法
            header('HTTP/1.1 416 Requested Range Not Satisfiable');
        }
    }else{
        $start = (int)$ranges[0];
        if($ranges[1]!=''){
            //Range: bytes=n-m 表示从文件的n偏移量读到m偏移量
            $end = (int)$ranges[1];
        }
        //Range: bytes=n- 表示从文件的n偏移量读到末尾
        $length = $end - $start + 1;
    }
    header('HTTP/1.1 206 PARTIAL CONTENT');
}

header("Content-Range: bytes {$start}-{$end}/{$size}");
header("Content-Length: $length");

$buffer = 8096;
$file = fopen($localfile, 'rb');
if($file){
    fseek($file, $start);
    while (!feof($file) && ($p = ftell($file)) <= $end){
        if ($p + $buffer > $end) {
            $buffer = $end - $p + 1;
        }
        set_time_limit(0);
        echo fread($file, $buffer);          //这都是返回的结果,是一个ts文件数据流
        flush();
    }
    fclose($file);
}
die;

?>

PHP 输出视频流 在线视频读取 隐藏真实播放地址 兼容ios 设备 UC浏览器等_Syspan-CSDN博客

然后在ajaxdemo中新建static文件夹,放置 .ts后缀的视频文件

这是php文件读取文件返回数据流的代码,具体详情就不做深究。

开启warmpserver后 ,查看数据流

http://localhost/ajaxdemo/getTsFile.php

image-20211126141811531

好像也没什么数据,不急,这里确实看不到。

- 前端实现

前面已经讲解如果安装模块,但真实有效果的js文件只有一个, 那就是编译后的dist文件下的 mux.js文件。html页面中只需要引入这个文件就可以,其它的都不需要。

  1. ajax获取数据流
  2. 响应后,进行数据流处理: 转换数据编码格式,
  3. 不断混合音频和视频流,并添加到标签上的src属性上
  • 混合音频操作是不间断进行的,在不断添加数据帧,混合音频。按帧进行混合。

实现代码

<!DOCTYPE html>
<html> 
<head>
  <title>Basic Transmuxer Test</title> 
  <script src="./assets/mux.js"></script> 
</head>

<body>
  <div id=video-wrapper>
    <h2>下面是视频</h2>
    <video id="my-video" controls></video>
  </div>
  <script>
    //  绑定文档节点指向document,并返回一个函数, 就可以使用$('xx'), 当然,只是简化,而不是jquery
    var $ = document.querySelector.bind(document);
    var vjsParsed, video, mediaSource;

    // 定义通用的事件回调处理函数,只做打印事件类型
    function logevent(event) {
      console.log(event);
    }

    // 一、使用ajax去获取 数据流
    let xhr = new XMLHttpRequest();
    xhr.open('GET', "http://localhost/ajaxdemo/getTsFile.php");
    // 接收的是 video/mp2t 二进制数据,并且arraybuffer类型方便后续直接处理 
    xhr.responseType = "arraybuffer";
    xhr.send();
    xhr.onreadystatechange = function () {
      if (xhr.readyState == 4) {
        if (xhr.status == 200) {
          transferFormat(xhr.response);
        } else {
          console.log('error');
        }
      }
    }

    // 二、接收到响应后,对数据流进行处理 
    function transferFormat(data) {
      //   (1. 将源数据从ArrayBuffer格式保存为可操作的Uint8Array格式 
      var segment = new Uint8Array(data);

      //   (2.remux选项默认为true,将源数据的音频视频混合为mp4,设为false则不混合
      var transmuxer = new muxjs.mp4.Transmuxer({ remux: true });

      // 注意:接收无音频ts文件,OutputType设置为'video',并且设置combined为 'false',
      //      在监听data事件的时候,控置转换流的类型 
      var combined = true;
      var outputType = 'audio';
 
      var remuxedSegments = [];
      var remuxedBytesLength = 0;
      var remuxedInitSegment = null;


      // 监听data事件,开始转换流
      transmuxer.on('data', function (event) {
        console.log("已经监听data事件", event.type, event);
        // event.type 有video和audio两种类型的数据流, 一种音频流,视频流, 不限制就只混合。
        // if (event.type === outputType) {  }
        remuxedSegments.push(event);
        remuxedBytesLength += event.data.byteLength;
        remuxedInitSegment = event.initSegment; 
      });

      //  监听转换完成事件,拼接最后结果并传入MediaSource
      transmuxer.on('done', function () {
        var offset = 0;
        var bytes = new Uint8Array(remuxedInitSegment.byteLength + remuxedBytesLength)
        bytes.set(remuxedInitSegment, offset);
        offset += remuxedInitSegment.byteLength;

        for (var j = 0, i = offset; j < remuxedSegments.length; j++) {
          bytes.set(remuxedSegments[j].data, i);
          i += remuxedSegments[j].byteLength;
        }
        remuxedSegments = [];
        remuxedBytesLength = 0;
        // 解析出转换后的mp4相关信息,与最终转换结果无关
        vjsParsed = muxjs.mp4.tools.inspect(bytes);
        console.log('transmuxed', vjsParsed);
        
        // (3.准备资源数据,添加到标签的视频流中
        prepareSourceBuffer(combined, outputType, bytes);
      });

      // 再次进入数据的监听
      // push方法可能会触发'data'事件,因此要在事件注册完成后调用
      // 传入源二进制数据,分割为m2ts包,依次调用上图中的流程
      transmuxer.push(segment); 

      // flush的调用会直接触发'done'事件,因此要事件注册完成后调用
      // 将所有数据从缓存区清出来
      transmuxer.flush(); 
    }

    // 三.混合音频后,不断添加混合流数据到src上
    function prepareSourceBuffer(combined, outputType, bytes) {
      var buffer;
      video = document.querySelector('#my-video');
      video.controls = true;
      video.volume = 1;
      mediaSource = new MediaSource();
      video.src = URL.createObjectURL(mediaSource);
  
      // 转换后mp4的音频格式 视频格式
      var codecsArray = ["avc1.64001f", "mp4a.40.5"];

      mediaSource.addEventListener('sourceopen', function () {
        // MediaSource 实例默认的duration属性为NaN
        mediaSource.duration = 0;
        // 转换为带音频、视频的mp4
        if (combined) {
          buffer = mediaSource.addSourceBuffer('video/mp4;codecs="' + 'avc1.64001f,mp4a.40.5' + '"');
          console.log("音频和视频都有", 'video/mp4;codecs="' + 'avc1.64001f,mp4a.40.5' + '"')
        } else if (outputType === 'video') {
          // 转换为只含视频的mp4
          buffer = mediaSource.addSourceBuffer('video/mp4;codecs="' + codecsArray[0] + '"');
          console.log("只有视频", 'video/mp4;codecs="' + codecsArray[0] + '"')
        } else if (outputType === 'audio') {
          // 转换为只含音频的mp4
          buffer = mediaSource.addSourceBuffer('audio/mp4;codecs="' + (codecsArray[1] || codecsArray[0]) + '"');
          console.log("只有音频")
        }

        buffer.addEventListener('updatestart', logevent);
        buffer.addEventListener('updateend', logevent);
        buffer.addEventListener('error', logevent);
        video.addEventListener('error', logevent);
        // mp4 buffer 准备完毕,传入转换后的数据
        // 将 bytes 放入 MediaSource 创建的sourceBuffer中
        // https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/appendBuffer
        buffer.appendBuffer(bytes);
        // 自动播放,一般不会自动播放。需要用户进行操作后,才可以再次由浏览器自动运行。
        // 这是浏览器厂商的对视频播放的限制。因为自动播放会导致不友好的体验。
        // video.play();
      });
    };

  </script>
</body>

</html>

再来看一下结果

image-20211126145616771

image-20211126145701992

数据流出现了,所以说,一般的数据流,我们看到都是比较奇特的东西

- 后端接口

​ 说了这么多,那到底与接口有什么关系呢? 一般我们前端是通过异步请求访问接口。 而后端会将访问到对应接口的主机进行权限认证,然后返回相应的数据 。

​ 上面的php文件其实就已经是简单实现后端的响应,而

  echo fread($file, $buffer);          //这都是返回的结果,是一个ts文件数据流

这一句就是说明返回的是一个.ts后缀名的文件数据流。 我们在访问

http://localhost/ajaxdemo/getTsFile.php

这个就类似后端的接口,我们一般前端访问的是 https://abc.com/api/ABCSDFSFDSFDSFDLfS 这种类似的接口。 后端响应的是数据流文件,而不是json文本文件。


实战分析

在这里我们来实战分析一下,别人家的网站是怎么播放视频的。 比如 Acfun 视频, 大家不要去乱抓取别人的视频,在这里只是分析一下实现原理。

链接不告诉,自己找哈!

image-20211126233542682

播放视频期间,在开发者选项的网络选项卡中,可以找到Fetch/XHR 异步请求。

image-20211126233700630

然后你就会找到非常多相似的数据

image-20211126233854976

点击查看请求头,大体都是一样的,只有后面的.ts部分不一样,

image-20211126234044206

image-20211126234102138

查看响应的数据,就是不同的文件数据流。

image-20211126234224771

浏览器到底做了什么?

​ 我们去访问 https://xx.acfun.cn/xxx.ts?ABCDSDFSDF 接口请求数据。如果浏览器支持响应的数据格式 ,浏览器就会根据它的数据类型来展示,也可以理解为播放。

​ 当服务器响应的数据是文本数据,图片数据,视频数据,流数据,那么浏览器就会根据响应标志头的 content-type 来展示文本,图片,视频,等数据。

​ 如果浏览器可以展示这个数据,它就会展示出这个对应的数据类型,(图片,文本,视频),当然如果不支持,那它只能使用默认方式给你下载到本地(有可能下载一个没有后缀名的文件)

什么是TS文件_百度百科

ts文件的响应标志头,写着响应

image-20211127002759732

mp4文件的响应标志头

image-20211127003047709

json文本数据的响应标志头

image-20211127003209046

综上,我们也可以通过 其它方式 访问以上的 url路径来保存 ts数据流, 进而获取到 .ts后缀的ts视频文件。

到这里就不再分析了,否则又过线了呀!

三、总结

学习前端是如何对分段的视频数据处理,对前后端分离的职责会更加清晰。也许,小编的文章不是非常的优秀,但希望看完篇文件对你有帮助,要不点点赞?

最后,附上源码链接

实现视频流分段 · 小野猫/ShareCode - 码云 - 开源中国