使用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数据

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

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

相关推荐
却尘14 分钟前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare15 分钟前
浅浅看一下设计模式
前端
Lee川18 分钟前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix1 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人1 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl1 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人1 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼1 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空1 小时前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust