vListV5.5(ffmpeg与web的碰撞) 教你使用libmedia

in 前后端开发 with 0 comment

浏览器软解HEVC

由来

前几天在逛Github的时候,发现了一个项目叫做libmedia,号称是"一个 TypeScript 实现的高性能媒体库"
在我眼中,lib开头的库大多是优秀到足以给大家使用的,如libxml zlib libopus之类的,于是我点击去了解了一下,好家伙连文档都没有,只有TypeDOC给的少量注释
但是这个难不倒我,于是我去扒源码,然后给了我极大的震撼
作者将ffmpeg的一些库使用TypeScript重写了一遍,

库

然后为了性能,使用了cheap库
cheap底层原理,readme也基本上说清楚了

cheap 将 struct 的概念引入 typescript,struct 和 C 中的 struct 概念一致,表示一段内存中的数据结构布局,如此可以在 wasm 和 js 中各自根据布局操作内存,而双方之间传递数据时传递内存的开始位置,也就是 pointer(指针),从而避免了数据的序列化和反序列化的开销。

有了 struct 了我们不仅可以在 wasm 和 js 之间高效的传递数据,还想到目前 js 中多线程编程也面临着无法进行数据共享的问题,如果我们的 struct 是在 SharedArrayBuffer 中,让所有 worker 使用同一个 SharedArrayBuffer,这样所有 worker 都实现了数据共享,不同 worker 之间通过传递指针来高效的进行数据传递

震惊了我,cheap使TypeScript都有指针了,连int32 float64之类的类型全部定义了一遍,而且聪明的编译器居然学习了VUE将指针自动解包。
于是我得出了一个结论,cheap将成为WASM的未来方向,而libmedia将成为Web多媒体的黑马。她不火谁火?

一些小问题和原因

于是立刻马上,我开始尝试将libmedia打包入我的项目vlist5,结果,哈哈哈(自嘲)报错了。详细的讨论见 https://github.com/zhaohappy/libmedia/issues/5
WebPack这个旧时代的骄傲已经被Vite抢走了皇冠,但是Cheap居然不支持Vite编译
气呼呼的,我翻了半天WebPack文档,绝望地发现,WebPack压根不支持打包成ES模块
那么只能从UMD模块中找到未来了,为此我还特意研究了一下UMD,想到了3种对策

尝试:require()

CommonJS是NodeJS的模块管理器,Vite支持对应的Loader,可以打包CJS模块
安装:

npm i vite-plugin-require-transform --save-dev

配置:

import { defineConfig } from 'vite'
import requireTransform from 'vite-plugin-require-transform';

