几年前, 我开始从事于完全用 JavaScript 编写的 MPEG1 视频解码器上现在, 我终于找到了清理该库的时间, 改善其性能使其具有更高的错误恢复能力和模块化能力, 并添加 MP2 音频解码器和 MPEG-TS 解析器这使得该库不仅仅是一个 MPEG 解码器, 而是一个完整的视频播放器
在本篇博文中, 我想谈一谈我在开发这个库时遇到的挑战和各种有趣的事情你将在官方网站上找到 demo 源代码和文档以及为什么要使用 JSMpeg:
最近, 我需要为一位客户在 JSMpeg 中实现音频流传输, 然后我才意识到该库处于一种多么可怜的状态从其首次发布以来, 它已经有很多发展了在过去的几年里, webGL 渲染器 WebSocket 客户端渐进式加载基准测试设备等等已被加入但所有这些都保存在一个单一的庞大的类中, 条件判断随处可见
我决定首先通过分离它的逻辑组件来梳理清楚其中的混乱我还总结了完成实现需要哪些: 解复用器 MP2 解码器和音频输出:
源代码(Sources): AJAX, 渐进式 AJAX 和 WebSocket
解复用器(Demuxer): MPEG-TS (Transport Stream)
解码器(Decoder): MPEG1 视频 & MP2 音频
渲染器(Render): Canvas2D & WebGL
音频输出: WebAudio
加上一些辅助类:
一个位缓存(Bit Buffer), 用于管理原始数据
一个播放器(Player), 整合其他组件
每个组件 (除了 Sources 之外) 都有一个. write(buffer)方法来为其提供数据这些组件可以连接到接收处理结果的目标组件上流经该库的完整流程如下所示:
- / -> MPEG1 Video Decoder -> Renderer
- Source -> Demuxer
- \ -> MP2 Audio Decoder -> Audio Output
JSMpeg 目前有 3 种不同的 Source 实现 (AJAX\AJAX 渐进式和 WebSocket), 还有 2 种不同的渲染器(Canvas2D 和 WebGL) 该库的其他部分对这此并不了解 - 即视频解码器不关心渲染器内部逻辑采用这种方法可以轻松添加新的组件: 更多的 Source, 解复用器, 解码器或输出
我对这些连接在库中的工作方式并不完全满意每个组件只能有一个目标组件 (除了多路解复用器, 每个流有都有一个目标组件) 这是一个折衷最后, 我觉得: 其他部分会因为没有充分的理由而过度工程设计并使得库过于复杂化
WebGL 渲染
MPEG1 解码器中计算密集度最高的任务之一是将 MPEG 内部的 YUV 格式 (准确地说是 Y'Cr'Cb) 转换为 RGBA, 以便浏览器可以显示它简而言之, 这个转换看起来像这样:
- for (var i = 0; i <pixels.length; i+=4 ) {
- var y, cb, cr = /* fetch this from the YUV buffers */;
- pixels[i + 0 /* R */] = y + (cb + ((cb * 103)>> 8)) - 179;
- pixels[i + 1 /* G */] = y - ((cr * 88)>> 8) - 44 + ((cb * 183)>> 8) - 91;
- pixels[i + 2 /* B */] = y + (cr + ((cr * 198)>> 8)) - 227;
- pixels[i + 4 /* A */] = 255;
- }
对于单个 1280x720 视频帧, 该循环必须执行 921600 次以将所有像素从 YUV 转换为 RGBA 每个像素需要对目标 RGB 数组写入 3 次 (我们可以预先填充 alpha 组件, 因为它始终是 255) 这是每帧 270 万次写入操作, 每次需要 5-8 次加减乘和位移运算对于一个 60fps 的视频, 我们 每秒钟完成 10 亿次以上的操作 再加上 JavaScript 的开销 JavaScript 可以做到这一点, 计算机可以做到这一点, 这一事实仍然让我大开眼界
使用 WebGL , 这种颜色转换 (以及随后在屏幕上显示) 可以大大加快逐像素的少量操作对 GPU 而言是小菜一碟 GPU 可以并行处理多个像素, 因为它们是独立于任何其他像素的运行在 GPU 上的 WebGL 着色器 (shader) 甚至不需要这些烦人的位移 - GPU 喜欢浮点数:
- void main() {
- float y = texture2D(textureY, texCoord).r;
- float cb = texture2D(textureCb, texCoord).r - 0.5;
- float cr = texture2D(textureCr, texCoord).r - 0.5;
- gl_FragColor = vec4(
- y + 1.4 * cb,
- y + -0.343 * cr - 0.711 * cb,
- y + 1.765 * cr,
- 1.0
- );
- }
使用 WebGL, 颜色转换所需的时间从 JS 总时间的 50% 下降到仅需 YUV 纹理上传时间的约 1%
我遇到了一个与 WebGL 渲染器偶然相关的小问题 JSMpeg 的视频解码器不会为每个颜色平面生成三个 Uint8Arrays , 而是一个 Uint8ClampedArrays 它是这样做的, 因为 MPEG1 标准规定解码的颜色值必须是紧凑的, 而不是分散的让浏览器通过 ClampedArray 进行交织比在 JavaScript 中执行更快
依然存在于某些浏览器 (Chrome 和 Safari) 中的缺陷会阻止 WebGL 直接使用 Uint8ClampedArray 因此, 对于这些浏览器, 我们必须为每个帧的每个数组创建一个 Uint8Array 视图这个操作非常快, 因为没有需要真实复制的事情, 但我仍然希望不使用它
JSMpeg 会检测到这个错误, 并仅在需要时使用该解决方法我们只是尝试上传一个固定数组并捕获此错误令人遗憾的是, 这种检测会触发控制台中的一个非静默的警告, 但这总比没有好吧
- WebGLRenderer.prototype.allowsClampedTextureData = function() {
- var gl = this.gl;
- var texture = gl.createTexture();
- gl.bindTexture(gl.TEXTURE_2D, texture);
- gl.texImage2D(
- gl.TEXTURE_2D, 0, gl.LUMINANCE, 1, 1, 0,
- gl.LUMINANCE, gl.UNSIGNED_BYTE, new Uint8ClampedArray([0])
- );
- return (gl.getError() === 0);
- };
对直播流媒体的 WebAudio
很长一段时间里, 我假设为了向 WebAudio 提供原始 PCM 样本数据而没有太多延迟或爆破音, 你需要使用 ScriptProcessorNode 只要你从脚本处理器获得回调, 你就可以及时复制解码后的采样数据这确实有效我试过这个方法它需要相当多的代码才能正常工作, 当然这是计算密集型和不优雅的作法
幸运的是, 我最初的假设是错误的
WebAudio 上下文维护自己的计时器, 它有别于 JavaScript 的 Date.now()或 performance.now() 此外, 你可以根据上下文的时间指导你的 WebAudio 源在未来的准确时间调用 start()有了这个, 你可以将非常短的 PCM 缓冲器串在一起, 而不会有任何瑕疵
你只需计算下一个缓冲区的开始时间, 就可以连续添加所有之前的缓冲区的时间总是使用 WebAudio Context 自己的时间来做这件事是很重要的
- var currentStartTime = 0;
- function playBuffer(buffer) {
- var source = context.createBufferSource();
- /* load buffer, set destination etc. */
- var now = context.currentTime;
- if (currentStartTime < now) {
- currentStartTime = now;
- }
- source.start(currentStartTime);
- currentStartTime += buffer.duration;
- }
不过需要注意的是: 我需要获得队列音频的精确剩余时间我只是简单地将它作为当前时间和下一个启动时间的区别来实现:
- // Don't do that!
- var enqueuedTime = (currentStartTime - context.currentTime);
我花了一段时间才弄明白, 这行不通你可以看到, 上下文的 currentTime 只是每隔一段时间才更新一次它不是一个精确的实时值
- var t1 = context.currentTime;
- doSomethingForAWhile();
- var t2 = context.currentTime;
- t1 === t2; // true
因此, 如果需要精确的音频播放位置(或者基于它的任何内容), 你必须恢复到 JavaScript 的 performance.now() 方法
iOS 上的音频解锁
你将要爱上苹果时不时扔到 Web 开发人员脸上的麻烦其中之一就是在播放任何内容之前都需要在页面上解锁音频总的来说, 音频播放只能作为对用户操作的响应而启动你点击了一个按钮, 音频则播放了
这是有道理的我不反驳它当你访问某个网页时, 你不希望在未经通知的情况下发出声音
是什么让它变得糟糕透顶呢? 是因为苹果公司既没有提供一种利索的解锁音频的方法, 也没有提供一种方法来查询 WebAudio Context 是否已经解锁你所要做的就是播放一个音频源并不断检查是否正在顺序播放尽管如此, 在播放之后你还不能马上检查是的, 你必须等一会!
- WebAudioOut.prototype.unlock = function(callback) {
- // This needs to be called in an onclick or ontouchstart handler!
- this.unlockCallback = callback;
- // Create empty buffer and play it
- var buffer = this.context.createBuffer(1, 1, 22050);
- var source = this.context.createBufferSource();
- source.buffer = buffer;
- source.connect(this.destination);
- source.start(0);
- setTimeout(this.checkIfUnlocked.bind(this, source, 0), 0);
- };
- WebAudioOut.prototype.checkIfUnlocked = function(source, attempt) {
- if (
- source.playbackState === source.PLAYING_STATE ||
- source.playbackState === source.FINISHED_STATE
- ) {
- this.unlocked = true;
- this.unlockCallback();
- }
- else if (attempt < 10) {
- // Jeez, what a shit show. Thanks iOS!
- setTimeout(this.checkIfUnlocked.bind(this, source, attempt+1), 100);
- }
- };
来源: http://www.tuicool.com/articles/uYzAjey