diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aeab427e583..73689c5e5eb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,7 +49,7 @@ jobs: [ "$v" = "$version" ] || bad="$bad $p=$v" done # Every *_VERSION constant pins a published package version. - for f in packages/ffmpeg/src/const.ts apps/website/src/components/Playground/const.ts scripts/download-assets.js; do + for f in packages/ffmpeg/src/const.ts apps/website/src/components/Playground/const.ts apps/vanilla-app/download-assets.js; do grep -q "_VERSION = \"$version\"" "$f" || bad="$bad $f" if grep -E '_VERSION = "[0-9]' "$f" | grep -vq "\"$version\""; then bad="$bad $f" diff --git a/apps/angular-app/angular.json b/apps/angular-app/angular.json index f06dcbfb796..b643320f2aa 100644 --- a/apps/angular-app/angular.json +++ b/apps/angular-app/angular.json @@ -22,7 +22,12 @@ "tsConfig": "tsconfig.app.json", "assets": [ "src/favicon.ico", - "src/assets" + "src/assets", + { + "glob": "*.js", + "input": "node_modules/@project516/ffmpeg-wasm/dist/esm", + "output": "ffmpeg" + } ], "styles": [ "src/styles.css" diff --git a/apps/angular-app/src/app/app.component.ts b/apps/angular-app/src/app/app.component.ts index 35833af542b..bde7d309c93 100644 --- a/apps/angular-app/src/app/app.component.ts +++ b/apps/angular-app/src/app/app.component.ts @@ -4,7 +4,7 @@ import { RouterOutlet } from '@angular/router'; import { FFmpeg } from '@project516/ffmpeg-wasm'; import { fetchFile, toBlobURL } from '@project516/ffmpeg-wasm-util'; -const baseURL = 'https://cdn.jsdelivr.net/npm/@project516/ffmpeg-wasm-core-mt@0.13.1/dist/esm'; +const baseURL = 'https://cdn.jsdelivr.net/npm/@project516/ffmpeg-wasm-core@0.13.1/dist/esm'; @Component({ selector: 'app-root', @@ -23,15 +23,14 @@ export class AppComponent { this.message = message; }); await this.ffmpeg.load({ + // Angular does not bundle workers from node_modules; angular.json copies + // the library's to /ffmpeg. + classWorkerURL: new URL('ffmpeg/worker.js', document.baseURI).href, coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'), wasmURL: await toBlobURL( `${baseURL}/ffmpeg-core.wasm`, 'application/wasm', ), - workerURL: await toBlobURL( - `${baseURL}/ffmpeg-core.worker.js`, - 'text/javascript', - ), }); this.loaded = true; } diff --git a/apps/vanilla-app/README.md b/apps/vanilla-app/README.md index 1f57b0dc40c..156769faeef 100644 --- a/apps/vanilla-app/README.md +++ b/apps/vanilla-app/README.md @@ -5,7 +5,7 @@ You need to download assets from npm before running the examples: ```bash -$ npm run download +$ pnpm download ``` ## Run @@ -13,7 +13,7 @@ $ npm run download To run this example, execute: ```bash -$ npm start +$ pnpm start ``` Visit http://localhost:8080 to check available examples. diff --git a/scripts/download-assets.js b/apps/vanilla-app/download-assets.js similarity index 95% rename from scripts/download-assets.js rename to apps/vanilla-app/download-assets.js index eb3b11c6e36..a71d139b01a 100644 --- a/scripts/download-assets.js +++ b/apps/vanilla-app/download-assets.js @@ -1,3 +1,4 @@ +// Downloads the @project516 packages from npm into public/assets. const tar = require("tar"); const fs = require("fs"); diff --git a/apps/vanilla-app/package.json b/apps/vanilla-app/package.json index ea97103ba3e..f006e64d6a8 100644 --- a/apps/vanilla-app/package.json +++ b/apps/vanilla-app/package.json @@ -4,8 +4,8 @@ "description": "browser example", "private": true, "scripts": { - "download": "node ../../scripts/download-assets.js", - "prestart": "npm run download", + "download": "node download-assets.js", + "prestart": "pnpm download", "start": "node server.js" }, "author": "Jerome Wu ", diff --git a/apps/website/docs/faq.md b/apps/website/docs/faq.md index b663d7b5c31..b13709834e7 100644 --- a/apps/website/docs/faq.md +++ b/apps/website/docs/faq.md @@ -12,19 +12,32 @@ releases; if you are pinned to one of those, use [fluent-ffmpeg](https://www.npmjs.com/package/fluent-ffmpeg) instead, or upgrade. -### Why ffmpeg.wasm is so slow comparing to ffmpeg? - -As of now, WebAssembly is still a lot slower than native, it is possible to further speed up using -WebAssembly intrinsic, which is basically writing assembly code. It is something we are investigating -and hope to introduce in the future. - -If you are OK with more unstable version of ffmpeg.wasm, using ffmpeg.wasm multithread (mt) version -can have around 2x speed comparing to single thread (but consume a lot more memory and cpu) +### Why is ffmpeg.wasm so slow compared to native ffmpeg? + +The core is built with `--disable-asm`, so FFmpeg and its libraries run +plain C code instead of the hand-written assembly and SIMD that native builds +use. Encoding is several times slower than native. Decoding +and remuxing are closer. See [Performance](/docs/performance) and +[FATE and benchmarks](/docs/fate-and-benchmarks) for measurements. + +Ways to speed things up: + +- Use `@project516/ffmpeg-wasm-core-mt` on a cross-origin isolated page. + Threads run FFmpeg's pipeline and the encoders in parallel, at the cost of + more memory and CPU. +- Pick a faster preset: `-preset ultrafast` for x264, `-deadline realtime` + for VP9. +- Remux with `-c copy` when you do not need to re-encode. +- Mount large inputs with `WORKERFS` instead of copying them in with + `writeFile()`. +- Build a smaller core with a [preset](/docs/presets) if you only need a few + codecs, which also shortens load time. ### Is RTSP supported by ffmpeg.wasm? -We are trying to support, but so far WebAssembly itself lack of features like sockets which makes -it hard to implement RTSP protocol. Possible workarounds are still under investigation. +No. Browsers do not give WebAssembly raw TCP or UDP sockets, so network +protocols such as RTSP and RTMP cannot work. Fetch the input in JavaScript and +write it to the file system, or use WebRTC or MediaRecorder for live streams. ### What is the license of ffmpeg.wasm? @@ -37,7 +50,42 @@ The whole project, including both packages, is licensed under AGPL-3.0-or-later. ### What is the maximum size of input file? -2 GB, which is a hard limit in WebAssembly. Might become 4 GB in the future. +It depends on how the file gets in. FFmpeg's own memory is capped at 2 GB in +both cores: the single-thread core starts at 48 MB and grows on demand, the +multi-thread core starts at 1 GB. A file copied in with `writeFile()` also +needs room in the browser's memory, so large files fail well before 2 GB on +low-memory devices. + +For big inputs, mount the `File` with `WORKERFS` instead, which reads it +lazily without copying: + +```js +await ffmpeg.mount('WORKERFS', { files: [file] }, '/input'); +await ffmpeg.exec(['-i', `/input/${file.name}`, 'output.mp4']); +await ffmpeg.unmount('/input'); +``` + +`WORKERFS` mounts are read-only, so write outputs outside the mount point. + +### Why are all log messages of type `stderr`? + +FFmpeg writes its log, including the banner, progress and warnings, to +stderr, the same as native `ffmpeg`. Listen with `ffmpeg.on('log', ...)` to +read it. Only a few outputs, such as `ffprobe` results printed without +`-o`, go to stdout. + +### Why do raw frames differ slightly from native FFmpeg? + +Native FFmpeg uses assembly paths in swscale whose rounding differs a little +from the C code this build uses, so pixel values can be off by one. For +output closer to native, add `-sws_flags accurate_rnd+bitexact`. + +### How do I decode a WebM video with transparency? + +FFmpeg's built-in VP8 and VP9 decoders drop the alpha channel. Ask for the +libvpx decoder before the input: +`['-c:v', 'libvpx-vp9', '-i', 'input.webm', ...]`. The `full` preset, which +the published packages use, includes it. ### How can I build my own ffmpeg.wasm? diff --git a/apps/website/src/components/Playground/Workspace/Editor.tsx b/apps/website/src/components/Playground/Workspace/Editor.tsx index 53d9d661b30..c0cac2548c1 100644 --- a/apps/website/src/components/Playground/Workspace/Editor.tsx +++ b/apps/website/src/components/Playground/Workspace/Editor.tsx @@ -14,11 +14,20 @@ import "ace-builds/src-noconflict/mode-text"; import "ace-builds/src-noconflict/theme-dracula"; import "ace-builds/src-noconflict/theme-github"; -const genFFmpegText = (args: string) => { - let data: any = []; +// [] while the JSON is incomplete or not a list of strings. +export const parseArgs = (args: string): string[] => { try { - data = JSON.parse(args); - } catch (e) {} + const data: unknown = JSON.parse(args); + return Array.isArray(data) && data.every((a) => typeof a === "string") + ? data + : []; + } catch { + return []; + } +}; + +const genFFmpegText = (args: string) => { + const data = parseArgs(args); return `// equivalent ffmpeg.wasm API call ffmpeg.exec(${JSON.stringify(data)}); diff --git a/apps/website/src/components/Playground/Workspace/index.tsx b/apps/website/src/components/Playground/Workspace/index.tsx index e114f16ad0a..8ae9d5088f1 100644 --- a/apps/website/src/components/Playground/Workspace/index.tsx +++ b/apps/website/src/components/Playground/Workspace/index.tsx @@ -12,7 +12,7 @@ import { downloadFile } from "@site/src/util"; import { Node } from "./types"; import FileSystemManager from "./FileSystemManager"; import { SAMPLE_FILES } from "../const"; -import Editor from "./Editor"; +import Editor, { parseArgs } from "./Editor"; const defaultArgs = JSON.stringify(["-i", "video.webm", "video.mp4"], null, 2); @@ -155,7 +155,7 @@ export default function Workspace({ ffmpeg: _ffmpeg }: WorkspaceProps) { ffmpeg.on("log", logListener); ffmpeg.on("progress", progListener); const start = performance.now(); - const code = await ffmpeg.exec(JSON.parse(args)); + const code = await ffmpeg.exec(parseArgs(args)); setTime(performance.now() - start); ffmpeg.off("log", logListener); ffmpeg.off("progress", progListener);