import { mkdir, readFile, writeFile, copyFile } from 'node:fs/promises';
import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import path from 'node:path';
// Run explicitly when packaging a release; normal builds never fetch source archives.
const destination = 'apps/web/public/vendor/ffmpeg-0.12.10';
const cache = '.local/media-source-cache';
await mkdir(cache, { recursive: true });
await mkdir(`${destination}/sources`, { recursive: true });
await mkdir(`${destination}/licenses`, { recursive: true });
const sources = JSON.parse(await readFile('scripts/media-sources.json', 'utf8'));
const sha256 = bytes => createHash('sha256').update(bytes).digest('hex');
const inventory = [];
for (let offset = 0; offset < sources.length; offset += 4) {
await Promise.all(sources.slice(offset, offset + 4).map(async ([name, repo, commit, ref, licenses]) => {
const url = `https://codeload.github.com/${repo}/tar.gz/${commit}`;
const archive = path.join(cache, `${name}-${commit}.tar.gz`);
let bytes;
try { bytes = await readFile(archive); }
catch (error) {
if (error.code !== 'ENOENT') throw error;
const response = await fetch(url, { signal: AbortSignal.timeout(180000) });
if (!response.ok) throw new Error(`${name}: source HTTP ${response.status}`);
bytes = Buffer.from(await response.arrayBuffer());
await writeFile(archive, bytes);
}
const listing = spawnSync('tar', ['-tzf', archive], { encoding: 'utf8', maxBuffer: 20 * 1024 * 1024 });
if (listing.status !== 0) throw new Error(`${name}: invalid source archive`);
const prefix = listing.stdout.split('/')[0];
const licenseFiles = [];
for (const license of licenses) {
const result = spawnSync('tar', ['-xOzf', archive, `${prefix}/${license}`], { maxBuffer: 4 * 1024 * 1024 });
if (result.status !== 0 || !result.stdout.length) throw new Error(`${name}: missing ${license}`);
const file = `licenses/${name}-${license.replaceAll('/', '-')}`;
await writeFile(path.join(destination, file), result.stdout);
licenseFiles.push(file);
}
const files = [];
for (let start = 0, part = 0; start < bytes.length; start += 12 * 1024 * 1024, part++) {
const data = bytes.subarray(start, start + 12 * 1024 * 1024);
const file = `sources/${name}.tar.gz${bytes.length > 12 * 1024 * 1024 ? `.part${part}` : ''}`;
await writeFile(path.join(destination, file), data);
files.push({ file, bytes: data.length, sha256: sha256(data) });
}
inventory.push({ name, repo, commit, ref, url, bytes: bytes.length, sha256: sha256(bytes), files, licenses: licenseFiles });
console.log(`${name}: ${bytes.length} source bytes, ${files.length} part(s), ${licenseFiles.length} license(s)`);
}));
}
inventory.sort((a, b) => sources.findIndex(s => s[0] === a.name) - sources.findIndex(s => s[0] === b.name));
const lock = JSON.parse(await readFile('package-lock.json', 'utf8'));
const binaries = [];
for (const file of ['ffmpeg-core.js', 'ffmpeg-core.wasm']) {
const bytes = await readFile(`node_modules/@ffmpeg/core/dist/esm/${file}`);
binaries.push({ file, bytes: bytes.length, sha256: sha256(bytes) });
}
const manifest = { core: '0.12.10', wrapper: '0.12.15', upstreamRelease: 'https://github.com/ffmpegwasm/ffmpeg.wasm/releases/tag/v12.15', npmIntegrity: Object.fromEntries(['core', 'ffmpeg'].map(name => [name, lock.packages[`node_modules/@ffmpeg/${name}`].integrity])), binaries, sources: inventory };
await writeFile(`${destination}/source-manifest.json`, JSON.stringify(manifest, null, 2));
await copyFile('scripts/prepare-media-assets.mjs', `${destination}/prepare-media-assets.mjs.txt`);
await copyFile('scripts/prepare-media-sources.mjs', `${destination}/prepare-media-sources.mjs.txt`);
await copyFile('scripts/media-sources.json', `${destination}/media-sources.json`);
const html = `
FFmpeg licenses and source | igkeeper
FFmpeg licenses and corresponding source
igkeeper uses the unmodified @ffmpeg/core 0.12.10 ESM binary under GPL-2.0-or-later, and the @ffmpeg/ffmpeg 0.12.15 wrapper under MIT. Copyright remains with the respective authors. These components are provided without warranty; see the full license texts below. This software is based in part on the work of the Independent JPEG Group.
GNU General Public License version 2 · Source manifest, hashes and npm integrity · Binary deployment script · Source packaging script
Download source archives
All archives are hosted here. Each row is a complete pinned source tree with its original copyright notices and build files. For split archives, download every part in numeric order and concatenate the binary bytes before extracting the .tar.gz. Each part and the assembled archive have SHA-256 hashes in the manifest. Test-only Git submodules are not included.
Build and deployment notes
The ffmpeg-wasm source archive includes the original Dockerfile, Makefile, build scripts, modified CLI and bindings. With Docker available, extract that archive and run make prd for the single-thread production build; it uses Emscripten 3.1.40, FFmpeg n5.1.4 and the dependencies listed above. The archives can replace network clones at the listed commits. The x264 and LAME build refs were branches; their pinned commits are the refs resolved during this release preparation and predate the upstream release. Upstream did not publish signed build provenance. We have not independently rebuilt the binary byte for byte.
igkeeper does not modify the encoding algorithms. It splits the original WASM into three binary parts to fit the hosting file limit. The browser verifies the hashes in the binary-part manifest and concatenates those parts in order before loading it. Source archives and the deployment scripts allow this packaging to be inspected.
Back to igkeeper terms
`;
await writeFile(`${destination}/LICENSES.html`, html);
console.log(`Prepared ${inventory.length} source archives and license notices.`);