feat: publish av-tools 0.1.0

This commit is contained in:
2026-07-24 14:58:03 +02:00
commit fcc537b024
254 changed files with 63270 additions and 0 deletions

141
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,141 @@
# Architecture
av-tools is a static, local-first React application. The browser receives code
and the two self-hosted FFmpeg cores; media bytes do not leave the origin
through an application backend. Toolbox Portal assembles the already-built
artifact and never compiles its source.
## Layers and boundaries
```text
Toolbox AppShell / standalone context
└─ React application
├─ Quick Convert and sequential Edit workflows
├─ project + typed preset state
├─ media inspection / preview / result views
├─ bounded application queue + resource policy
└─ typed command plans
└─ EngineManager
├─ @ffmpeg/ffmpeg wrapper worker
├─ ST or MT same-origin core
└─ per-job virtual filesystem
Browser storage boundary
├─ object URLs (ephemeral previews)
├─ IndexedDB (validated user presets)
└─ OPFS cache (optional derivatives; never source in the current app)
```
- `src/toolbox/` owns the one typed manifest and suite integration.
- `src/media/` normalizes ffprobe JSON, duration/timecode, stream selection,
playback MIME decisions and safe names.
- `src/project/` owns the versioned, non-destructive project document and
migrations. A source is represented by identity/metadata and must be
reattached after load; the current application does not persist source bytes.
- `src/presets/` defines typed built-ins and validates user presets against
detected capabilities.
- `src/commands/` converts validated operations to a `CommandPlan`. A plan has
generated input/output paths, argument arrays, expected outputs,
capabilities and cleanup intent; it is not a shell string.
- `src/ffmpeg/` selects/loads the engine, discovers capabilities, mounts
inputs, executes one job, reads outputs, sanitizes diagnostics and cleans.
- `src/jobs/` provides typed queue, progress, cancellation, recovery and
durable-metadata infrastructure. The mounted application scheduler is a
bounded in-memory promise chain around `EngineManager`.
- `src/waveform/`, `src/storage/` and `src/export/` keep bounded analysis,
persistence and browser-download responsibilities outside the UI.
The DOM receives normalized values as text/attributes. It never renders media
metadata as HTML. Command builders never accept arbitrary virtual paths or a
raw command field.
## FFmpeg lifecycle
1. Inspect secure-context, worker, `crossOriginIsolated` and
`SharedArrayBuffer` availability.
2. Select MT only when all MT prerequisites are true (or ST when explicitly
selected). A failed MT load is terminated and retried once with ST.
3. Load wrapper/core assets via base-relative, same-origin URLs.
4. Query version/build configuration/formats/codecs/encoders/filters
sequentially and publish immutable capability state.
5. For one job, validate resource estimates and structured settings, allocate
a generated directory, mount/write inputs, run a generated argument array,
read only declared outputs, and re-probe applicable results.
6. Return result bytes and an export report, then unmount/delete virtual files.
7. Retain the idle engine for the next sequential job; terminate it on mode
changes, unrecoverable errors or cancellation.
The FFmpeg instance is a single process. `maxConcurrentFFmpegJobs` is one and
no probe/export operations overlap on it. The application-facing scheduler is
also bounded: it retains at most eight scheduled operations in memory,
including the active operation, and advances its promise chain after either
success or failure. Queue state is deliberately not persisted across a page
reload.
## Project and edit model
A project is versioned JSON containing source descriptors, a sequential clip
timeline, non-destructive clip in/out and transform/audio settings, stream
selection, metadata/chapter/subtitle edits and export preset references. Clips
do not overlap. There is no arbitrary compositing graph, collaborative state or
server-side document.
The structural-export panel maps the selected clip's current range to either a
stream-copy or preset-backed trim plan. It maps the visible timeline order to
either a compatibility-checked stream-copy concat plan or a normalized
re-encode plan. Normalized concat requires the user to choose what happens when
some clips lack audio; unchecked fast compatibility fails closed.
Migrations are explicit version-to-version functions. Unknown future versions,
invalid shapes and unsafe source references fail with actionable validation
errors. Source bytes are not embedded in normal project JSON. Reattachment
matches stable descriptors and asks the user before associating a local file.
## Command plans and filesystem
Plans use arrays such as `['-i', generatedInput, ...]`; no shell evaluates
them. Filtergraph builders escape values for their exact FFmpeg grammar.
Generated paths contain a job id plus bounded safe basename and never reuse a
user-controlled directory. Expected output paths are declared before
execution, so the result reader cannot enumerate arbitrary output.
Small inputs may be copied into FFmpeg's in-memory filesystem. Where supported,
WORKERFS mounts `File`/`Blob` inputs to avoid an initial JavaScript copy; it
does not remove WASM address-space/output constraints. OPFS is optional for
regenerable derivatives and uses asynchronous origin-private file handles with
an in-memory fallback. See `docs/STORAGE.md`.
## Result flow
Output bytes become a Blob with an operation-derived filename and MIME type.
The UI creates a short-lived object URL only when the browser can preview that
container/codec, exposes an explicit save/download action, and includes probe
data plus warnings in the export report. Revoking a preview does not invalidate
the retained Blob until the result is cleared. Batch derivatives may be
assembled into a bounded ZIP.
For unsupported direct playback, a preview proxy is a disposable H.264/AAC MP4
derivative rather than a source mutation. Duration and maximum width are
bounded by validated user controls. Its cache identity includes the source
fingerprint and proxy settings, so only an exact matching derivative is reused;
cache failure falls back to normal generation.
## Cancellation and recovery
ffmpeg.wasm cancellation terminates the active wrapper/core worker rather than
pretending a native FFmpeg process can receive a signal. The job becomes
cancelled, outstanding promises are rejected, object URLs/mounts are released
where possible, and that engine generation is discarded. The next job creates
a fresh engine and re-runs capability discovery. A cancellation is complete
only after the stale worker cannot publish a result.
## Integration boundaries
`AppShell` owns suite chrome, theme/personalization, help and app switching.
av-tools passes `manifestUrl="./toolbox-app.json"` and works when context is
missing, malformed or unavailable. It does not embed another switcher.
The manifest advertises secure context, workers, IndexedDB and top-level
navigation. It does not require cross-origin isolation because ST is a valid
fallback. Portal receives only the deterministic ZIP/checksum and places it
under a locked target such as `apps/av/`.

