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

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

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

相关推荐
iphone1085 分钟前
一次编码,多端运行:HTML5多终端调用
前端·javascript·html·html5
老坛00122 分钟前
2025决策延迟的椭圆算子分析:锐减协同工具的谱间隙优化
前端
老坛00123 分钟前
从记录到预测:2025新一代预算工具如何通过AI实现前瞻性资金管理
前端
今禾26 分钟前
" 当Base64遇上Blob,图像转换不再神秘,让你的网页瞬间变身魔法画布! "
前端·数据可视化
华科云商xiao徐30 分钟前
高性能小型爬虫语言与代码示例
前端·爬虫
十盒半价31 分钟前
深入理解 React useEffect:从基础到实战的全攻略
前端·react.js·trae
攀登的牵牛花32 分钟前
Electron+Vue+Python全栈项目打包实战指南
前端·electron·全栈
iccb101332 分钟前
我是如何实现在线客服系统的极致稳定性与安全性的
前端·javascript·后端
一大树33 分钟前
Vue3祖孙组件通信方法总结
前端·vue.js
不要进入那温驯的良夜34 分钟前
跨平台UI自动化-Appium
前端