前言
针对现如今移动端设备音频广泛的应用场景,我们在测试相应程序的过程中不可避免的需要对音频能力进行验证,为了能够在云真机测试平台中完成不同场景下的音频测试,就需要实时获取客户端设备中播放的音频,进而能够直接通过 Web 端平台进行测试。
根据应用场景,需要从移动端设备中读取音频数据,即获取到音频裸数据 PCM (Pulse Code Modulation) 数据,然后将 PCM 数据通过一系列方式,最终发送到 Web 端进行播放。
技术选型
编码技术
由于获取到的音频数据是裸数据(PCM数据),如果将其存储在本地磁盘中,音频文件的体积是可接受的,但是我们应用的场景是实时传输,直接传输 PCM 数据必然会有传输数据量过大的问题,因此需要使用压缩编码技术对裸数据进行压缩后传输。常见的编码技术有 AAC、MP3、WAV 和 WMA 等等。
通过参考 音频编码方案之间音质比较(AAC,MP3,WMA等) 文章可以看出,在码率较低等情况下,不同编码方案的音频的音质排序为:AAC+ > MP3PRO > AAC > RealAudio > WMA > MP3。
由于 AAC 是一种高压缩比的音频压缩算法,但它的压缩比要远超过较老的音频压缩算法,如 AC-3、MP3 等。并且其质量可以同未压缩的 CD 音质相媲美。因此最终选择 AAC 编码技术对 PCM 数据进行压缩。
前端解码方案
选择使用 jMuxer 作为解码器,是一个基于 MSE 技术的 JavaScript 开源库,允许 JavaScript 动态构建 <video>
和 <audio>
的媒体流,支持对接收到的 AAC 数据进行解码,并且有着接近原生的解码速度。
AAC音频编码技术
AAC是高级音频编码(Advanced Audio Coding)的缩写,出现于1997年,最初是基于MPEG-2的音频编码技术。由Fraunhofer IIS、Dolby Laboratories、AT&T、Sony等公司共同开发,目的是取代MP3格式。2000年,MPEG-4标准出台,AAC重新集成了其它技术(PS,SBR),为区别于传统的MPEG-2 AAC,故含有SBR或PS特性的AAC又称为MPEG-4 AAC。
AAC 是新一代的音频有损压缩技术,音频文件格式有 ADIF 和 ADTS:
- ADIF:Audio Data Interchange Format 音频数据交换格式。这种格式的特征是可以确定的找到这个音频数据的开始,不需进行在音频数据流中间开始的解码,即它的解码必须在明确定义的开始处进行。故这种格式常用在磁盘文件中。
- ADTS:Audio Data Transport Stream。是 AAC 音频的传输流格式。这种格式的特征是它是一个有同步字的比特流,解码可以在这个流中任何位置开始。
总的来说,ADTS 可以在任意帧解码,也就是说它每一帧都有头信息。ADIF 只有一个统一的头,所以必须得到所有的数据后解码。
对原始帧加上 ADTS 头进行 ADTS 的封装,就形成了 ADTS 帧。 AAC音频文件的每一帧由 ADTS Header和 AAC Audio Data组成。结构体如下:
而本次方案中由于实时传输,将会采用 ADTS 格式的数据帧进行数据传输。一个数据包 ADTS Header 分为:
- 固定头(fixed header):数据每一帧都相同,可参考 AAC的ADTS头文件信息介绍;
- 可变头(variable header):在帧与帧之间可变;
有时候可能我们接收到的 AAC 数据包是不合法的,也就是 ADTS Header 不合法,可以将 Header 通过 P23工具 对数据进行验证。
JMuxer 解码
在通过 websocket 获取到 AAC 数据包时,其实拿到的是16进制的数据,在解码的过程中会转换成10进制数据,最后会转成二进制进行解析。
JMuxer 库 同时支持音视频解码。首先介绍以下 JMuxer 常用的参数和方法。
参数
属性 | 属性值 | 说明 | 默认值 |
---|---|---|---|
node | Tag ID | video/audio 标签 ID | - |
mode | audio/video/both | 解码模式 | both |
flushingTime | 时间(毫秒) | 缓存刷新频率 | 1500 |
maxDelay | 时间(毫秒) | 最大延时 | 500 |
clearBuffer | true/false | 清除Buffer | true |
fps | 帧数 | 视频帧数 | duration |
onReady | 函数 | MSE 就绪后回调 | - |
onError | 函数 | Buffer 异常回调 | - |
debug | true/false | 是否打印日志 | false |
方法
函数 | 参数 | 说明 |
---|---|---|
feed | data object | 对象参数包含 audio, video 和 duration,如果没有提供 duration,将根据 fps 计算。 |
createStream | - | 写入缓冲区,nodeJS可用 |
reset | - | 重置并重新开始 JMuxer |
destroy | - | 实例销毁 |
整体代码实现
整体 Demo 可参考:github.com/Lewage59/pr…
/**
* 音频接受处理器
*/
import JMuxer from 'jmuxer';
import Socket from './socket';
const DEFAULT_WS_URL = 'ws://localhost:8080';
export default class AudioProcessor {
constructor(options) {
const wsUrl = options.wsUrl || DEFAULT_WS_URL
/**
* node: 'player',
* mode: 'audio',
* debug: true,
* flushingTime: 0,
* wsUrl
*/
this.jmuxer = new JMuxer({
mode: 'audio',
flushingTime: 0,
onReady() {
console.log('Jmuxer audio init onReady!');
},
onError(data) {
console.error('Buffer error encountered', data);
},
...options
});
this.audioDom = document.getElementById(options.node)
this.initWebSocket(wsUrl)
}
initWebSocket(url) {
const that = this
this.ws = new Socket({
url,
binaryType: 'arraybuffer',
onmessage: function(event) {
const data = that.parse(event.data);
data && that.jmuxer.feed(data);
}
});
}
/**
* 音频解析
* @param {*} data AAC Buffer 视频流
* @returns
*/
parse(data) {
let input = new Uint8Array(data)
return {
audio: input
};
}
onPlay() {
this.audioDom.load()
const playPromise = this.audioDom.play()
if (playPromise !== undefined) {
playPromise.then(() => {
this.audioDom.play()
})
}
}
onPause() {
this.audioDom.pause()
}
onReload() {
this.audioDom.load()
}
onDestroy() {
this.ws.handleClose()
this.audioDom.pause()
this.jmuxer = null
}
}
而有时候前端需要模拟音视频服务进行调试的情况,首先可以可以通过 FFmpeg 将 MP3 音频转换为 AAC 音频格式,再将音频文件分割成数据帧通过 node 服务启动 websocket 进行传输数据帧,具体案例可参考 Node服务端AAC音频传输
写在最后
音视频相关技术在之前其实一直没有接触过,在这次的预研和项目落地的过程中,也对该技术有了新的认识,因此本文主要是对应用层面的讲解,没有深入探讨每个技术的细节,但我相信后续还会对该音视频技术持续关注。
如果对UI自动化测试、远程控制等感兴趣的小伙伴可以关注一下 Sonic 云真机测试平台。
Sonic,一站式开源分布式集群云真机测试平台,致力服务于中小企业的客户端UI测试(永久免费)