无插件版-web前端流媒体播放器实现经验分享

前言

音视频的技术深似海,假如我们需要实现一个m3u8,flv协议的视频播放器,在安防/直播/在线教育领域中,音视频的开发需求,技术壁垒要求都比较高,众所周知,这些协议类型无法在web上的video被直接播放和渲染出来。

视频编码格式-H264

H264又被称为AVC,是一种编码协议。

视频编码格式-H265

H265/HEVC,是H264的全面升级版本,保留了H264的某些技术优点,同时又有改善码流,编码质量,优化带宽等升级。

什么是硬解,什么是软解。

硬件解码:字面上理解就是用硬件解码。通过显卡的视频加速功能对高清视频进行解码。可以理解为有一个专门的电路板来进行视频的解码工作,是依靠GPU。
调用GPU的专门模块编码来解码,减少CPU运算。显卡核心GPU拥有独特的计算方法,解码效率非常高,这样不但能够减轻CPU的负担,还有着低功耗,发热少等特点。
可以理解成,部分电脑上支持硬解方案,部分电脑不行,但是对于用户来说,需要考虑可用性,硬解不成立的话要考虑软解方案。

软件解码:顾名思义就是通过软件解码,实际上还是要有GPU硬件支撑,一般采用webAssebmly的形式解成客户端可以接收的编码格式。

什么是YUV数据。

YUV,是一种颜色编码方法。常使用在各个影像处理元件中。 YUV在对照片或影片编码时,考虑到人类的感知能力,允许降低色度的带宽。

YUV是编译true-color颜色空间(color space)的种类,Y’UV, YUV, YCbCr,YPbPr等专有名词都可以称为YUV,彼此有重叠。 “Y”表示明亮度(Luminance、Luma),“U”和“V”则是色度、浓度(Chrominance、Chroma),Y′UV, YUV, YCbCr, YPbPr所指涉的范围,常有混淆或重叠的情况。 从历史的演变来说,其中YUV和Y’UV通常用来编码电视的模拟信号,而YCbCr则是用来描述数位的影像信号,适合影片与图片压缩以及传输,例如MPEG、JPEG。 但在现今,YUV通常已经在电脑系统上广泛使用。

Y’代表明亮度(luma; brightness)而U与V储存色度(色讯; chrominance; color)部分; 亮度(luminance)记作Y,而Y’的prime符号记作伽玛校正。

YUV Formats分成两个格式:

紧缩格式(packed formats):将Y、U、V值储存成Macro Pixels阵列,和RGB的存放方式类似。

平面格式(planar formats):将Y、U、V的三个分量分别存放在不同的矩阵中。

紧缩格式(packed format)中的YUV是混合在一起的,对于YUV4:4:4格式而言,用紧缩格式很合适的,因此就有了UYVY、YUYV等。

平面格式(planar formats)是指每Y分量,U分量和V分量都是以独立的平面组织的,也就是说所有的U分量必须在Y分量后面,而V分量在所有的U分量后面,此一格式适用于采样(subsample)。平面格式(planar format)有I420(4:2:0)、YV12、IYUV等。

常见的音频类型

常用的音频格式有MP3、WAV、WMA、MP2、Flac、MIDI、RA、APE、AAC、CDA、MOV,G711a等文件。

AAC:AAC文件全称Advanced Audio Coding,被称为高级音频编码。AAC文件也是属于一种有埙压缩格式,但是与MP3不同的是,它采用了全新的算法进行编码,利用AAC编码,可使人感觉声音质量没有明显降低的前提下,文件更小。
虽然它可以提供更好的音质,但是其效果还是不可以与APE和FLAC等格式文件相比。

关于音频解码库的详细介绍:blog.csdn.net/unique_no1/…

私有协议的流格式处理

例如后端给到一个私有协议的裸流格式,一般会提供每个字节的解码数据说明的文档,循环读取每个字节的数据,最终将视频数据,音频数据,描述性信息等等数据拿到之后组装成一个变量,分别做处理,音频数据转化为浏览器可以识别的数据。视频流数据可以转化为YUV数据,最终被浏览器渲染出来。

私有协议数据的获取,一般是读成ArrayBuffer的形式,再转为DataView,下文有说明。

解码过程跟现有开源框架flvjs等是差不多的思想。

JS对于二进制流数据的处理

ArrayBuffer对象、TypedArray视图和DataView视图是 JavaScript 操作二进制数据的一个接口。它们都是以数组的语法处理二进制数据,所以统称为二进制数组。

二进制数组由三类对象组成。

ArrayBuffer对象代表储存二进制数据的一段内存,它不能直接读写,只能通过视图(TypedArray视图和DataView视图)来读写,视图的作用是以指定格式解读二进制数据。

ArrayBuffer也是一个构造函数,可以分配一段可以存放数据的连续内存区域。

**

const buf = new ArrayBuffer(32);

