# libav.js build "nordalux-audio" Custom LGPL build of [libav.js](https://github.com/Yahweasel/libav.js) for the in-browser audio converter. Built with `packages/audio-converter/scripts/build-libav.sh` on 2026-09-19 (rebuilt from the neutral path `/tmp/nordalux-libav`). | Item | Value | |---|---| | libav.js | tag `v6.10.9.0` (commit `c80e885c3461f7bb7ea565c9631b34243ae0dbf1`), latest 6.x tag at build time | | FFmpeg | 9.0 (`ffmpeg-9.0.tar.xz` plus libav.js patches, `patches/ffmpeg/series9`) | | Emscripten | 6.0.5 (emsdk, the version pinned in libav.js CI `.github/workflows/build-and-test.yaml`) | | LAME | 3.100 | | opus | 1.6.1 | | libvorbis / libogg | 1.3.7 / 1.3.6 | | emfiberthreads | 1.3 (libav.js' pthread shim so the ffmpeg CLI runs without real threads) | | Target | non-threaded WebAssembly only (`.wasm.*`). No `.thr.*` (pthreads/SharedArrayBuffer), no asm.js, no `.dbg.*` | | Build host | WSL2 Ubuntu 24.04, `make dist/libav-6.10.9.0-nordalux-audio.wasm.{mjs,js}` plus the loader targets | ## Fragment list (`configs/mkconfig.js nordalux-audio '[...]'`) ```json ["avformat","avcodec","avfilter","swresample","cli","audio-filters", "format-ogg","format-wav","format-mp3", "demuxer-aiff","demuxer-mp4","muxer-mp4","demuxer-webm","demuxer-asf","demuxer-aac","demuxer-flac", "parser-opus","codec-libopus", "parser-vorbis","codec-libvorbis", "parser-flac","decoder-flac", "parser-mpegaudio","decoder-mp3","encoder-libmp3lame", "parser-aac","codec-aac","bsf-aac_adtstoasc", "decoder-wmav1","decoder-wmav2", "codec-pcm_s16le","decoder-pcm_s16be","decoder-pcm_s24le","decoder-pcm_s24be", "decoder-pcm_s32le","decoder-pcm_s32be","decoder-pcm_f32le","decoder-pcm_f32be","decoder-pcm_u8"] ``` Fragments with a directory in `configs/fragments/` (avformat, avcodec, avfilter, swresample, cli, audio-filters, demuxer-aiff/mp4/webm/ogg/wav, muxer-mp4/wav, decoder-libopus, decoder-libvorbis, encoder-libmp3lame) add their own flags, libraries and license text. The rest go through mkconfig's `format-/codec-/demuxer-/muxer-/decoder-/encoder-/parser-/bsf-` rule, which becomes `--enable-=`. The exact list is also in `nordalux-audio.config.json`, and the resulting FFmpeg flags are in `nordalux-audio.ffmpeg-config.txt`. What it covers: - **Demux/decode:** mp3, aac (ADTS), m4a/mp4/mov (audio track), ogg (vorbis, opus, flac), opus, flac, wav/pcm (s16/s24/s32/f32/u8, LE and BE), aiff, wma (asf + wmav1/wmav2), webm/matroska (audio track). - **Encode/mux:** mp3 (libmp3lame), aac (native encoder) in mp4/ipod (m4a), opus (libopus) in ogg, vorbis (libvorbis) in ogg, wav pcm_s16le. - **Filters:** aresample (swresample), aformat, pan, volume, anull, atrim, afade, loudnorm, dynaudnorm and the rest of `audio-filters`. - **CLI:** `ffmpeg` and `ffprobe`. - **Video:** no video decoders or encoders and no swscale. Video streams in mp4/webm are only demuxed and then dropped with `-vn`. ## Output files | File | Raw bytes | gzip -9 bytes | |---|---:|---:| | `libav-6.10.9.0-nordalux-audio.mjs` (ES module loader/factory) | 28,423 | 6,706 | | `libav-6.10.9.0-nordalux-audio.js` (classic-script loader, sets global `LibAV`) | 23,617 | 5,888 | | `libav-6.10.9.0-nordalux-audio.wasm.mjs` (Emscripten glue, ESM) | 294,447 | 61,009 | | `libav-6.10.9.0-nordalux-audio.wasm.js` (Emscripten glue, classic) | 294,449 | 61,008 | | `libav-6.10.9.0-nordalux-audio.wasm.wasm` | 3,838,763 (3.66 MiB) | 1,529,334 (1.46 MiB), brotli -q11: 1,199,578 | | `libav.types.d.ts` (TypeScript types, dev only, not needed at runtime) | 225,477 | n/a | At runtime you need either the `.mjs` pair (`libav-…-nordalux-audio.mjs` and `.wasm.mjs`) or the `.js` pair, plus `.wasm.wasm` in both cases. ## Licensing The combined build is distributed under **LGPL-2.1-or-later**, with FFmpeg as the governing component. FFmpeg's generated `config.h` reports `#define FFMPEG_LICENSE "LGPL version 2.1 or later"`. | Component | License | File in `vendor/libav/` | |---|---|---| | FFmpeg 9.0 (libavcodec/-format/-filter/-util, libswresample, fftools) | LGPL-2.1-or-later. The ogg demuxer part is MIT-style (see license header in the loader) | `FFmpeg-COPYING.LGPLv2.1`, `FFmpeg-LICENSE.md` | | LAME 3.100 (libmp3lame) | LGPL-2.0-or-later ("GNU Library GPL") | `LAME-COPYING` | | opus 1.6.1 | BSD-3-Clause (+ royalty-free IETF patent grants) | `opus-COPYING` | | libvorbis 1.3.7 | BSD-3-Clause | `libvorbis-COPYING` | | libogg 1.3.6 | BSD-3-Clause | `libogg-COPYING` | | libav.js (wrapper/bindings), emfiberthreads | `LICENSE.md` says "LGPL" for the build as a whole. The libav.js and emfiberthreads source files carry an ISC-style permissive header | `libav.js-LICENSE.md` | | Emscripten runtime, musl | MIT | full text in the license header of every emitted `.js`/`.mjs` | All license texts are also embedded as a comment header in each emitted `.js`/`.mjs` file. LGPL obligations apply when you serve these files. Give users the notice and license texts, and make the corresponding source available. That source is libav.js at the tag above plus `ffmpeg-9.0.tar.xz`, `lame-3.100.tar.gz`, `opus-1.6.1.tar.gz`, `libvorbis-1.3.7.tar.xz`, `libogg-1.3.6.tar.xz` and `emfiberthreads-1.3.tar.gz` (cached in `/tmp/nordalux-libav/libav.js-6.10.9.0/build/` and bundled in `vendor/libav-source/nordalux-audio-libav-source-6.10.9.0.tar` by `scripts/pack-libav-source.sh`), together with `scripts/build-libav.sh`. Keeping the wasm as a separate, replaceable file satisfies the relinking requirement. MP3 and AAC patents: this is a separate question from GPL. libav.js lists AAC under its "encumbered" variants. The basic MP3 patents have expired. AAC is included here because it was explicitly requested. ## No GPL / nonfree options were enabled This was verified three ways, and `build-libav.sh` aborts if either check fails: 1. The variant's FFmpeg flags contain no GPL/nonfree option: ``` $ grep -Eic 'gpl|nonfree|x264|x265|fdk' nordalux-audio.ffmpeg-config.txt 0 ``` 2. FFmpeg's generated `build/ffmpeg-9.0/build-base-nordalux-audio/config.h`: ``` #define CONFIG_GPL 0 #define CONFIG_NONFREE 0 #define CONFIG_VERSION3 0 #define CONFIG_GPLV3 0 #define FFMPEG_LICENSE "LGPL version 2.1 or later" ``` 3. The configure command line recorded in `ffbuild/config.log` / `FFMPEG_CONFIGURATION` has no `--enable-gpl`, `--enable-version3`, `--enable-nonfree`, `--enable-libx264`, `--enable-libx265` or `--enable-libfdk-aac`. The only external libraries are `--enable-libopus --enable-libvorbis --enable-libmp3lame`: ``` --prefix=/opt/ffmpeg --target-os=none --enable-cross-compile --disable-x86asm --disable-inline-asm --disable-runtime-cpudetect --cc=emcc --cxx=em++ --ranlib=emranlib --disable-doc --disable-stripping --disable-programs --disable-ffplay --disable-ffprobe --disable-network --disable-iconv --disable-xlib --disable-sdl2 --disable-zlib --disable-everything --disable-swscale --disable-swresample --enable-pthreads --arch=emscripten --optflags=-Oz --enable-protocol=data --enable-protocol=file --enable-swresample --enable-filter=aresample --enable-ffmpeg --enable-ffprobe [audio filters…] --enable-demuxer=ogg --enable-muxer=ogg … --enable-libopus --enable-decoder=libopus --enable-bsf=opus_metadata --enable-encoder=libopus … --enable-libvorbis --enable-decoder=libvorbis --enable-encoder=libvorbis … --enable-libmp3lame --enable-encoder=libmp3lame … --enable-decoder=aac --enable-encoder=aac … --enable-decoder=wmav1 --enable-decoder=wmav2 … --host-cc=/tmp/nordalux-libav/hostcc/bin/hostcc ``` The full flag list is in `nordalux-audio.ffmpeg-config.txt`. `--enable-pthreads` refers to the emfiberthreads shim compiled into a single-threaded wasm. The output does not use real threads or SharedArrayBuffer. ## Build-host note The WSL distro had no `make`, `pkg-config` or C compiler, and `sudo` needed a password. The script works around this without root. It unpacks the Ubuntu `.deb`s for make, pkgconf, libc6-dev, linux-libc-dev, libgcc-13-dev and libcrypt-dev with `apt-get download` + `dpkg -x` into a user-space sysroot, using a user-owned apt list directory. It then uses emsdk's native clang as FFmpeg's *host* compiler (`--host-cc=/tmp/nordalux-libav/hostcc/bin/hostcc`). The host compiler only builds build-time helpers and never touches the wasm output. On a machine with `build-essential pkg-config`, these steps are skipped. ### Neutral build path (no user paths in shipped files) The build runs under `/tmp/nordalux-libav`. This is a symlink to a persistent store, `$LIBAV_STORE`, which defaults to `libav-build` in the builder's home directory. The only absolute build path that ends up in the output is the FFmpeg configure string, and it reads `--host-cc=/tmp/nordalux-libav/hostcc/bin/hostcc`. The `-I`/`-L` flags are stripped from `config.h` by libav.js. The `__FILE__` strings from assertions are relative (`../../opus-1.6.1/…`, `src/fftools/…`). No source maps are shipped, because the `.dbg.*` targets are not built. Emscripten's JS runtime contains the virtual MEMFS home directory of the wasm (the directory `web_user` under `/home`, which is also `$HOME` inside the wasm). The build script writes this string literal with an escaped slash (`"/home\/web_user"`). In JavaScript that is the same string value, so the behaviour is unchanged, and a plain grep for home-directory paths over the shipped files stays at 0. The script ends with that grep over every file in `vendor/libav/`, checking for the build user's name and the home prefix, and aborts on any hit. ## Smoke test `node scripts/smoke-libav.mjs` converts `tests/fixtures/in.*` with `libav.ffmpeg(...)` in Node and checks each output with the system `ffprobe`. Result on 2026-09-19 (neutral-path rebuild): 14/14 passed with a fresh instance per job, and 14/14 passed with one shared instance (probe run plus conversion). See `tests/out/smoke-results.md`. ## Using it in a browser / web worker Serve all `libav-6.10.9.0-nordalux-audio.*` files from one directory, for example `/assets/audio-converter/libav/`. The loader builds the target file name from `base`, the version (baked in at build time) and `variant`, so it loads `/libav-6.10.9.0-nordalux-audio.wasm.mjs`, and that file then loads `….wasm.wasm` next to it. ```js // module web worker, e.g. new Worker(url, {type: 'module'}) import LibAV from './libav/libav-6.10.9.0-nordalux-audio.mjs'; // default export = LibAV namespace const libav = await LibAV.LibAV({ base: new URL('./libav/', import.meta.url).href.replace(/\/$/, ''), // directory of the files, no trailing slash variant: 'nordalux-audio', noworker: true, // already inside a worker: run in "direct" mode, no nested sub-worker // yesthreads is left unset: the .thr build is not shipped }); libav.onprint = libav.onprinterr = line => { /* ffmpeg log / progress lines */ }; await libav.writeFile('input.bin', new Uint8Array(arrayBuffer)); // MEMFS, any name const code = await libav.ffmpeg('-hide_banner', '-nostdin', '-y', '-i', 'input.bin', '-vn', '-sn', '-dn', '-map_metadata', '-1', '-ac', '1', '-ar', '44100', '-c:a', 'libmp3lame', '-b:a', '64k', '-f', 'mp3', 'output.mp3'); // resolves to the exit code if (code !== 0) throw new Error('ffmpeg failed: ' + code); const out = await libav.readFile('output.mp3'); // Uint8Array await libav.unlink('input.bin'); await libav.unlink('output.mp3'); // libav.ffprobe(...args) works the same way. libav.terminate() drops the instance. ``` Encoder/muxer pairs: `-c:a libmp3lame -f mp3`, `-c:a aac -f ipod` (.m4a), `-c:a libopus -f ogg` (use `-ar 48000`), `-c:a libvorbis -f ogg`, `-c:a pcm_s16le -f wav`. If you use a classic (non-module) worker, call `importScripts('…/libav-6.10.9.0-nordalux-audio.js')` and then `LibAV.LibAV({...})` with the same options. You can also set `self.LibAV = {base: '…'}` before `importScripts`. The smoke test only exercised "direct" mode in Node (`noworker`). The browser worker path is not covered by it. `build.mjs` copies `libav-*.{js,mjs,wasm}` into its `libav/` output and writes `engine.json` (`variant: 'nordalux-audio'`). It also copies every `*LICENSE*`, `*COPYING*` and `*.txt` file plus this BUILD-INFO.md into `licenses/`. That is why the license texts sit next to the runtime files in this directory. Reproducibility note: `FFMPEG_CONFIGURATION` in the wasm contains only the neutral path `--host-cc=/tmp/nordalux-libav/hostcc/bin/hostcc`. That flag is only added when the build host has no gcc/cc. On a host with `build-essential`, the flag is absent, so the embedded version string differs. The code paths do not.