Commit Graph
14 Commits
Author SHA1 Message Date
JakeBreath e97c3b9da0 Toast action results and confirm destructive actions in one dialog
Inline banners and per-row status text reported action results all over
the app; they are replaced by a small toast stack (bottom-right, Level 3
floating well styling) that only speaks for actions: successes fade,
errors stay until dismissed, and form-field validation stays inline.

Destructive actions no longer use bespoke inline confirm steps (the
library detail's Confirm delete button) or fire immediately (duplicate
copies/items, delete page selections, temp cleanup, upload discard, job
cancellation): they all go through one promise-based confirm dialog
(confirmAction) with a danger variant, Escape/backdrop to cancel.
2026-09-17 22:19:54 -05:00
JakeBreath a152063d76 Keep animated images animated in the optimizer
- Animated WebP (J-82) was being flattened to its first frame: browsers
  have no animated WebP encoder, so the modal now sniffs the file header
  (4 KB range request), explains the limitation and disables Process
  instead of overwriting the file with a single frame.
- APNGs saved as .png took the still-image path and lost their frames;
  the header sniff looks for the acTL chunk, routes them to the animation
  pipeline (all frames + delays) and switches the UI to the animation
  options. The worker double-checks the header too, so no path can
  flatten an APNG.
- New dependency-free imageformat module, verified against real files
  (J-82 animated, static WebP/PNG, and a generated APNG named .png).
2026-09-17 21:57:02 -05:00
JakeBreath 0fcc4e518a Widen the optimize modal so the encoder diagnostics fit on one line 2026-09-17 21:28:17 -05:00
JakeBreath 8ae8080426 Name the browser in the encoder diagnostics and offer a container switch
The optimizer now shows which browser is running (e.g. "Firefox 141")
next to the encoder probe results, explains the common Firefox case
("Firefox does not implement H.264/HEVC encoding"), and offers a
one-click switch to the container that has working encoders.
2026-09-17 21:13:49 -05:00
JakeBreath 01b9a6181a Probe real encoder output, order codecs by it, and add a quality slider
Your machine's H.264/HEVC encoders report support and then emit no video
samples at all, which no configure-time check can see. The optimizer now
test-encodes three frames per codec (cached for 30 days), shows the result
as diagnostics in the modal (AVC x / VP9 (hardware) / ...), feeds the
working codecs to the worker so Auto tries them first, and tells you to
switch to WebM when a container has no usable encoder.