上面代码生成了一段 32 字节的内存区域,每个字节的值默认都是 0。可以看到,ArrayBuffer构造函数的参数是所需要的内存大小(单位字节)。

为了读写这段内容,需要为它指定视图。DataView视图的创建,需要提供ArrayBuffer对象实例作为参数。

**

const buf = new ArrayBuffer(32);
const dataView = new DataView(buf);
dataView.getUint8(0) // 0, 参数表示读取的起始位置

上面代码对一段 32 字节的内存,建立DataView视图,然后以不带符号的 8 位整数格式,从头读取 8 位二进制数据,结果得到 0,因为原始内存的ArrayBuffer对象,默认所有位都是 0。

另一种TypedArray视图,与DataView视图的一个区别是,它不是一个构造函数,而是一组构造函数,代表不同的数据格式。

**

const buffer = new ArrayBuffer(12);

const x1 = new Int32Array(buffer);
x1[0] = 1;
const x2 = new Uint8Array(buffer);
x2[0] = 2;

console.log(x1[0]); //2

上面代码对同一段内存,分别建立两种视图:32 位带符号整数(Int32Array构造函数)和 8 位不带符号整数(Uint8Array构造函数)。由于两个视图对应的是同一段内存,一个视图修改底层内存,会影响到另一个视图。

TypedArray视图的构造函数,除了接受ArrayBuffer实例作为参数,还可以接受普通数组作为参数,直接分配内存生成底层的ArrayBuffer实例,并同时完成对这段内存的赋值。

**

const typedArray = new Uint8Array([0,1,2]);
console.log(typedArray.length);//3


typedArray[0] = 5;
console.log(typedArray);//Uint8Array [ 5, 1, 2 ]

上面代码使用TypedArray视图的Uint8Array构造函数,新建一个不带符号的 8 位整数视图。可以看到,Uint8Array直接使用普通数组作为参数,对底层内存的赋值同时完成。

ArrayBuffer.prototype.byteLength

ArrayBuffer实例的byteLength属性,返回所分配的内存区域的字节长度。

**

const buff = new ArrayBuffer(32);
console.log(buff.byteLength);//32

如果要分配的内存区域很大,有可能分配失败(因为没有那么多的连续空余内存),所以有必要检查是否分配成功。

**

if (buffer.byteLength === n) {
  // 成功
} else {
  // 失败
}

ArrayBuffer.prototype.slice()

ArrayBuffer实例有一个slice方法,允许将内存区域的一部分,拷贝生成一个新的ArrayBuffer对象。

**

const buff = new ArrayBuffer(32);
console.log(buff.byteLength);//32
const newBuffer = buff.slice(0, 3);

上面代码拷贝buffer对象的前 3 个字节(从 0 开始,到第 3 个字节前面结束),生成一个新的ArrayBuffer对象。

slice方法其实包含两步,第一步是先分配一段新内存,第二步是将原来那个ArrayBuffer对象拷贝过去。

slice方法接受两个参数,第一个参数表示拷贝开始的字节序号(含该字节),第二个参数表示拷贝截止的字节序号(不含该字节)。如果省略第二个参数,则默认到原ArrayBuffer对象的结尾。

除了slice方法,ArrayBuffer对象不提供任何直接读写内存的方法,只允许在其上方建立视图,然后通过视图读写。

ArrayBuffer.isView()

ArrayBuffer有一个静态方法isView,返回一个布尔值,表示参数是否为ArrayBuffer的视图实例。这个方法大致相当于判断参数,是否为TypedArray实例或DataView实例。

**

const buffer = new ArrayBuffer(8);
ArrayBuffer.isView(buffer) // false


const v = new Int32Array(buffer);
ArrayBuffer.isView(v) // true

原文:www.jianshu.com/p/40d4a4d13…

Wasm的调用

这里只说个人的方法。网上的材料参差不齐,这里选择了,自己最合适的引入方法。

一般ffmpeg,打出来的胶水代码有两个,一个是引用的js文件,一个是wasm二进制文件。

importScripts("./webcodec.js");

self.Module.onRuntimeInitialized = () => { console.log("wasm地址 wasm loaded..."); }
如何设置回调函数,直接,使用Module.addFunction这个方法去挂载。

Module.addFunction(自己的方法, "vi")

viiiiiii这种形式,第一个v标识返回值是void,后边的几个i表示几个参数

还有一种调用的方式,Module.ccall这个方法。此处省略。

线程模型的搭建,线程消息的发送和接收。(webWorker)

我的项目中的线程模型是这样的。

主线程(渲染线程)请求线程-视频解码线程-音频解码线程
image.png

由于音频和视频的解码数据是一种要被消费的,需要一直执行,需要新开一个线程独立出来,不能阻塞正常的画面渲染,真正的画面渲染,全部交给了主线程来实现。

页面的渲染

使用webGl渲染Yuv数据。

具体可以参考:zhuanlan.zhihu.com/p/377713635

后续会上源码

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYQwT7lu' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片