使用webCodec完成视频播放器(2)-音频数据播放

概要

本文主要讲解mp4中音频相关的处理方案,大致流程如下:

处理流程

mp4box解码

js 复制代码
// 请求视频资源
function queryVideo() {
    // fetch到的是mp4刻度流,包含视频,音频等多个track
    fetch(videoUrl.value)
        .then(res => res.arrayBuffer())
        .then(buffer => {
            videoBuffer = buffer;
            buffer.fileStart = 0;
            mp4boxfile.appendBuffer(buffer);
            // flush后即可触发onReady事件
            mp4boxfile.flush();
        })
}

function initMP4Box() {
    mp4boxfile = MP4Box.createFile();

    mp4boxfile.onReady = async (info) => {
        audioTrack = info.audioTracks?.[0];
        // 播放音频
        await getPcmData();
        startPlayAudio();
    }
}

和处理视频时的第一步一致,直接用Mp4Box解析出视频容器中的音频轨道即可。

这时候可以拿到音频的取样率(sampleRate)以及频道数(channelNum),为后续解码做准备。

decodeAudioData解码

js 复制代码
// 开始获取音频Sample
async function getPcmData() {
    if(!audioContext) {
        audioContext = new AudioContext({
            sampleRate: audioTrack.audio.sample_rate
        });
    }
    audioPcm = extractPCM4AudioBuffer(await audioContext.decodeAudioData(videoBuffer));
}

/**
 * 从 AudioBuffer 中提取 PCM
 */
function extractPCM4AudioBuffer(ab) {
    return Array(ab.numberOfChannels)
        .fill(0)
        .map((_, idx) => {
            return ab.getChannelData(idx);
        });
}

decodeAudioData配合刚刚获取到的sampleRate即可对视频数据进行解码。(直接将视频的ArrayBuffer喂给decodeAudioData就行,甚至不需要先拆分出音频)

播放

js 复制代码
let frameOffset = 0;
let endIdx = 0;
function tick(deltaTime, chan0, chan1) {
    // 这个方法有待商榷,不知move_duration是否确定为ms
    if(frameOffset >= audioTrack.movie_duration / 1000) {
        return { audio: [], state: "done"}; 
    }
    const frameCnt = Math.ceil(
        deltaTime * audioTrack.audio.sample_rate,
    );
    const audio = [
        ringSliceFloat32Array(chan0, endIdx,  endIdx + frameCnt),
        ringSliceFloat32Array(chan1, endIdx,  endIdx + frameCnt),
    ];
    endIdx = endIdx + frameCnt;
    frameOffset += deltaTime;
    return { audio, state: "success"};
}

/**
 *  循环 即 环形取值,主要用于截取 PCM
 */
function ringSliceFloat32Array(
  data,
  start,
  end
) {
    const cnt = end - start;
    const rs = new Float32Array(cnt);
    let i = 0;
    while (i < cnt) {
        rs[i] = data[(start + i) % data.length];
        i += 1;
    }
    return rs;
}

// 开始播放音频
function startPlayAudio() {
    // 每次取1s的数据
    const duration = 0.1;
    let startAt = 0;
    const ctx = new AudioContext();
    function play() {
        console.log("play audio");
        const {audio, state} = tick(duration, audioPcm[0], audioPcm[1]);
        if(state === "done") {
            return;
        }
        const [chan0, chan1] = audio;
        const buf = ctx.createBuffer(audioTrack.audio.channel_count, chan0?.length || 0, audioTrack.audio.sample_rate);
        buf.copyToChannel(chan0, 0);
        buf.copyToChannel(chan1, 1);
        const source = ctx.createBufferSource();
        source.buffer = buf;
        source.connect(ctx.destination);
        startAt = Math.max(ctx.currentTime, startAt);
        source.start(startAt);
        startAt += buf.duration;
        play()
    }
    play()
}

播放的关键点主要就是分片

  1. tick方法将pcm数据进行分段。因为之前得到了音频的sampleRate(取样率)采样大小,所以可以直接用deltaTime * audioTrack.audio.sample_rate获取特定时长的pcm数据。
  2. tick分片后,对这些pcm片段创建多个source进行分片播放,这样就不需要等待所有音频处理完成后再播放。

思考

现在可以使用pcm数据进行直接播放了,对比浏览器提供的video标签,有什么收益呢?

音量调节

音量调节的原理就是调节声音的振幅

我们可以看到,最终拿到的pcm数据如下:

只要将这些数据乘以想要调节的倍数,就可以做对应大小调节了,非常方便。

倍速播放

倍速播放的原理就是缩短每个音频片段的播放时长

只需要在上述地方做改动就行(如果不适用tick分片,直接播放一整段片段就不太好实现了)

声道处理

声道处理的原理就是处理单个声道的pcm数据

由于我们之前已经分别拿到了两条声道的数据,就可以很方便的处理单个声道的数据啦。

例如我们只需要一条声道的数据,直接注释一行代码就行,这时候耳机里就只有一边有声音了。

相关推荐
Hilaku9 分钟前
当 AI 一天写完一周的代码,我们还剩什么优势?
前端·javascript·程序员
尘世中一位迷途小书童13 分钟前
无人机航线里的云台角度怎么调?Cesium 做了个视锥 + 鹰眼联动
前端·javascript·数据可视化
思码梁田17 分钟前
CSS display 属性:从元素类型转换到隐藏元素的实用指南
前端·css·display·inline·block·none·inline-block
Csvn18 分钟前
⚡ Vite 依赖预构建(optimizeDeps)的 3 个经典坑:改了源码不生效、新增依赖 404、缓存怎么清都不行
前端
A242073493029 分钟前
Vue3 + TypeScript:后端数据在表格内渲染后进行增删改的完整实现步骤
前端·javascript·typescript
他们叫我 悦儿遥遥雨32 分钟前
.NET 8 Web开发入门(六):Blazor 全栈开发——告别 JavaScript 焦虑
前端·javascript·.net
用户21816970493039 分钟前
Flutter(九)StatelessWidget StatefullWidget
前端
paopaokaka_luck40 分钟前
基于springboot3+vue3的乡村医生诊疗管理系统(AI助手、协同过滤算法、webSocket实时聊天、Echarts图形化分析)
前端·网络·人工智能·spring boot·websocket·网络协议·echarts
编程风暴41 分钟前
3类人群的数据分析实习类型选择+4条选错红线
java·前端·数据分析
其美杰布-富贵-李41 分钟前
Vue 3 模块导入教程:import、export 与文件组织
前端·javascript·vue.js