H5实时解码音频并播放

音视频的格式是一个有歧义的说法。我们熟知的诸如Flv、Mp4、Mov啥的都是包装格式,可以理解为一种容器,就像一个盒子。里面放到是经过编码的音视频数据,而这些音视频数据都有自己的编码格式,如AAC、H264、H265等等。
今天要展示的是从直播流中获取到的音频编码数据进行解码并使用H5的音频API进行播放的过程。

这些格式分别是

  1. speex
  2. aac
  3. mp3

这些格式都有开源的解码库,不过都是c库,在H5中需要通过emscripten编译成js执行。

引入头文件

#ifdef USE_SPEEX
#include <speex/speex.h>
#endif
#ifdef USE_AAC
#include "aacDecoder/include/neaacdec.h"
// #include "libfdk-aac/libAACdec/include/aacdecoder_lib.h"
#endif
#ifdef USE_MP3
#include "libmad/mad.h"
//#include "libid3tag/tag.h"
#endif

定义变量

int bufferLength;
int bufferFilled;
u8 *outputBuffer;

#ifdef USE_AAC
    faacDecHandle faacHandle;
#endif
#ifdef USE_SPEEX
    i16 *audioOutput;
    void *speexState;
    SpeexBits speexBits;
#endif
#ifdef USE_MP3
    MP3Decoder mp3Decoder;
#endif

bufferLength 用于指定缓冲区的长度,bufferFilled用于指示缓冲中没有使用的数据,outputBuffer用来存放解码后的数据。
MP3Decoder是自己写的一个类,需要定义这几个成员

mad_stream inputStream;
mad_frame frame;
mad_synth synth;

初始化

    outputBuffer = (u8 *)malloc(bufferLength);
#ifdef USE_SPEEX
    audioOutput = (i16 *)malloc(640);
    auto mode = speex_lib_get_mode(SPEEX_MODEID_WB);
    speexState = speex_decoder_init(mode);
    speex_bits_init(&speexBits);
#endif
#ifdef USE_AAC
    faacHandle = faacDecOpen();
#endif

mp3的初始化

mad_stream_init(&inputStream);
mad_frame_init(&frame);
mad_synth_init(&synth);

解码

input对象中包含了经过协议拆包后的原始音频数据(RTMP协议或Flv格式中的格式)缓冲大小虽然是自己定义,但必须遵循下面的规则
aac:1024的倍数(AAC一帧的播放时间是= 10241000/44100= 22.32ms)
speex:320的倍数(320
1000/16000 = 20ms)
MP3:576的倍数(双声道1152 * 1000 /44100 = 26.122ms)
根据这些数据可以估算缓冲大小引起的音频的延时,然后需要和视频的延迟进行同步。

#ifdef USE_SPEEX
    if (input.length() <= 11)
    {
        memset(output, 0, 640);
    }
    else
    {
        speex_bits_read_from(&speexBits, (const char *)input, 52);
        speex_decode_int(speexState, &speexBits, audioOutput);
        memcpy(output, audioOutput, 640);
    }
    return 640;
#endif
#ifdef USE_AAC
    //0 = AAC sequence header ,1 = AAC raw 
    if (input.readB<1, u8>())
    {
        faacDecFrameInfo frame_info;
        auto pcm_data = faacDecDecode(faacHandle, &frame_info, (unsigned char *)input.point(), input.length());
        if (frame_info.error > 0)
        {
            emscripten_log(1, "!!%s\n", NeAACDecGetErrorMessage(frame_info.error));
        }
        else
        {
        int samplesBytes = frame_info.samples << 1;
        memcpy(output, pcm_data, samplesBytes);
        return samplesBytes;
        }
    }
    else
    {
        unsigned long samplerate;
        unsigned char channels;
        auto config = faacDecGetCurrentConfiguration(faacHandle);
        config->defObjectType = LTP;
        faacDecSetConfiguration(faacHandle,config);
        faacDecInit2(faacHandle, (unsigned char *)input.point(), 4, &samplerate, &channels);
        emscripten_log(0, "aac samplerate:%d channels:%d", samplerate, channels);
    }
#endif

mp3 比较复杂,这里不贴代码了,主要是mad库不能直接调用其提供的API,直播流中的MP3数据和mp3文件的格式有所不同导致。如果本文火的话,我就详细说明。