143
docs/FFMPEG_BUILD.md Normal file
View File

@@ -0,0 +1,143 @@
# FFmpeg WebAssembly build record
av-tools ships the published ESM assets from `@ffmpeg/core` and
`@ffmpeg/core-mt` **0.12.10**. The browser wrapper is `@ffmpeg/ffmpeg`
**0.12.15** and helpers are `@ffmpeg/util` **0.12.2**. All versions are exact,
not ranges.
## Underlying build
The matching ffmpeg.wasm upstream release is `v12.15` at
`71aa99d37c02a7b4c435275ca9ef50e612f6efa1`. Its Dockerfile selects:
- FFmpeg **n5.1.4** (libavcodec 59.37.100);
- Emscripten SDK **3.1.40**;
- production `-O3 -msimd128`;
- pthread flags for the MT build and explicit thread disabling for ST.
The build configuration embedded in both copied WASM files is:
```text
--target-os=none
--arch=x86_32
--enable-cross-compile
--disable-asm
--disable-stripping
--disable-programs
--disable-doc
--disable-debug
--disable-runtime-cpudetect
--disable-autodetect
--nm=emnm
--ar=emar
--ranlib=emranlib
--cc=emcc
--cxx=em++
--objcc=emcc
--dep-cc=emcc
--extra-cflags='-I/opt/include -O3 -msimd128'
--extra-cxxflags='-I/opt/include -O3 -msimd128'
--enable-gpl
--enable-libx264
--enable-libx265
--enable-libvpx
--enable-libmp3lame
--enable-libtheora
--enable-libvorbis
--enable-libopus
--enable-zlib
--enable-libwebp
--enable-libfreetype
--enable-libfribidi
--enable-libass
--enable-libzimg
```
The ST string also has `--disable-pthreads --disable-w32threads
--disable-os2threads`; the MT build supplies Emscripten pthread flags instead.
At runtime the application executes `-version`, `-buildconf`, `-formats`,
`-codecs`, `-encoders` and `-filters` sequentially. Presets are enabled only
when the active core reports every required muxer, encoder and filter. Build
flags alone are not treated as a capability guarantee.
## External libraries
The upstream build references x264 `4-cores`, x265 3.4, libvpx v1.13.1, LAME
`master`, Ogg v1.3.4, Theora v1.1.1, Opus v1.3.1, Vorbis v1.3.3, zlib v1.2.11,
libwebp v1.3.2, FreeType VER-2-10-4, FriBidi v1.0.9, HarfBuzz 5.2.0, libass
0.15.0 and zimg release-3.0.5. These resolve to x264
`33cac6b77d5b9259c552156013a817ab23119612`, x265
`2bb5520e9596f361bf0ed81b3b8da0d7fd999069` and LAME
`2badea1974ae36cb8312afe99cff1e6b3b5decee`. All three commits predate the
ffmpeg.wasm v12.15 release; the branch-based x264/LAME commits remain the
respective branch tips. `SOURCE.md` records retrieval commands.
## Shipped files
| Mode | File | Bytes | SHA-256 |
| ---- | -------------------------- | -------: | ------------------------------------------------------------------ |
| ST | `st/ffmpeg-core.js` | 111804 | `67a48f11645f85439f3fde4f2119042c16b374b910206b7a7a24f342e28dcae3` |
| ST | `st/ffmpeg-core.wasm` | 32232419 | `9f57947a5bd530d8f00c5b3f2cb2a3492faa7e5d823315342d6a8656d0a6b7b7` |
| MT | `mt/ffmpeg-core.js` | 128947 | `270a2e6ff945e173238610669a3f7132df5f9c52698a9bf708cf5c2ab6bda0de` |
| MT | `mt/ffmpeg-core.wasm` | 32718323 | `be2c97605366b78f3f13e21b52e81a55a79e1f29c133b03a68ec187b1a2ec41a` |
| MT | `mt/ffmpeg-core.worker.js` | 2115 | `f77898d631dc010b45c29c23cb4379c611a7d7b131bf591d08a656bb729a4ca3` |
`npm run vendor:ffmpeg` copies these files from `node_modules` to
`public/vendor/ffmpeg/0.12.10/`; `npm run vendor:verify` checks versions,
sizes, paths and digests. Vite then copies them unchanged into `dist`. A normal
production build performs no network download.
## Multithread Chromium guardrail
The pinned multithread core can stall in Chromium when FFmpeg lets codecs use
the browser's full reported logical-core count. This is tracked upstream in
[ffmpeg.wasm issue 654](https://github.com/ffmpegwasm/ffmpeg.wasm/issues/654).
av-tools keeps the pthread core but adds `-threads 2` as an input and output
codec option for MT jobs. ST arguments remain unchanged. A regular browser
test performs an actual MT H.264/AAC conversion; loading the core alone is not
accepted as functional evidence.
Every execution also has a watchdog: an explicit shorter plan timeout wins;
otherwise the timeout is at least two minutes, scales to thirty times the
expected media duration, and is capped at the displayed four-hour product
limit. Timeout is still a failure, not a successful partial export.
## Long-session heap guardrail
The pinned Emscripten core was observed to end a later heterogeneous browser
workflow with a bare `Aborted()` even though the same command passed in a
fresh core and all per-job virtual files had been removed. Its `tile` filter
also aborted deterministically on a fourth consecutive contact sheet in local
stress runs. av-tools therefore refreshes an idle core before its seventeenth
media execution and before a fourth contact-sheet execution. Application queue
state, sources and completed results remain outside the worker and are not
discarded. A bare core abort also triggers recreation after cleanup so a
manual or queued retry starts on a fresh instance.
This is a conservative browser-memory guardrail, not a guarantee that sixteen
large jobs or three large contact sheets fit in memory. Cancellation and
failed filesystem-cleanup recovery continue to recreate the core immediately.
## Opus regression risk
Pinned core 0.12.10 predates upstream ffmpeg.wasm fixes associated with stack
size and Opus zero-byte/out-of-bounds failures (including upstream issues
`#591` and `#775`). Therefore:
- decoder/encoder listing is not enough to certify Opus;
- real ST and MT fixture encodes must produce a non-empty, re-probeable output;
- the Opus audio and VP9/Opus presets must remain disabled with a diagnostic
if that smoke fails;
- a core upgrade requires new checksums, capability snapshots, legal review
and regression tests.
This is a known pinned-core risk, not a claim that every Opus job fails.
## Licence consequence
The core packages declare GPL-2.0-or-later and the actual build enables GPL and
links external codecs. The wrapper/util are MIT. av-tools is
GPL-3.0-or-later, and the distributed application must not be described as
MIT-only. Publish the reviewed Corresponding Source and external notices with
the binary release. See `LICENSE`, `LICENSES/`, `THIRD_PARTY_NOTICES.md` and
`SOURCE.md`.

123
docs/FORMAT_SUPPORT.md Normal file
View File

@@ -0,0 +1,123 @@
# Format and feature support
FFmpeg capability is determined from the **loaded core**, not from filename
extensions or this document. The app queries muxers, demuxers, decoders,
encoders and filters; a preset is disabled with missing components listed.
Browser playback is a separate capability checked with MIME/media APIs.
## 0.1.0 candidate fixture matrix
All 15 files are generated and re-probed with native FFmpeg 7.1.3 for test
preparation and pass the checksum/probe verifier. That proves fixture
provenance, not ffmpeg.wasm behavior. A 2026-07-24 Vitest run passed 303 tests
with one conditional native subtitle-filter skip and includes broad execution
of typed plans against native FFmpeg 7.1.3.
Chromium automation also runs a broad operation matrix against the real pinned
ST core. The real MT core is queried and completes an H.264/AAC conversion, but
the complete ST operation matrix has not been duplicated under MT. The
following table distinguishes exact browser evidence from still-pending
coverage; it must not be generalized to Firefox, Safari or mobile.
| Input | Fixture | Expected streams | Browser preview expectation | Recorded pinned-core Chromium evidence |
| ------------------- | ---------------------------------- | -------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------- |
| MP4 / H.264 + AAC | `pattern-av.mp4`, compatible clips | H.264 video, AAC audio | commonly playable after `canPlayType`/media load succeeds | broad ST operations; real MT conversion; exhaustive MT pending |
| WebM / VP8 + Vorbis | `pattern.webm` | VP8 video, Vorbis audio | commonly playable in Chromium/Firefox; browser-dependent elsewhere | ST probe, remux and VP8/Vorbis re-encode; MT pending |
| WAV / PCM s16le | `tone.wav` | mono PCM | commonly playable | ST WAV output re-probed; exact input fixture and MT pending |
| MP3 | `tone.mp3` | mono MP3 | commonly playable | ST MP3 outputs re-probed; exact input fixture and MT pending |
| FLAC | `tone.flac` | mono FLAC | browser/version-dependent | ST input and FLAC output re-probed; MT pending |
| MP3 + attached PNG | `attached-cover.mp3` | MP3 audio, attached-picture PNG | audio may play; cover rendering is app-owned | ST probe/stream selection; MT pending |
| MP4 tags | `metadata.mp4` | H.264/AAC | same as MP4 | ST import/removal and output metadata round-trip; MT pending |
| MP4 chapters | `chapters.mp4` | H.264/AAC/data plus two chapters | media may play; browser does not expose all chapter data | ST generated-output chapter round-trip; fixture import pending |
| malformed MP4 | `truncated.mp4` | no usable streams expected | must not preview | ST bounded failure, cleanup and recovery; MT pending |
The runtime query for pinned core 0.12.10 recorded 341 demuxers, 173 muxers,
515 decoders, 204 encoders and 465 filters (1,698 entries total). Runtime
discovery only says that FFmpeg exposes an item; the application must still
build a valid plan, the input must be compatible, resources must fit and the
browser may still be unable to preview the result.
## Output presets
Preset availability always depends on detected runtime requirements:
| Preset family | Required examples | Browser result preview |
| ------------------ | ---------------------------------------- | ------------------------------------------------------ |
| MP4 H.264/AAC | `mp4`, `libx264`, `aac` | usually, but verify actual output |
| WebM VP9/Opus | `webm`, `libvpx-vp9`, `libopus` | browser-dependent; Opus core regression gate applies |
| WebM VP8/Vorbis | `webm`, `libvpx`, `libvorbis` | commonly Chromium/Firefox |
| Matroska H.264/AAC | `matroska`, `libx264`, `aac` | generally download-only in browsers |
| animated WebP | `webp`, `libwebp_anim` | browser-dependent image preview |
| GIF | `gif`, `gif`, `palettegen`, `paletteuse` | broadly image-previewable |
| MP3 | `mp3`, `libmp3lame` | commonly playable |
| M4A/AAC | `ipod`, `aac` | commonly playable |
| Ogg Vorbis | `ogg`, `libvorbis` | browser-dependent |
| Opus | `opus`, `libopus` | browser-dependent; pinned-core regression gate applies |
| WAV/PCM | `wav`, `pcm_s16le` | commonly playable |
| FLAC | `flac`, `flac` | browser-dependent |
“Lossless” applies only to a lossless audio codec such as PCM/FLAC relative to
its decoded samples. It does not make a lossy source lossless. Stream-copy
remux avoids re-encoding compatible streams but may change container metadata.
## Structural editing
- The Edit workspace exposes fast/accurate trim for the selected timeline range
and fast/normalized concat for clips in visible timeline order. Availability
still fails closed against loaded-core capabilities.
- Fast trim uses stream copy where container/stream/timestamp constraints are
compatible. It can begin at a nearby keyframe and is not promised
frame-accurate.
- Accurate trim decodes/re-encodes selected streams and can still be limited by
source timestamps.
- Stream-copy concat requires compatible codec parameters, time bases,
dimensions, sample layout and selected streams; the UI requires a completed
compatibility check before enabling it. Otherwise normalized re-encoding is
required.
- Normalized concat uses an explicit export preset and requires an explicit
insert-silence, drop-all-audio or reject policy when clips have mismatched
audio presence.
- Crop, resize, frame-rate conversion, fades, normalization, burn-in and
contact sheets require specific filters discovered at runtime.
- Even-dimension correction is applied when the selected pixel format/encoder
requires it and is reported in the plan/result.
## Subtitles
The text fixtures cover SRT, WebVTT and ASS import with explicit time shift.
The Chromium ST matrix converts a generated SRT to WebVTT with a time shift,
soft-muxes it into Matroska, burns it into H.264 video and extracts the
soft-muxed stream. The exact standalone WebVTT and ASS fixtures and equivalent
MT paths are not separately browser-smoked. Soft-subtitle muxing depends on
container support (for example converted text in MP4 versus text/ASS in
Matroska). ASS burn-in requires `subtitles`/libass and local font support; no
remote font is fetched. Bitmap subtitle streams are identified but are not
claimed to support arbitrary conversion or OCR. Unsupported container/codec
combinations must remain disabled with reasons.
## Metadata and chapters
Format and stream tags can be inspected. Edits/removal are limited to fields
the selected muxer writes; “remove metadata” is best effort and the output is
re-probed to report remaining structural/encoder/container fields. Stream
language, title and dispositions are separate from format tags.
Chapters use validated integer time bases, monotonic non-overlapping ranges and
escaped titles. Some output containers do not support chapters. Browser media
elements do not reliably expose them, so ffprobe round-trip is authoritative.
## Known unsupported or conditional cases
- remote protocols, live capture, DRM/encrypted media and network playlists;
- hardware-accelerated encoding or desktop-FFmpeg performance;
- files above the 2 GiB WASM input ceiling or stricter product policy;
- arbitrary filters/commands, arbitrary compositing and nonlinear overlap;
- frame-accurate stream-copy trimming;
- complete metadata erasure/anonymity;
- browser preview of every FFmpeg-readable result;
- Opus presets until real non-empty ST/MT encoding smoke passes for core
0.12.10;
- any capability omitted by the active core's runtime query.
Update this file only from re-probed generated/public-domain fixtures and
record the engine mode, browser and exact core version.

81
docs/KNOWN_LIMITATIONS.md Normal file
View File

@@ -0,0 +1,81 @@
# Known limitations
This is the conservative 0.1.0 development baseline. A capability appearing in
FFmpeg output does not remove these product/browser/resource limits.
## Functional
- The editor is a sequential light editor: no overlapping tracks, arbitrary
compositing, collaborative state or professional nonlinear workflow.
- Remote URLs/protocols, live capture, DRM/encrypted media and server-side
transcoding are outside scope.
- Fast stream-copy trim can start at a nearby keyframe; it is not promised
frame-accurate. Accurate mode requires decoding/re-encoding.
- Stream-copy concatenate works only when selected streams and codec/container
parameters are compatible. Other input needs normalized re-encoding.
- Re-encoding is not lossless. PCM/FLAC can preserve decoded audio samples but
cannot restore information already lost in a source.
- Metadata removal is best effort. Muxers may add structural/encoder fields,
unknown structures can remain, and this is not an anonymity guarantee.
- Subtitle support is text/container/filter-dependent. No OCR or universal
bitmap-subtitle conversion is promised; burn-in never fetches remote fonts.
- Projects omit source bytes by default and therefore require local-file
reattachment after reopening.
- The operation queue is bounded to eight scheduled items, runs one FFmpeg
operation at a time and exists only in page memory. Reloading or closing the
page discards queued work.
- A file-generating batch and its managed result collection are bounded to 255
outputs, leaving the 256th ZIP entry available for an optional export report.
- Preview proxies are bounded, lossy H.264/AAC derivatives for local playback;
they do not replace the source or guarantee that every browser can play the
result.
## Format and browser
- There is no “all FFmpeg formats supported” claim. The active core gates
presets from runtime formats/codecs/encoders/filters.
- FFmpeg-readable output may be download-only because browser media elements
support fewer container/codec combinations.
- Browser support, codec policy and memory differ across Chromium, Firefox and
Safari and across desktop/mobile versions.
- Core 0.12.10 has an Opus regression risk. Opus remains release-gated until
non-empty, re-probeable ST and MT fixture outputs pass.
- MT needs HTTPS, COOP/COEP/CORP and `SharedArrayBuffer`. ST fallback works
without isolation but is not a guarantee that a large job fits.
- Top-level navigation is supported; iframe embedding is not.
## Resource and performance
- ffmpeg.wasm documents a 2 GiB input ceiling; the application uses stricter
1.5 GiB hard input/total and 1 GiB estimated-output defaults. Practical
limits may be far lower.
- Only one FFmpeg job runs at once. Core startup downloads/instantiates roughly
32 MiB of WASM and capability discovery adds latency.
- To bound retained native heap state in long heterogeneous sessions, an idle
core is refreshed before its seventeenth media execution and before a fourth
contact sheet. The queue and completed results survive, but that next job
pays core startup and capability discovery latency again.
- WebAssembly can be substantially slower than native FFmpeg and has no
supported hardware encoder.
- Mobile/background-tab throttling, browser quota eviction and page
termination can interrupt a job. Progress is an estimate.
- OPFS/IndexedDB are origin-private, quota-managed and not guaranteed durable.
## Deployment and release
- A Portal deployment must permit `media-src 'self' blob:`, serve WASM as
`application/wasm`, retain the isolation headers and cache only versioned
FFmpeg assets as immutable. These are deployment requirements and require
verification against the live container.
- The application is GPL-3.0-or-later and its native legal files and pinned
source identities are included in the release record. Publish the production
ZIP/checksum, exact `v0.1.0` source and FFmpeg Corresponding Source bundle
together; development-only smoke artifacts must not be distributed.
- Chromium covers a broad real pinned-core ST operation matrix, a real MT
H.264/AAC conversion, MT-to-ST fallback, nested static/Toolbox-context
loading and one two-size ST/MT performance baseline. Firefox, Safari, mobile,
exhaustive MT feature parity and repeated performance measurements remain
unrecorded. Live Portal container/header verification is a deployment gate.
See `docs/FORMAT_SUPPORT.md`, `docs/PERFORMANCE.md`,
`docs/PORTAL_REQUIREMENTS.md` and `docs/RELEASE.md` for testable detail.

81
docs/PERFORMANCE.md Normal file
View File

@@ -0,0 +1,81 @@
# Performance and resource observations
ffmpeg.wasm runs native media code compiled to WebAssembly. It is normally much
slower than desktop FFmpeg and may hold source, decoded frames, filters and
output at once. No hardware encoder is used.
## Fixed download footprint
Uncompressed self-hosted core assets for 0.12.10:
| Mode | JavaScript/worker | WASM | Total |
| ------------- | ------------------: | -----------: | -----------: |
| single-thread | 111,804 B | 32,232,419 B | 32,344,223 B |
| multithread | 128,947 B + 2,115 B | 32,718,323 B | 32,849,385 B |
Only the selected mode is loaded. HTTP compression may reduce JavaScript
transfer but not necessarily WASM to the same degree; report transfer and
decoded sizes separately. Versioned core assets may be cached immutably.
## Benchmark record
One opt-in pinned-core baseline was recorded on 2026-07-24 with Chrome for
Testing 149.0.7827.55 (Playwright 1.61.1), Linux 7.0.0-28 on Ubuntu 24.04,
Intel Core i7-8700 (6 cores/12 threads) and 31 GiB RAM.
`navigator.hardwareConcurrency` reported 12 and every sample was
cross-origin-isolated. Each row used a fresh browser context; the first core
fetch may populate the browser HTTP cache, so later rows are explicitly
potentially warm-cache observations.
Preset: MP4, H.264/libx264 CRF 23 medium, AAC. Times are milliseconds.
“Export end-to-end” spans the Convert click through result construction,
cleanup, verification re-probe and visible result. The three narrower engine
columns come from the export validation report.
| Source | Input bytes | Mode | Init | Probe | FFmpeg exec | Output read | Cleanup | Export end-to-end | Output bytes | Observed JS heap |
| --------------------------------------- | ----------: | ----------------- | ----: | ----: | ----------: | ----------: | ------: | ----------------: | -----------: | ---------------: |
| 2 s, 160×90 H.264/AAC synthetic pattern | 34,057 | ST | 294.4 | 131.4 | 315.6 | 1.0 | 1.2 | 422.4 | 68,700 | 10,000,000 B |
| same | 34,057 | MT (`-threads 2`) | 914.5 | 126.4 | 294.3 | 1.3 | 1.9 | 416.8 | 68,753 | 10,000,000 B |
| 6 s, 640×360 H.264/AAC synthetic loop | 526,632 | ST | 270.2 | 163.6 | 6,854.1 | 1.3 | 0.8 | 7,030.9 | 585,981 | 15,200,000 B |
| same | 526,632 | MT (`-threads 2`) | 946.3 | 121.6 | 4,053.1 | 1.6 | 0.5 | 4,497.5 | 585,884 | 11,200,000 B |
Command:
```sh
npm run benchmark:browser
```
Result: **1 passed (18.2 s)**; test body 17.6 s. Chromium exposed
`performance.memory.usedJSHeapSize`, but this coarse post-operation value is
not a peak-WASM-memory measurement. One run is not a statistical distribution;
cold-cache repetitions, median/range, mobile and other-browser measurements
remain release-quality evidence gaps. Never present this workstation as a
universal promise.
## Measurement boundaries
- initialization: wrapper construction through successful capability query;
- probe: input mount/write through parsed ffprobe JSON;
- conversion: accepted command through exit code;
- output read: exit through Blob/result construction;
- cleanup: final result read through job directory unmount/delete;
- cancellation: cancel request through worker termination and recoverable idle
state (fresh-engine load is a separate measure).
Progress events are experimental estimates and not a timer. Use monotonic
browser timing around lifecycle boundaries.
## Product limits
Default soft input warning is 256 MiB; hard single/total input is 1.5 GiB.
Estimated output warns at 512 MiB and is blocked at 1 GiB. FFmpeg jobs are
serial; the application retains at most eight scheduled operations in its
in-memory queue. Timeline clips are capped at 100, streams per input at 64,
images at 8192×8192/67,108,864 pixels, generated output files at 255,
contact-sheet cells at 500 and runtime at four hours. The documented
ffmpeg.wasm input ceiling is 2 GiB, but real limits can be substantially lower
on mobile or memory-constrained browsers.
Lowering resolution, duration, frame rate, passes and output count reduces
work. MT can be faster but consumes more memory/CPU and requires isolation; ST
is not inherently a low-memory guarantee.

115
docs/PORTAL_REQUIREMENTS.md Normal file
View File

@@ -0,0 +1,115 @@
# Toolbox Portal requirements
av-tools is built and packaged independently. Toolbox Portal consumes the
exact ZIP and SHA-256 from a reviewed release lock; it must never import the
app source into its React build.
## Required serving behavior
All app, wrapper, core, WASM and worker responses are same-origin and served
over HTTPS (or trusted localhost). Apply:
```text
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Resource-Policy: same-origin
X-Content-Type-Options: nosniff
```
Serve `.wasm` as `application/wasm`, JavaScript modules as a JavaScript MIME
type and JSON as `application/json`. Test real responses under
`/apps/av/vendor/ffmpeg/0.12.10/...`; a file-extension allowlist alone does not
prove the MIME type.
Suggested CSP:
```text
default-src 'self';
script-src 'self' 'wasm-unsafe-eval';
worker-src 'self' blob:;
connect-src 'self';
img-src 'self' blob: data:;
media-src 'self' blob:;
font-src 'self';
style-src 'self';
object-src 'none';
frame-src 'none';
frame-ancestors 'none';
base-uri 'none';
form-action 'none';
```
`'wasm-unsafe-eval'` is the narrow WebAssembly permission; broad
`'unsafe-eval'` is unnecessary. `worker-src blob:` covers the wrapper-created
worker path. `media-src blob:` permits local source/result previews.
## Immutable reference and active-worktree state
Portal revision `bda9da044d61c109afd9969d357cc27f76648938` already sends COOP
`same-origin`, COEP `require-corp`, CORP `same-origin`, `script-src 'self'
'wasm-unsafe-eval'`, and `worker-src 'self' blob:`. Its packaged nginx CSP does
**not** declare `media-src`; `default-src 'self'` therefore blocks local
`blob:` audio/video URLs. The expected browser diagnostic is equivalent to:
```text
Refused to load media from 'blob:…' because it violates
"default-src 'self'".
```
Smallest required Portal change: append exactly
`media-src 'self' blob:;` to that CSP. This permits only same-origin and
in-memory Blob media; it does not permit remote media hosts, inline script or
eval. If the Portal is not changed, conversion/download can remain available
but Blob source/result preview must show a clear disabled reason.
As inspected on 2026-07-24, the active
`/mnt/DATA/git/toolbox-portal/deploy/nginx.conf` contains that exact directive
as a local worktree patch. The worktree still resolves to the immutable
revision above, so the patch is useful for local deployment testing but is not
a committed Portal revision and must not be cited as published release
evidence. The clean-reference assembly smoke therefore continues to require
`--allow-known-header-gap` until a reviewed Portal commit contains the fix.
The nginx profile includes `.wasm` in its static allowlist/gzip types but
relies on the base image `mime.types`. Verify `Content-Type:
application/wasm` against the pinned image; add an explicit mapping if the
response differs.
## Caching
The immutable Portal reference's cache map makes Vite content-hashed assets
immutable but leaves versioned FFmpeg cores at `no-cache`. Correctness is
unaffected.
For performance, the exact directory
`apps/av/vendor/ffmpeg/0.12.10/{st,mt}/` may receive:
```text
Cache-Control: public, max-age=31536000, immutable
```
Only do this while a version path is immutable and verified against
`version.json`. Keep `index.html`, manifest/catalog/release JSON and legal
notices revalidated.
## Nested path and context
The artifact uses `base: './'`, `entry: './'`, a relative icon, a relative
manifest URL and relative core URLs. Portal may therefore target `av`, yielding
`apps/av/`. Do not rewrite files to root-absolute `/assets` or `/vendor`.
Use top-level navigation; embedding is unsupported.
The app remains usable without valid Toolbox context. It validates context
version/origin and falls back to standalone shell behavior without moving or
duplicating suite controls.
## Fallback contract
MT requires all isolation headers plus secure context and
`SharedArrayBuffer`. When any prerequisite or MT load fails, the app terminates
that instance and loads ST. The manifest intentionally declares
`crossOriginIsolated: false` because isolation is not an unconditional
requirement; it declares workers, secure context, IndexedDB and top-level
context.
See `scripts/portal-assembly-smoke.mjs` and `docs/RELEASE.md` for the temporary,
non-mutating assembly procedure.

View File

@@ -0,0 +1,40 @@
# Reference implementations
Inspection date: **2026-07-24**.
The references were cloned beside av-tools under
`/mnt/DATA/git/references/`; no reference repository was nested in this
working tree. Revisions are full immutable commit ids.
| Repository | Revision inspected | Used for | Copied or adapted |
| -------------------------------------------------------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| av-tools, `git@git.add-ideas.de:zemion/av-tools.git` | no revision: canonical `main` was unborn and had no commits | canonical destination and licence/history check | the empty repository was scaffolded in place; no prior code existed |
| Toolbox SDK, `https://git.add-ideas.de/zemion/toolbox-sdk.git` | `53c40a61ba1581246f65773fcbb1c1cfd31ac98e` (SDK 0.2.2) | manifest schema/definition, `AppShell`, context behavior, shared styles, token-free registry, nested-path checks and toolchain baseline | published 0.2.2 packages are direct dependencies; documented usage patterns were followed; no SDK source file was copied |
| Toolbox Portal, `https://git.add-ideas.de/zemion/toolbox-portal.git` | `bda9da044d61c109afd9969d357cc27f76648938` | release ZIP/sidecar/lock formats, independent assembly, catalogue behavior, path security, nested deployment and production headers | no portal application source was copied; the documented artifact contract informed an independent release packager and smoke harness |
| ffmpeg.wasm, `https://github.com/ffmpegwasm/ffmpeg.wasm.git` | `f876f907c7e9b9bf51d4ed0b913a855a63ae63fc` | current API, worker lifecycle, filesystem, loading, ST/MT behavior, licence documentation and build layout | no reference-worktree code was copied; exact published npm packages are dependencies and their five core assets are copied verbatim by the vendor script |
For the **pinned** package set, the more specific upstream release tag
`v12.15` at `71aa99d37c02a7b4c435275ca9ef50e612f6efa1` was also inspected. It
contains `@ffmpeg/ffmpeg` 0.12.15 and core/core-mt 0.12.10. That revision's
build selects FFmpeg n5.1.4.
The user's active `/mnt/DATA/git/toolbox-portal` worktree still has Portal
revision `bda9da044d61c109afd9969d357cc27f76648938` as `HEAD`, but currently
contains a local `media-src 'self' blob:` nginx CSP patch. That mutable
worktree state is documented for deployment testing only; it does not replace
the immutable reference in the table.
Official behavior references:
- `https://ffmpegwasm.netlify.app/docs/overview/`
- `https://ffmpegwasm.netlify.app/docs/getting-started/usage/`
- `https://ffmpegwasm.netlify.app/docs/api/ffmpeg/classes/ffmpeg/`
- `https://ffmpegwasm.netlify.app/docs/faq/`
- `https://ffmpeg.org/ffmpeg.html`
- `https://ffmpeg.org/ffprobe.html`
- `https://ffmpeg.org/ffmpeg-formats.html`
- `https://ffmpeg.org/ffmpeg-filters.html`
- `https://ffmpeg.org/ffmpeg-utils.html`
Command plans are based on those primary references and runtime capability
queries, not unverified command snippets.

130
docs/RELEASE.md Normal file
View File

@@ -0,0 +1,130 @@
# Release process
av-tools is licensed GPL-3.0-or-later. A publishable release is the exact
reviewed source tag, production ZIP/checksum, Corresponding Source and
third-party notices distributed together. The steps below fail closed where
practical.
## 1. Resolve legal/source identity
The repository already declares the application licence and immutable
third-party source identities. Before any distribution:
1. commit the reviewed GPL-3.0-or-later source;
2. create and verify annotated tag `v0.1.0` at that exact commit;
3. push the commit and tag to the canonical public repository;
4. assemble, review and publish corresponding source and exact external
notices for both core packages and linked libraries;
5. keep the manifest/package metadata, `SOURCE.md`, ZIP and tag version aligned.
`SOURCE.md` pins FFmpeg, ffmpeg.wasm, x264 and LAME by full commit. Do not
describe the complete bundle as MIT-only.
## 2. Reproducible checks
With Node 22+ and the lockfile:
```sh
npm ci
npm run fixtures:verify
npm run check
npm run test:browser
```
`check` verifies manifest/vendor assets, types, lint/format, unit tests,
production build and `toolbox-check dist`. Browser tests must exercise ST, MT,
ST fallback, cancellation/recovery, nested paths and no runtime CDN request.
Record exact results in the release report.
## 3. Package
Normal packaging validates the root GPL version 3 text and matching
`GPL-3.0-or-later` package/manifest metadata:
```sh
npm run package:release
npm run source:package
```
The legacy development-only escape hatch is retained only for checking an
otherwise unlicensed temporary fixture:
```sh
npm run package:release -- --allow-unlicensed-development-smoke
```
Pass `--force` only to replace the exact same-version ZIP and sidecar. Output:
```text
release/av-tools-0.1.0.zip
release/av-tools-0.1.0.zip.sha256
release/ffmpeg-core-0.12.10-corresponding-source.tar.xz
release/ffmpeg-core-0.12.10-corresponding-source.tar.xz.sha256
```
Verify independently:
```sh
node scripts/checksum-release.mjs
(cd release && sha256sum -c ffmpeg-core-0.12.10-corresponding-source.tar.xz.sha256)
unzip -l release/av-tools-0.1.0.zip
tar -tJf release/ffmpeg-core-0.12.10-corresponding-source.tar.xz
```
The packager:
- rejects symlinks, special files, traversal, absolute/ambiguous/duplicate
archive paths and credential-like files;
- rejects source maps and local absolute paths in text release files;
- requires entry, manifest, icon and every declared asset;
- requires root `CHANGELOG.md`, `SOURCE.md`, `THIRD_PARTY_NOTICES.md`,
`LICENSES/`, `assets/` and `vendor/`;
- adds installed runtime npm licence files;
- sorts paths, stores files as mode 0644, uses 1980-01-01 00:00:00 UTC and
produces deterministic ZIP32 bytes;
- excludes `node_modules`, test fixtures and source because it starts from
`dist` plus an explicit legal/document list.
When used against a fixture without an application licence, the escape hatch adds
`DEVELOPMENT-ONLY-NOT-FOR-DISTRIBUTION.txt` inside the ZIP.
## 4. Portal assembly smoke
Use the clean reference clone beside av-tools, never the user's active Portal
worktree:
```sh
npm run portal:smoke -- \
--artifact release/av-tools-0.1.0.zip \
--allow-known-header-gap
```
The smoke harness clones the exact Portal HEAD to a temporary directory,
copies the ZIP there, writes a temporary one-app lock derived from
`release/toolbox.lock.example.json`, runs `npm ci`, tests, build and assembly,
then verifies the generated catalogue/release records and copied FFmpeg assets.
The temporary directory is removed unless `--keep-temp` is explicitly passed.
No machine-specific path is committed and no Portal worktree is changed.
Without `--allow-known-header-gap`, the clean immutable Portal reference's CSP
omission of `media-src blob:` is a failing deployment check. The override still
reports the gap and is only for testing archive assembly. See
`docs/PORTAL_REQUIREMENTS.md`.
The user's active `/mnt/DATA/git/toolbox-portal` worktree contains the narrow
directive as a local patch, but the smoke intentionally does not consume that
mutable worktree. Until the patch exists in a reviewed immutable Portal
revision, retain the clean-reference failure/override behavior above.
## 5. Publish only reviewed bytes
From a clean `v0.1.0` checkout, rerun all checks, create the ZIP once, verify
the sidecar, assemble that exact ZIP through Portal, and publish the immutable
ZIP, `.zip.sha256`, manifest, changelog, Corresponding Source and notices. Put
its exact lowercase digest/version/id/target in a reviewed Portal lock. Never
use mutable “latest” URLs or credentials in a lock.
The final report must state inspected commits, exact packages/build config,
capabilities, completed tested milestones, all check results, artifact paths,
Portal result, fixture provenance, licence status, headers, gaps, performance
and browser limits. A UI or command builder alone is not a completed milestone.

140
docs/SECURITY.md Normal file
View File

@@ -0,0 +1,140 @@
# Security and privacy
## Trust boundary
Imported media, filenames, container metadata, tags, chapters, subtitles,
attachments, codec logs, project JSON and Toolbox context are untrusted. FFmpeg
parsing occurs in a worker but still consumes memory/CPU in the user's browser.
The application cannot make a malicious codec safe; it limits exposure and
keeps cleanup/recovery explicit.
The supported product has:
- no upload or processing backend;
- no telemetry, analytics or third-party runtime script;
- no remote-media protocol or automatic external fetch;
- no shell/native command execution;
- no arbitrary FFmpeg command input;
- no persistent source copy in the current application.
Browser extension, operating-system, reverse-proxy and compromised dependency
behavior remain outside this boundary.
## Paths and command construction
User names are display labels only. Virtual filesystem paths are generated from
job ids plus sanitized, length-bounded basenames; separators, traversal,
control characters, reserved names and collisions are handled centrally.
Outputs must be declared in a typed plan before execution.
Commands are argument arrays assembled from discriminated operation/preset
types. No shell parses them. Filters, metadata, concat lists, chapter metadata
and subtitle values use syntax-specific escaping; one escaping routine is not
reused across incompatible grammars. Capability requirements are checked before
execution. Logs are line/byte bounded and local path-like names are redacted in
exportable diagnostics.
## Rendering and external URLs
Metadata is rendered as React text, never `dangerouslySetInnerHTML`. Subtitle
text is parsed and rendered or sent through a generated local file; it is not
inserted as HTML. Extracted URLs are inert text. The application does not open
links from media automatically. No remote font is fetched; ASS burn-in uses
packaged/system-generic font configuration only when the required local
capability is available.
Preview/download URLs are `blob:` values made from local bytes. They are
revoked on replacement, project close and unmount. Browser support for a
container is not evidence that its contents are safe or fully decodable.
## Resource controls
The administrator-adjustable defaults are product limits, not FFmpeg limits:
| Limit | Default |
| --------------------------------- | ----------------: |
| soft input warning | 256 MiB |
| hard single input | 1536 MiB |
| hard total input | 1536 MiB |
| soft output estimate | 512 MiB |
| hard output estimate | 1024 MiB |
| concurrent FFmpeg jobs | 1 |
| scheduled operations in memory | 8 |
| timeline clips | 100 |
| streams per input | 64 |
| image dimension / pixel count | 8192 / 67,108,864 |
| generated outputs / contact cells | 255 / 500 |
| job runtime | 4 hours |
Before allocation, the application estimates source/output bytes, duration,
resolution, frame/pixel work, passes, retained results and output count.
Estimates are labelled as estimates. Hard-limit violations do not start; soft
violations explain resolution/duration/output-count alternatives. The
255-output limit keeps a whole managed batch saveable in the 256-entry ZIP
limit with an optional report. Memory-constrained devices receive stronger
warnings only when a reliable browser signal exists.
WASM itself has a documented 2 GiB input ceiling; practical browser limits can
be much lower, especially when input and output coexist. Archive/export and
cache operations also enforce entry, total-size and path limits.
## Workers and cancellation
Wrapper, core, pthread and waveform workers are same-origin or short-lived
`blob:` workers created by trusted application code. OPFS caching currently
uses asynchronous browser file handles rather than a separate storage worker.
MT requires shared memory and cross-origin isolation. ST loads when isolation
is absent. Cancelling FFmpeg terminates and discards its worker/core; a new
instance is required before another job. Stale generations cannot commit a
result.
## Content Security Policy
Tested deployment policy target:
```text
default-src 'self';
script-src 'self' 'wasm-unsafe-eval';
worker-src 'self' blob:;
connect-src 'self';
img-src 'self' blob: data:;
media-src 'self' blob:;
font-src 'self';
style-src 'self';
object-src 'none';
frame-src 'none';
frame-ancestors 'none';
base-uri 'none';
form-action 'none';
```
Do not replace `'wasm-unsafe-eval'` with broad `'unsafe-eval'`. All core assets
are same-origin. `media-src blob:` is required for result/source previews.
The exact headers, immutable Portal reference gap and active-worktree local
patch are in `docs/PORTAL_REQUIREMENTS.md`.
## Cross-origin isolation
MT needs secure HTTPS (localhost during development is acceptable),
`SharedArrayBuffer`, and:
```text
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Resource-Policy: same-origin
```
Isolation affects every resource in the page. Do not weaken COEP or allow
cross-origin dependencies merely to make MT load. The safe fallback is ST.
## Dependency and vulnerability response
Exact runtime versions and core digests are recorded in `SOURCE.md`. Run
`npm audit`, tests, real core smoke tests and legal review before an update.
Core upgrades change a large native attack surface and require new capability
snapshots, fixtures, checksums and browser tests.
Report vulnerabilities privately to the repository owner through the Gitea
instance when possible. Do not attach sensitive media, credentials or an
unredacted diagnostic log to a public issue. Include the app/core version,
browser, mode, smallest synthetic reproduction, impact and cleanup result.

93
docs/STORAGE.md Normal file
View File

@@ -0,0 +1,93 @@
# Storage and cleanup
av-tools separates source bytes, engine files, derivatives and project
metadata. The default policy is deliberately non-persistent for source media.
## Object URLs
Browser preview and result playback use `URL.createObjectURL(File|Blob)`.
Object URLs reveal bytes only to the current browser context but retain the
underlying object until revoked. Each owner revokes its previous URL when a
source/result changes and all URLs on project close/unmount. A preview failure
does not trigger an upload or external fallback.
## FFmpeg virtual filesystem
Every execution receives a generated job directory. Inputs are copied to the
in-memory filesystem or mounted read-only through WORKERFS where browser/core
support is verified. Outputs are always in generated paths. The runner reads
only declared outputs, then deletes output/input/control files and unmounts.
WORKERFS can avoid an initial JavaScript byte copy; FFmpeg decoding and output
still consume WASM/browser memory. Cancelling terminates the engine, so cleanup
also discards that entire virtual filesystem.
Each probe/export records its actual input mode and byte accounting. A
successful WORKERFS mount reports zero copied input bytes; if mounting fails,
the bounded compatibility fallback reports the exact bytes copied into engine
memory. This is runtime evidence rather than a speculative preflight claim.
## IndexedDB
The mounted application currently stores validated user presets in IndexedDB.
The codebase also contains a durable job-metadata store, but the application
queue described in the UI is deliberately in memory and does not instantiate
that store. Project documents are validated, versioned JSON that the user
explicitly saves and loads; they are not currently auto-saved to IndexedDB.
Media source bytes are not written there by default.
Clearing site data removes these records. Private browsing and quota eviction
may remove them earlier; the app must treat storage as recoverable cache, not
the only copy of an important user preset.
## Origin-private file system
OPFS is optional for bounded preview proxies, waveform peaks, thumbnails and
other regenerable derivatives. The current implementation uses asynchronous
origin-private file handles and falls back to a 128 MiB least-recently-used
in-memory cache if OPFS is unavailable or fails. A derivative key includes a
source fingerprint, operation, relevant settings and a cache-schema version;
waveform keys additionally identify the selected stream and analysis settings.
Preview-proxy settings are part of that identity. The current UI validates a
1300 second maximum duration and a 480, 720, 960, 1280 or 1920 pixel maximum
width. A matching local derivative is reused without FFmpeg execution; changing
the source or either setting produces a different cache lookup. A cache miss or
cache failure falls back to generation and never mutates/persists the source.
The application works without OPFS. The storage panel reports the browser's
origin-wide quota estimate when available and offers per-entry or global cache
deletion. The current OPFS layer relies on browser quota enforcement rather
than pre-reserving space; a failed cache write switches subsequent cache work
to the bounded in-memory fallback. OPFS writes close their
`FileSystemWritableFileStream` on success and abort it on error.
OPFS is private to the current **origin**. Moving an app from one hostname,
scheme or port to another does not move cached data. Toolbox and standalone
deployments at different origins have different caches even if their files are
identical.
## Source persistence policy
Normal import retains the user-selected `File` only in the active page. Saved
projects contain source descriptors/fingerprints and prompt for reattachment;
they do not embed source bytes. If a later UI offers “keep source on this
device,” it must be explicit, show estimated/quota usage, identify the origin,
allow per-source deletion and never be silently enabled by a project import.
Downloads are explicit user actions. The File System Access API, when
available, writes only after the user selects a target; Blob download remains
the fallback.
## Cleanup matrix
| Resource | Normal completion | Cancellation/error | User control |
| ------------------------ | ---------------------------------------------------- | -------------------------------------------------------- | ----------------------- |
| source/result object URL | retained only while displayed, then revoked | revoked when owning state clears | clear source/result |
| FFmpeg job files/mount | delete/unmount in finalization | engine termination discards FS; stale generation ignored | cancel job/reset engine |
| result Blob | retained until replaced/cleared/download page closes | failed partial output is not published | clear result |
| IndexedDB user presets | retained intentionally | transaction rollback | delete/reset presets |
| OPFS derivative | retained until expiry or explicit deletion | failed write is not indexed; cache switches to memory | delete item/clear all |
The storage screen reports approximate usage; browser quota figures remain
estimates and are not a durability promise.