Video quality is now a 10-100 slider with a predicted bitrate and size
for this clip (mirroring Mediabunny's mapping), which makes the trade-off
visible instead of guessing from presets.
2026-09-17 21:11:46 -05:00
JakeBreath 33ae790bd0 Detect a missing video track and retry with another encoder
Your symptom (audio kept, video 0x0) means the hardware H.264 encode
silently produced no video samples while the audio was copied. The result
check now parses the moov box and requires a 'vide' handler, so an
audio-only output is treated as a failed attempt: the pipeline walks all
codec x hardware/software configurations (starting with hardware when
enabled) and only reports an error when every one fails, naming what each
attempt returned. Resizing is also skipped entirely unless a smaller
height was requested, keeping the scaler out of the pipeline.
2026-09-17 21:04:58 -05:00
JakeBreath e7657a3dd0 Fix the OPFS output lifecycle (locked stream on close)
StreamTarget closes the underlying writer when the output is finalized —
that is also what commits an OPFS file — so closing it ourselves threw
"Can not close locked stream". finalize() now simply reads the committed
file back, and every failure path (invalid attempt, encode error,
validation failure) aborts the write and deletes the temporary entry.
2026-09-17 21:01:15 -05:00
JakeBreath bd812bb601 Write video output to a real OPFS file instead of assembling chunks
The hand-rolled chunk assembly was verified correct in Node but still
produced a broken MP4 in the browser, so stop relying on it: the muxer now
streams into an Origin Private File System file (random access is exactly
what MP4 needs), the worker returns the OPFS File directly, and the entry
is deleted after a successful apply (stale ones are purged hourly). The
chunk collector remains only as a fallback for browsers without OPFS.

The result is validated before it reaches the UI: MP4s must contain a
moov box and WebM files must start with the EBML magic, so a broken muxer
output surfaces as an error instead of a 0x0 preview.
2026-09-17 20:58:26 -05:00
JakeBreath 9162cf23ce Fix the streaming assembler duplicating overlapped bytes
When the muxer patched a byte range inside an already-written chunk (the
mdat header, for example), the merge trimmed the right side of the old
segment but left its full blob on the left, duplicating megabytes and
shifting every box offset — the MP4 still reported its duration but had
no usable video track (0x0 in the browser, "moov atom not found" in
ffprobe).

The collector/assembler now lives in its own module and was verified in
Node with a real transmux of J-81: the assembled file matches the source
(h264 1280x720, 500 frames, AAC, 20.84 s) byte for byte in structure.
2026-09-17 20:50:08 -05:00
JakeBreath 038773e79e Use bitrate-based quality presets for video encoding
Qualitative presets made Mediabunny use quantizer (CRF-like) encoding, but
hardware H.264 encoders commonly ignore the per-frame quantizer and fall
back to a very low default bitrate, producing files far smaller than the
preset implies. Passing preferBitrate makes the preset map to an explicit
bitrate so hardware and software paths agree.
2026-09-17 20:45:47 -05:00
JakeBreath fed1e8bb5a Fix chunk assembly order and surface video duration in the optimizer
- The streaming assembler applied chunks sorted by position, so header
  patches written last could be overwritten by earlier data. Chunks now
  apply in arrival order (newest write wins per byte range) and are only
  laid out by position afterwards.
- The Optimize modal now reads the media metadata itself: both previews
  show resolution and duration, and the processed video is flagged in red
  when it comes out shorter than the original (a truncated encode is no
  longer something you have to guess by file size).
2026-09-17 20:45:12 -05:00
JakeBreath 58f030878e Fix duplicate React keys and the MP4 encoder probe
- MatchCard and IqdbCard are siblings in the library detail aside and
  both used key={item.j_id}, so React warned about duplicate children
  (J-81 twice). Their keys are now unique per card.
- The video pipeline no longer decides "this browser can't encode" from
  a single getFirstEncodableVideoCodec probe with source dimensions.
  It now probes Conversion.init with the real (even) output size across
  codec candidates and hardware preferences, uses the first valid
  configuration, and reports exactly what was tried when nothing works.
  It also fails fast with a clear message if VideoEncoder is missing in
  the worker.
2026-09-17 20:41:16 -05:00
JakeBreath f7e2242e5f Fix the client optimizer: PNG init, GIF compositing, streaming video
- PNG failed because wasm-bindgen's init only accepts a URL string (or a
  module/buffer): passing { module_or_path } broke every PNG optimization.
- GIF/APNG showed flashing colors because patches were written with
  putImageData, which ignores the transparency that means "keep the
  previous frame". Patches now blend through drawImage with correct
  disposal handling, and quantization keeps a transparent palette entry.
- Video no longer downloads the whole file into memory: Mediabunny reads
  it with range requests (UrlSource) and the muxer streams into Blob
  chunks (StreamTarget) that are assembled with last-write-wins range
  merging. BufferTarget held the entire output in memory, which crashed
  the tab on large files.
2026-09-17 20:10:31 -05:00
JakeBreath bab9904fc8 Client-side optimization pipeline with apply-to-J-ID
Backend:
- POST /api/files/J-x/optimize/ applies a browser-processed file: replaces
  every copy (renaming when the extension changes), recomputes MD5, size
  and perceptual hashes, seeds guest visibility; 400 when identical,
  409 when the result matches another item, owner/staff only.

Frontend (no server-side processing by design):
- optimize.worker.ts + pipelines: Mediabunny/WebCodecs for video with a
  prefer-hardware hint and per-browser codec detection; MozJPEG/OxiPNG/
  libwebp (jSquash) for images; gifuct-js+gifenc and UPNG for GIF/APNG.
- OptimizeModal: per-file-type options, original vs processed previews
  with sizes/savings, progress bar with ETA, then Apply (overwrite).
- Optimize button on the library detail for the uploader/staff.
2026-09-17 20:03:22 -05:00