export default defineConfig({
      plugins: [
        requireTransform({
            fileRegex: /.js$/
        }),
    ],
);

导入:

const a = require('libmedia/dist/avplayer/avplayer.js');

然后

继续报错

我为此特意扒了一下源码,发现因为document.currentScriptnull造成的

使用eval

可惜,document.currentScript是只读的属性,无法修改骗过UMD加载器
于是我想到了隔离法,将globalThiswindow变量遮蔽,暴露一些用到的库
的确可以,实测能用,但是太不雅观了

import AVPLAYER_SRC from 'libmedia/dist/avplayer/avplayer?url';
const Func = await (await fetch(FILE_PATH)).text(),
    _G = globalThis;
(function(){
    const globalThis = {
            document: {
                currentScript: {
                    src: AVPLAYER_SRC
                }
            },
            Math: _G.Math,
            URL: _G.URL,
            parseFloat: _G.parseFloat,
            parseInt: _G.parseInt,
            navigator: _G.navigator
        },
        window = globalThis;
    eval(Func);
})();

要是未来添加了几个依赖,那工程量就很大了。放弃

使用<script>标签

最后我还是向 UMD 势力低头了,古老的加载方式,我想想...这还是在vList3那会儿使用过的<script>大法

const script = document.createElement('script');
script.src = AVPLAYER_SRC;
document.body.append(script);
await new Promise(rs => script.onload = rs);

的确可以,就是需要加一个全局定义的TypeScript Declare文件(.d.ts)
懒得写,想躺平,于是偷懒使用JavaScript + .d.ts中转,将API二次包装成reactive

export interface Export {
    url: string,
    playBackRate: number,
    loop: boolean,
    volume: 1,
    time: {
        total: number,
        current: number
    },
    tracks: {
        audio: Array<Stream>,
        audioTrack: number,
        video: Array<Stream>,
        videoTrack: number,
        subtitle: Array<Stream>,
        subTrack: number
    },
    ended: boolean,
    play: boolean,
    stop: boolean,
    destroy: boolean,
    status: Stat,
    display: {
        fill: boolean,
        rotate: 0,
        flip: {
            vertical: boolean,
            horizontal: boolean
        }
    },
    func: {
        snapshot: boolean,
        seek: number,
        resize: [number, number]
    }
}

这样TypeScript也有了,Vite打包也有了放屁,这些需要动态加载的文件怎么办

动态文件

我苦苦思索,最后决定手动拷贝,在package.json上动手脚

"build": "run-p type-check \"build-only {@}\" -- && cp node_modules/libmedia/dist/avplayer/*.avplayer.js dist/assets/ || copy node_modules\\libmedia\\dist\\avplayer\\*.avplayer.js dist/assets/",

这串超长JSON实现了跨平台(Windows/Unix)编译并拷贝动态文件到dist/,至此我们的打包完成了!

初始化

官方只有一个 网站用作示例,不服输的我直接扒源代码找到了初始化方法(大哥,写点文档不难吧)

player = new AVPlayer({
    container: document.querySelector(id),
    isLive: isLiveComponent.isLive,
    getWasm: (type, codecId) => {
        switch (type) {
            case 'decoder': {

                if (codecId >= 65536 && codecId <= 65572) {
                    return `../dist/decode/pcm${(enableSimdComponent.enableSimd) ? '-simd' : (supportAtomic ? '-atomic' : '')}.wasm`
                }

                switch (codecId) {
                    // H264
                    case 27:
                        return `../dist/decode/h264${(enableSimdComponent.enableSimd) ? '-simd' : (supportAtomic ? '-atomic' : '')}.wasm`
                    // AAC
                    case 86018:
                        return `../dist/decode/aac${(enableSimdComponent.enableSimd) ? '-simd' : (supportAtomic ? '-atomic' : '')}.wasm`
                    // MP3
                    case 86017:
                        return `../dist/decode/mp3${(enableSimdComponent.enableSimd) ? '-simd' : (supportAtomic ? '-atomic' : '')}.wasm`
                    // HEVC
                    case 173:
                        return `../dist/decode/hevc${(enableSimdComponent.enableSimd) ? '-simd' : (supportAtomic ? '-atomic' : '')}.wasm`
                    // VVC
                    case 196:
                        return `../dist/decode/vvc${(enableSimdComponent.enableSimd) ? '-simd' : (supportAtomic ? '-atomic' : '')}.wasm`
                    // Mpeg4
                    case 12:
                        return `../dist/decode/mpeg4${(enableSimdComponent.enableSimd) ? '-simd' : (supportAtomic ? '-atomic' : '')}.wasm`
                    // AV1
                    case 225:
                        return `../dist/decode/av1${(enableSimdComponent.enableSimd) ? '-simd' : (supportAtomic ? '-atomic' : '')}.wasm`
                    // Speex
                    case 86051:
                        return `../dist/decode/speex${(enableSimdComponent.enableSimd) ? '-simd' : (supportAtomic ? '-atomic' : '')}.wasm`
                    // Opus
                    case 86076:
                        return `../dist/decode/opus${(enableSimdComponent.enableSimd) ? '-simd' : (supportAtomic ? '-atomic' : '')}.wasm`
                    // flac
                    case 86028:
                        return `../dist/decode/flac${(enableSimdComponent.enableSimd) ? '-simd' : (supportAtomic ? '-atomic' : '')}.wasm`
                    // vorbis
                    case 86021:
                        return `../dist/decode/vorbis${(enableSimdComponent.enableSimd) ? '-simd' : (supportAtomic ? '-atomic' : '')}.wasm`
                    // vp8
                    case 139:
                        return `../dist/decode/vp8${(enableSimdComponent.enableSimd) ? '-simd' : (supportAtomic ? '-atomic' : '')}.wasm`
                    // vp9
                    case 167:
                        return `../dist/decode/vp9${(enableSimdComponent.enableSimd) ? '-simd' : (supportAtomic ? '-atomic' : '')}.wasm`
                    default:
                        return null
                }
                break
            }
            case 'resampler':
                return `../dist/resample/resample${(enableSimdComponent.enableSimd) ? '-simd' : (supportAtomic ? '-atomic' : '')}.wasm`
            case 'stretchpitcher':
                return `../dist/stretchpitch/stretchpitch${(enableSimdComponent.enableSimd) ? '-simd' : (supportAtomic ? '-atomic' : '')}.wasm`
        }
    },
    checkUseMES: (streams) => {
        return useMseComponent.useMse
    },
    enableHardware: enableWebcodecComponent.enableHardwareAcceleration,
    enableWebGPU: enableWebGPUComponent.enableWebGPU,
    loop: loopComponent.loop,
    jitterBufferMax: 4,
    jitterBufferMin: 1,
    lowLatency: true
})

这么长?什么情况?

目瞪口呆.webp

别慌,这么长大部分都是 getWasm 函数,还有simd atomic加速

simd

可以看到simd兼容性不错,我们就不判断了,直接使用simd加速

import PCM_WASM from 'libmedia/dist/decode/pcm-simd.wasm?url';
import H264_WASM from 'libmedia/dist/decode/h264-simd.wasm?url';
import AAC_WASM from 'libmedia/dist/decode/aac-simd.wasm?url';
import MP3_WASM from 'libmedia/dist/decode/mp3-simd.wasm?url';
import HEVC_WASM from 'libmedia/dist/decode/hevc-simd.wasm?url';
import VVC_WASM from 'libmedia/dist/decode/vvc-simd.wasm?url';
import MP4_WASM from 'libmedia/dist/decode/mpeg4-simd.wasm?url';
import AV1_WASM from 'libmedia/dist/decode/av1-simd.wasm?url';
import OPUS_WASM from 'libmedia/dist/decode/opus-simd.wasm?url';
import FLAC_WASM from 'libmedia/dist/decode/flac-simd.wasm?url';
import OGG_WASM from 'libmedia/dist/decode/vorbis-simd.wasm?url';
import VP9_WASM from 'libmedia/dist/decode/vp9-simd.wasm?url';
import RSP_WASM from 'libmedia/dist/resample/resample-simd.wasm?url';
import SP_WASM from 'libmedia/dist/stretchpitch/stretchpitch-simd.wasm?url';
const CODEC_MAP = {
    12: MP4_WASM,
    27: H264_WASM,
    167: VP9_WASM,
    173: HEVC_WASM,
    196: VVC_WASM,
    225: AV1_WASM,
    86018: AAC_WASM,
    86017: MP3_WASM,
    86021: OGG_WASM,
    86028: FLAC_WASM,
    86076: OPUS_WASM,
};

把你想要用的wasm一股脑全部import一遍,别忘了?url结尾
然后体验一下超短初始化命令

const player = new globalThis.AVPlayer({
    "container": el,
    "enableHardware": true,
    "getWasm": (type, codecId) => {
        switch (type) {
            case 'decoder': 
                if (codecId >= 65536 && codecId <= 65572) return PCM_WASM;
                else return CODEC_MAP[codecId] || null;

            case 'resampler':
                return RSP_WASM;

            case 'stretchpitcher':
                return SP_WASM;
        }
    },
    "enableWebGPU": false,
    "simd": true
});

怎么样,这下不可怕了吧

使用

摩拳擦掌,我们终于开始battle浏览器了,首先是初始化视频

player.load('/a.mkv');

当然,这个.load()是异步函数,想要体验video.onload欢迎使用这个Promise
注意返回值是ms

const duration = ref(0);
player.load('/a.mkv').then(() => duration.value = Number(player.getDuration()));
注意 这个 libmedia 大部分数字返回值都是BigInt
不要忘记使用 Number() 处理返回值

暂停

player.pause()

播放

player.play()

拖拽进度条然后seek 注意返回值是ms

player.seek(102400)

于是这个播放器就可以使用啦,恭喜

Responses