来源

最近项目中需要前端播放 .ts 格式视频,捣鼓了几天学习到很多知识,也发掘了一种未曾见过的优秀解决方案,分享给有同样需求的同学。

常见方案

在网上查找的大部分解决方案都是用诸如videojs等网页播放器,接收 .m3u8索引文件的方式来播放ts切片。这种方案的缺点是需要后端对原始ts切片做处理,生成 .m3u8索引文件

  1. ffmpeg -i source.ts -c copy -map 0 -f segment -segment_list playlist.m3u8 -segment_time 10 output%03d.ts

项目中已存储的 .ts 切片数量众多,已经占用了NAS服务器绝大部分的资源,生成的 .m3u8 索引虽然非常小,但会生成一堆切片后的新 .ts 视频,例如上述指令将会生成一堆 10s 长度的 ts新切片。出于各种考虑后端的同学拒绝了这种重新生成新切片加索引的方案。

邪道方案

在我们的项目中,每一个ts切片已经就是一个独立内容的视频了,时长在20s以内,因此其实不用切割,只需要生成一个索引文件就可以了, .m3u8格式如下:

  1. #EXTM3U
  2. #EXT-X-VERSION:3
  3. #EXT-X-MEDIA-SEQUENCE:0
  4. #EXT-X-ALLOW-CACHE:YES
  5. #EXT-X-TARGETDURATION:93
  6. #EXTINF:92.008578,
  7. test.ts
  8. #EXT-X-ENDLIST

定义好的时长并不影响最终网页播放器计算出的时长,因此可以取一个统一的极大值,整体上就只有倒数第二行的ts文件名需要根据不同 ts 视频修改,可以用脚本统一生成所有ts文件的索引文件。这个方案极其low,当然也被后端同学拒绝了。

插件方案

VLC Web Plugin,一个需要VLC播放器以及浏览器插件的方案,并且不支持Chrome,使用复杂,感兴趣的同学可以自行尝试。

优雅方案

在中文互联网搜索无果后,果断转向了Google,然而也未果,正当我绝望地准备调整心态,接受下载后VLC播放的保底方案时,终于发现了一丝线索,在vediojs的Github页面中,Issue1441Issue4297 中,面对videojs能否直接播放 .ts 的疑问,开发团队都表示虽然库本身没有直接的相关实现,但可以利用相关的逻辑自行实现。最重要的是都指出了mux.js这一工具。根据实测,只用这一个库即可在web端直接播放 .ts 视频,如下是它的转化流程。
播放.ts - 图1
Transfer Diagram

代码示例