释放资源

#ifdef USE_AAC
    faacDecClose(faacHandle);
#endif
#ifdef USE_SPEEX
    speex_decoder_destroy(speexState);
    speex_bits_destroy(&speexBits);
    free(audioOutput);
#endif
    free(outputBuffer);

mp3

mad_synth_finish(&synth);
mad_frame_finish(&frame);

播放

创建AudioContext对象

window.AudioContext = window.AudioContext || window.webkitAudioContext;
var context = new window.AudioContext();

创建audioBuffer

var audioBuffers = []
var audioBuffer = context.createBuffer(channels, frameCount, samplerate);

播放音频(带缓冲)

                var playNextBuffer = function() {
                    isPlaying = false;
                    if (audioBuffers.length) {
                        playAudio(audioBuffers.shift());
                    }
                    if (audioBuffers.length > 1) audioBuffers.shift();
                    //console.log(audioBuffers.length)
                };
                var copyAudioOutputArray = resampled ? function(target) {
                    for (var i = 0; i < allFrameCount; i++) {
                        var j = i << 1;
                        target[j] = target[j + 1] = audioOutputArray[i] / 32768;
                    }
                } : function(target) {
                    for (var i = 0; i < allFrameCount; i++) {

                        target[i] = audioOutputArray[i] / 32768;
                    }
                };
                var copyToCtxBuffer = channels > 1 ? function(fromBuffer) {
                    for (var channel = 0; channel < channels; channel++) {
                        var nowBuffering = audioBuffer.getChannelData(channel);
                        if (fromBuffer) {
                            for (var i = 0; i < frameCount; i++) {
                                nowBuffering[i] = fromBuffer[i * (channel + 1)];
                            }
                        } else {
                            for (var i = 0; i < frameCount; i++) {
                                nowBuffering[i] = audioOutputArray[i * (channel + 1)] / 32768;
                            }
                        }
                    }
                } : function(fromBuffer) {
                    var nowBuffering = audioBuffer.getChannelData(0);
                    if (fromBuffer) nowBuffering.set(fromBuffer);
                    else copyAudioOutputArray(nowBuffering);
                };
                var playAudio = function(fromBuffer) {
                    if (isPlaying) {
                        var buffer = new Float32Array(resampled ? allFrameCount * 2 : allFrameCount);
                        copyAudioOutputArray(buffer);
                        audioBuffers.push(buffer);
                        return;
                    }
                    isPlaying = true;
                    copyToCtxBuffer(fromBuffer);
                    var source = context.createBufferSource();
                    source.buffer = audioBuffer;
                    source.connect(context.destination);
                    source.onended = playNextBuffer;
                    //setTimeout(playNextBuffer, audioBufferTime-audioBuffers.length*200);
                    source.start();
                };

其中playNextBuffer 函数用于从缓冲中取出数据
copyAudioOutputArray 函数用于将音频数据转化成浮点数。
copyToCtxBuffer 函数用于将音频数据拷贝进可以播放的缓冲数组中。
这些函数对单声道和双声道进行了处理

var resampled = samplerate < 22050;

对于频率小于22khz的数据,我们需要复制一份,模拟成22khz,因为H5只支持大于22khz的数据。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容

  • 看看自己做的觉得好有食欲啊,不过以后还是少做饭了。昨天和今天把电视剧《我的前半生》看完了,觉得也还好吧,说的感情、...
    馨晴Sophia阅读 121评论 0 0
  • 【今日三只】 1.学习:看书 2.生活学习:心理fm&人民日报&樊登读书&笔记&熊猫小课 3.工作:两篇稿件, 【...
    张张happy阅读 137评论 0 0
  • 春天
    茶语咖啡阅读 548评论 2 1
  • 时光的沧桑给了你历练 我要怎么走才能对得起你 夜晚才刚刚开始 我却独自听到怜惜之声 你又要飘然在这里吗 这是不是你...
    安之若素007阅读 166评论 0 1
  • 2018年6月28日,就在这一天,决定考研。 13年毕业到今天五年了。人生的第6个五年已经是过去时。进入而立之年,...
    沐晨菇凉阅读 263评论 0 0