@(音视频)[Audio|Video|MSE]
音视频随着互联网的发展,对音视频的需求越来越多,然而音视频无乱是播放还是编解码,封装对性能要求都比较高,那现阶段的前端再音视频领域都能做些什么呢。
[TOC]
音频或视频的播放
html5 audio
提起音视频的播放,我萌首先想到的是 HTMLMediaElement,video 播放视频,audio 播放音频。举个栗子:
<audio controls autoplay loop=”true” preload=”auto” src=”audio.mp3″></audio>
controls 指定浏览器渲染成 html5 audio.
autoplay 属性告诉浏览器,当加载完的时候,自动播放.
loop 属性循环播放.
preload 当渲染到 audio 元素时,便加载音频文件.
移动端的浏览器并不支持 autoplay 和 preload 属性,即不会自动加载音频文件,只有通过一些事件触发,比如 touch、click 事件等触发加载然后播放.
媒体元素还有一些改变音量,某段音频播放完成事件等,请阅读 HTMLMediaElement.
当然如果你的网页是跑在 WebView 中,可以让客户端设置一些属性实现预加载和自动播放。
AudioContext
虽然使用 html5 的 audio 可以播放音频,但是正如你看到存在很多问题,同时我萌不能对音频的播放进行很好的控制,比如说从网络中获取到音频二进制数据,有的时候我萌想顺序播放多段音频,对于使用 audio 元素也是力不从心,处理起来并不优雅。举个栗子:
function queuePlayAudio(sounds) {
let index = 0;
function recursivePlay(sounds, index) {
if(sounds.length == index) return;
sounds[index].play();
sounds[index].onended = recursivePlay.bind(this, sounds, ++index);
}
}
监听 audio 元素的 onended 事件,顺序播放。
为了更好的控制音频播放,我萌需要 AudioContext.
AudioContext 接口表示由音频模块连接而成的音频处理图,每个模块对应一个 AudioNode。AudioContext 可以控制它所包含的节点的创建,以及音频处理、解码操作的执行。做任何事情之前都要先创建 AudioContext 对象,因为一切都发生在这个环境之中。
可能理解起来比较晦涩,简单的来说,AudioContext 像是一个工厂,对于一个音频的播放,从音源到声音控制,到链接播放硬件的实现播放,都是由各个模块负责处理,通过 connect 实现流程的控制。
现在我萌便能实现音频的播放控制,比如从网络中获取。利用 AJAX 中获取 arraybuffer 类型数据,通过解码,然后把音频的二进制数据传给 AudioContext 创建的 BufferSourceNode,最后通过链接 destination 模块实现音频的播放。
export default class PlaySoundWithAudioContext {
constructor() {
if(PlaySoundWithAudioContext.isSupportAudioContext()) {
this.duration = 0;
this.currentTime = 0;
this.nextTime = 0;
this.pending = [];
this.mutex = false;
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
}
static isSupportAudioContext() {
return window.AudioContext || window.webkitAudioContext;
}
play(buffer) {
var source = this.audioContext.createBufferSource();
source.buffer = buffer;
source.connect(this.audioContext.destination);
source.start(this.nextTime);
this.nextTime += source.buffer.duration;
}
addChunks(buffer) {
this.pending.push(buffer);
let customer = () => {
if(!this.pending.length) return;
let buffer = this.pending.shift();
this.audioContext.decodeAudioData(buffer, buffer => {
this.play(buffer);
console.log(buffer)
if(this.pending.length) {
customer()
}
}, (err) => {
console.log(‘decode audio data error’, err);
});
}
if(!this.mutex) {
this.mutex = true;
customer()
}
}
clearAll() {
this.duration = 0;
this.currentTime = 0;
this.nextTime = 0;
}
}
AJAX 调用
function xhr() {
var XHR = new XMLHttpRequest();
XHR.open(‘GET’, ‘//example.com/audio.mp3’);
XHR.responseType = ‘arraybuffer’;
XHR.onreadystatechange = function(e) {
if(XHR.readyState == 4) {
if(XHR.status == 200) {
playSoundWithAudioContext.addChunks(XHR.response);
}
}
}
XHR.send();
}
使用 Ajax 播放对于小段的音频文件还行,但是一大段音频文件来说,等到下载完成才播放,不太现实,能否一边下载一边播放呢。这里就要利用 fetch 实现加载 stream 流。
fetch(url).then((res) => {
if(res.ok && (res.status >= 200 && res.status <= 299)) {
readData(res.body.getReader())
} else {
that.postMessage({type: constants.LOAD_ERROR})
}
})
function readData(reader) {
reader.read().then((result) => {
if(result.done) {
return;
}
console.log(result);
playSoundWithAudioContext.addChunks(result.value.buffer);
})
}
简单的来说,就是 fetch 的 response 返回一个 readableStream 接口,通过从中读取流,不断的喂给 audioContext 实现播放,测试发现移动端不能顺利实现播放,pc 端浏览器可以。
PCM audio
实现 audioContext 播放时,我萌需要解码,利用 decodeAudioDataapi 实现解码,我萌都知道,一般音频都要压缩成 mp3,aac 这样的编码格式,我萌需要先解码成 PCM 数据才能播放,那 PCM 又是什么呢?我萌都知道,声音都是由物体振动产生,但是这样的声波无法被计算机存储计算,我萌需要使用某种方式去刻画声音,于是乎便有了 PCM 格式的数据,表示麦克风采集声音的频率,采集的位数以及声道数,立体声还是单声道。
Media Source Extensions
Media Source Extensions 可以动态的给 Audio 和 Video 创建 stream 流,实现播放,简单的来说,可以很好的播放进行控制,比如再播放的时候实现 seek 功能什么的,也可以在前端对某种格式进行转换进行播放,并不是支持所有的格式的。
通过将数据 append 进 SourceBuffer 中,MSE 把这些数据存进缓冲区,解码实现播放。这里简单的举个使用 MSE 播放 audio 的栗子:
export default class PlaySoundWithMSE{
constructor(audio) {
this.audio = audio;
if(PlaySoundWithMSE.isSupportMSE()) {
this.pendingBuffer = [];
this._mediaSource = new MediaSource();
this.audio.src = URL.createObjectURL(this._mediaSource);
this._mediaSource.addEventListener(‘sourceopen’, () => {
this.sourcebuffer = this._mediaSource.addSourceBuffer(‘audio/mpeg’);
this.sourcebuffer.addEventListener(‘updateend’,
this.handleSourceBufferUpdateEnd.bind(this));
})
}
}
addBuffer(buffer) {
this.pendingBuffer.push(buffer);
}
handleSourceBufferUpdateEnd() {
if(this.pendingBuffer.length) {
this.sourcebuffer.appendBuffer(this.pendingBuffer.shift());
} else {
this._mediaSource.endOfStream();
}
}
static isSupportMSE() {
return !!window.MediaSource;
}
}
HTML5 播放器
谈起 html5 播放器,你可能知道 bilibili 的 flv.js, 它便是依赖 Media Source Extensions 将 flv 编码格式的视频转包装成 mp4 格式,然后实现播放。
从流程图中可以看到,IOController 实现对视频流的加载,这里支持 fetch 的 stream 能力,WebSocket 等,将得到的视频流,这里指的是 flv 格式的视频流,将其转封装成 MP4 格式,最后将 MP4 格式的数据通过 appendBuffer 将数据喂给 MSE, 实现播放。
未来
上面谈到的都是视频的播放,你也看到,即使播放都存在很多限制,MSE 的浏览器支持还不多,那在视频的编码解码这些要求性能很高的领域,前端能否做一些事情呢?前端性能不高有很多原因,在浏览器这样的沙盒环境下,同时 js 这种动态语言,性能不高,所以有大佬提出把 c ++ 编译成 js , 然后提高性能,或许你已经知道我要说的是什么了,它就是 ASM.js,它是 js 的一种严格子集。我萌可以考虑将一些视频编码库编译成 js 去运行提高性能,其中就不得不提到的 FFmpeg, 可以考虑到将其编译成 asm, 然后对视频进行编解码。
写在最后
我萌可以看到,前端对音视频的处理上由于诸多原因,可谓如履薄冰,但是在视频播放上,随着浏览器的支持,还是可以有所作为的。
招纳贤士
今日头条长期大量招聘前端工程师,可选北京、深圳、上海、厦门等城市。欢迎投递简历到 tcscyl@gmail.com / yanglei.yl@bytedance.com