示例中是以 ajax 的方式接收 .ts 二进制数据,mux.js引入方式可以直接标签引入,也可以npm install mux.js后 import进页面。

  1. var $ = document.querySelector.bind(document);
  2. var vjsParsed,
  3. video,
  4. mediaSource;
  5. // 定义通用的事件回调处理函数,只做打印事件类型
  6. function logevent (event) {
  7. console.log(event);
  8. }
  9. // ajax
  10. let xhr = new XMLHttpRequest();
  11. xhr.open('GET', "./test.ts");
  12. // 接收的是 video/mp2t 二进制数据,并且arraybuffer类型方便后续直接处理
  13. xhr.responseType = "arraybuffer";
  14. xhr.send();
  15. xhr.onreadystatechange = function () {
  16. if (xhr.readyState ==4) {
  17. if (xhr.status == 200) {
  18. transferFormat(xhr.response);
  19. } else {
  20. console.log('error');
  21. }
  22. }
  23. }
  24. function transferFormat (data) {
  25. // 将源数据从ArrayBuffer格式保存为可操作的Uint8Array格式
  26. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
  27. var segment = new Uint8Array(data);
  28. var combined = false;
  29. // 接收无音频ts文件,OutputType设置为'video',带音频ts设置为'combined'
  30. var outputType = 'video';
  31. var remuxedSegments = [];
  32. var remuxedBytesLength = 0;
  33. var remuxedInitSegment = null;
  34. // remux选项默认为true,将源数据的音频视频混合为mp4,设为false则不混合
  35. var transmuxer = new muxjs.mp4.Transmuxer({remux: false});
  36. // 监听data事件,开始转换流
  37. transmuxer.on('data', function(event) {
  38. console.log(event);
  39. if (event.type === outputType) {
  40. remuxedSegments.push(event);
  41. remuxedBytesLength += event.data.byteLength;
  42. remuxedInitSegment = event.initSegment;
  43. }
  44. });
  45. // 监听转换完成事件,拼接最后结果并传入MediaSource
  46. transmuxer.on('done', function () {
  47. var offset = 0;
  48. var bytes = new Uint8Array(remuxedInitSegment.byteLength + remuxedBytesLength)
  49. bytes.set(remuxedInitSegment, offset);
  50. offset += remuxedInitSegment.byteLength;
  51. for (var j = 0, i = offset; j < remuxedSegments.length; j++) {
  52. bytes.set(remuxedSegments[j].data, i);
  53. i += remuxedSegments[j].byteLength;
  54. }
  55. remuxedSegments = [];
  56. remuxedBytesLength = 0;
  57. // 解析出转换后的mp4相关信息,与最终转换结果无关
  58. vjsParsed = muxjs.mp4.tools.inspect(bytes);
  59. console.log('transmuxed', vjsParsed);
  60. prepareSourceBuffer(combined, outputType, bytes);
  61. });
  62. // push方法可能会触发'data'事件,因此要在事件注册完成后调用
  63. transmuxer.push(segment); // 传入源二进制数据,分割为m2ts包,依次调用上图中的流程
  64. // flush的调用会直接触发'done'事件,因此要事件注册完成后调用
  65. transmuxer.flush(); // 将所有数据从缓存区清出来
  66. }
  67. function prepareSourceBuffer (combined, outputType, bytes) {
  68. var buffer;
  69. video = document.createElement('video');
  70. video.controls = true;
  71. // MediaSource Web API: https://developer.mozilla.org/zh-CN/docs/Web/API/MediaSource
  72. mediaSource = new MediaSource();
  73. video.src = URL.createObjectURL(mediaSource);
  74. $('#video-wrapper').appendChild(video); // 将H5 video元素添加到对应DOM节点下
  75. // 转换后mp4的音频格式 视频格式
  76. var codecsArray = ["avc1.64001f", "mp4a.40.5"];
  77. mediaSource.addEventListener('sourceopen', function () {
  78. // MediaSource 实例默认的duration属性为NaN
  79. mediaSource.duration = 0;
  80. // 转换为带音频、视频的mp4
  81. if (combined) {
  82. buffer = mediaSource.addSourceBuffer('video/mp4;codecs="' + 'avc1.64001f,mp4a.40.5' + '"');
  83. } else if (outputType === 'video') {
  84. // 转换为只含视频的mp4
  85. buffer = mediaSource.addSourceBuffer('video/mp4;codecs="' + codecsArray[0] + '"');
  86. } else if (outputType === 'audio') {
  87. // 转换为只含音频的mp4
  88. buffer = mediaSource.addSourceBuffer('audio/mp4;codecs="' + (codecsArray[1] ||codecsArray[0]) + '"');
  89. }
  90. buffer.addEventListener('updatestart', logevent);
  91. buffer.addEventListener('updateend', logevent);
  92. buffer.addEventListener('error', logevent);
  93. video.addEventListener('error', logevent);
  94. // mp4 buffer 准备完毕,传入转换后的数据
  95. // 将 bytes 放入 MediaSource 创建的sourceBuffer中
  96. // https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/appendBuffer
  97. buffer.appendBuffer(bytes);
  98. // 自动播放
  99. // video.play();
  100. });
  101. };

IE8及以上 、 IE Edge 、Chrome 、 Firefox 浏览器下均能正常播放。希望本文能帮到各位开发同学。