Commit graph

2 commits

Author SHA1 Message Date
mithral
fd377a7446 Offer transcoding as a resolve engine, as the last resort
Fourth and final step of ADR-008's server half. When nothing the client
declared can direct-play a file and this host can re-encode, resolve now
answers playable with engine "transcode". The client sees a string it already
understands, so the frozen JSON contract did not change.

The ordering is the whole point and is asserted directly: transcoding sits
after the direct-play loop, so a file that plays directly never pays for it.
Re-encoding something a client could already decode would burn the GPU and
lose quality for nothing, which is the failure mode ADR-008 rejects by
choosing direct play first. Verified red-green - hoisting the check above the
loop fails with "direct play must always win".

The capability is resolved once at boot and passed in, not read inside a
request: detection spawns ffmpeg to prove the encoder works, and that must
never happen on the request path. It is also passed rather than read from the
transcode package so the tests can drive both answers without hardware.

TranscodeID is deliberately not a registry entry. Every other engine is
something a client declares; this one is the server's own capability, so it
is not advertised in /playback/engines and a client must not name it.

On a host with no usable encoder canTranscode is false and every answer is
byte-identical to what it was before any of this existed - which is this
deployment, and is why ADR-002 still holds for it.
2026-10-12 12:03:41 -04:00
mithral
67f145a9b1 Waves 7-8: playback engine selection, and missing files you can see
Wave 7 - engines, chosen per file by the client's own capabilities
- internal/engine: a registry. html5 is built in and declares a CONSERVATIVE
  browser baseline (h264/vp8/vp9/av1, deliberately no hevc and no mpeg4, because
  that is what was measured). mpv, vlc and mdk are registered as native engines
  declaring the broad support those libraries really have.
- The server never declares a file playable. It reports what is in the file and
  matches it against the engines the CLIENT declares. Proven:
      engines=["html5"]       -> playable=false engine=null reason=vcodec:hevc
      engines=["html5","mpv"] -> playable=true  engine=mpv
  Same file, different answer, because the client offered a different engine.
  That is what makes adding a native client a registration rather than a rewrite.
- internal/playback: GET /playback/engines and POST /playback/resolve, behind
  auth and the same grant chokepoint. An unreadable item is 404, not 403.
- web/src/lib/capabilities.ts: uses navigator.mediaCapabilities.decodingInfo(),
  which returns {supported, smooth, powerEfficient}. That is real client-hardware
  detection and it distinguishes "cannot decode" from "can decode but it will
  stutter and drain the battery". Feature-detected; degrades to the server
  baseline rather than throwing.
- VideoPlayer consults the server as an ADVISORY only. The element remains the
  source of truth: the server's note appears first, then the element's own
  confirmation replaces it once videoWidth proves the answer. Verified on HEVC —
  "Playback may not work here" at 0-906ms, "Video cannot play here" from ~1208ms.

Wave 8 - missing files: visibility and an explicit purge
- migrations/0011 adds missing_since to media_items and media_files.
- It is written only on the TRANSITION into missing, so the age says when the
  file first vanished rather than the last rescan. Proven byte-identical across a
  rescan, and cleared when a file is restored.
- GET /api/v1/missing (grant-scoped) and POST /api/v1/libraries/{id}/purge
  (admin only). The purge deletes only items that are missing AND have no
  present file, and its cascade into progress/item_people/item_views/game_saves
  is enumerated in a comment — that cascade is exactly why deletion is never
  automatic. Proven: 19 items -> 18, only the missing one, the non-missing item
  untouched and its files intact, second purge returns 0.
- MissingFiles.svelte in the Manage nav: title, kind, library, path, human age,
  and a per-library Purge behind a confirmation that names the count and says
  progress cannot be regenerated.

Coordinator wired playback.NewHandlers into main.go (the one line agents were
told to leave alone).

Verified end to end: 17 packages pass, gofmt/vet clean, svelte-check 0/0, and
every claim above checked against the running server.
2026-10-11 14:36:55 -04:00