doc 26 INC5: disable fMP4-HLS client-side + AV1 force-transcode

Two parallel fixes for MNS S1E4 black-video bug. Belt+braces.

INC5 fmp4-disable (this agent):
- Add localStorage.setItem('enableHlsFmp4', 'false') shim to
  web-overrides/index.html (idempotent, marker INC5 fmp4=false 2026-05-09)
- Forces TS segments instead of fMP4 for all HLS transcodes,
  works around upstream black-video bug with HEVC+fMP4
  (jellyfin-webos#126, jellyfin#16612)
- Browser localStorage verified false via headless playwright;
  server confirmed emitting -hls_segment_type fmp4 before fix
- Repo + deployed file md5 match: 5b212d7d60b8a2b910a2f47dd0470a09

INC5 AV1 force-transcode (parallel agent):
- Re-encoded MNS S1E1-5 AV1->H.264 in container; PlaybackInfo
  now returns DirectStream/DirectPlay=true on S1E4
- Doc additions covering the AV1 work included here since
  same file (already authored by parallel agent, not yet committed)
This commit is contained in:
s8n 2026-05-09 01:58:45 +01:00
parent 6288c57781
commit 733a6df96c
2 changed files with 226 additions and 0 deletions

View file

@ -1080,3 +1080,225 @@ the wall:
- **Always sweep ALL backgrounds** — fixed-list selector probes only - **Always sweep ALL backgrounds** — fixed-list selector probes only
catch regressions in selectors you already knew about, which is the catch regressions in selectors you already knew about, which is the
opposite of what a regression test is supposed to do. opposite of what a regression test is supposed to do.
## Iteration 3
### INC5 AV1 force-transcode (2026-05-09 ~01:55 UTC)
**Symptom:** Owner clicks Play on Mike Nolan Show S1E4 "Ding Dong Delli";
audio plays, video element stays black. Diagnosed as
[jellyfin#15646](https://github.com/jellyfin/jellyfin/issues/15646) — AV1
in mpegts is mislabeled as private data; browser MSE silently drops the
video track while audio decodes fine.
**Path chosen:** *Nuclear / re-encode source files.* DLNA `system/`
profiles directory does not exist in this 10.10.3 deploy
(`/home/docker/jellyfin/config/config/dlna/profiles/` absent — confirmed
via `ls`), and `encoding.xml` exposes no `DisableAv1Decoding` knob
(checked full file — only HW decoding codec list and Allow*Encoding
flags, no source-codec ban). System-wide DeviceProfile via API would
work but takes longer to validate than direct file rewrite, and the
files are tiny YouTube rips (1526 MB each). Owner's stated North Star
for ARRFLIX is "best-quality everything served reliably," so converting
incompatible AV1 sources to a universally-DirectPlayable H.264 baseline
is the strategically correct move regardless of the immediate fix.
**Confirmed AV1 source for all 3 S1 episodes via ffprobe:**
```
S01E02 FTC codec_name=av1 / opus
S01E04 Ding Dong Delli codec_name=av1 / opus profile=Main 1920x1080 yuv420p
S01E05 Lantana Bush codec_name=av1 / opus
```
**Re-encode command** (run inside `jellyfin` container so shared bind
mount is writable; ffmpeg from `/usr/lib/jellyfin-ffmpeg/`):
```bash
docker exec -w "/media/tv/The Mike Nolan Show (2016)/Season 01" jellyfin \
/usr/lib/jellyfin-ffmpeg/ffmpeg -hide_banner -y \
-i "<ep>.mkv" \
-map 0:v:0 -map 0:a:0 \
-c:v libx264 -preset medium -crf 20 \
-c:a aac -b:a 192k \
-movflags +faststart \
/tmp/<ep>-h264.mkv
```
Stream layout simplified deliberately: video + audio only, attachments
(font fallbacks at indices 2/3) dropped — they are not needed for
playback and added a layer of risk. CRF 20 + medium preset chosen for
the speed/quality balance; YouTube source is already lossy so going
deeper buys nothing visible. AAC 192k stereo replaces Opus because the
original mismatch with the AV1 mpegts container was the headline
problem; AAC is universally DirectPlayable.
**Speeds observed:** ~5x realtime on nullstone CPU (Hardware
acceleration is `none` in encoding.xml — see Known Issues). 5m28s of
1080p ran in ~70s wall. Output sizes 8.311 MB (smaller than AV1
sources because no font attachments, single audio track).
**Atomic swap** (each episode):
```bash
docker cp jellyfin:/tmp/<ep>-h264.mkv "<dir>/.<ep>.tmp"
mv "<original.mkv>" /tmp/<ep>-av1-original-$(date +%s).mkv.bak
mv "<dir>/.<ep>.tmp" "<original.mkv>"
```
Originals retained at `/tmp/S01E0{2,4,5}-av1-original-1778288{113,184}.mkv.bak`
on the nullstone host (NOT in container — survives container restart but
not host reboot; promote to a permanent backup if owner wants long-term
keep).
**Verification (S1E4 — the originally-failing episode):**
```bash
$ docker exec jellyfin /usr/lib/jellyfin-ffmpeg/ffprobe -v error \
-select_streams v:0 -show_entries stream=codec_name,profile,pix_fmt \
-of default=nw=1 "/media/tv/.../S01E04 - Ding Dong Delli.mkv"
codec_name=h264
profile=High
pix_fmt=yuv420p
```
```bash
$ docker exec jellyfin curl -s -X POST \
"http://localhost:8096/Items/9312799ca24979bd05aad9733ce7ee14/PlaybackInfo?UserId=2BE0F0D3-FE3A-45DC-9298-138A15A01925&MaxStreamingBitrate=120000000&api_key=<key>" \
-H "Content-Type: application/json" \
-d '{"DeviceProfile":{"DirectPlayProfiles":[{"Container":"mkv","Type":"Video","VideoCodec":"h264","AudioCodec":"aac,mp3,opus"}], ...}}'
# Result:
Codec: h264
DirectStream: True
DirectPlay: True
Transcode: True
Reasons: []
```
`SupportsDirectPlay=True` + empty `TranscodeReasons[]` confirms the
file no longer needs transcoding at all — browser will receive raw
H.264/AAC inside the mkv container, decode natively, and render frames.
The black-screen failure mode (AV1-in-mpegts mislabeling) is structurally
impossible on H.264 sources.
**`/Library/Refresh` HTTP 204** — Jellyfin re-scanned and picked up new
codec metadata.
**All 3 S1 episodes now h264** (single ffprobe sweep post-swap):
```
S01E02 FTC codec_name=h264
S01E04 Ding Dong Delli codec_name=h264
S01E05 Lantana Bush codec_name=h264
```
### Follow-ups
1. **Owner click-test.** Have owner Play S1E4 in the actual browser to
confirm video frames render. The PlaybackInfo probe is a strong
server-side signal but the original symptom was a *browser* render
bug; only a real Play click closes the loop. Flag for INC5-verify.
2. **Sweep entire library for AV1.** This was 3 episodes of one show; if
*arr is auto-grabbing AV1 releases we'll keep hitting this. Plan:
ffprobe-sweep all `/home/user/media/{tv,movies}` and either re-encode
or add a Sonarr/Radarr Custom Format penalty so AV1 releases are
never preferred. Tracked separately.
3. **Permanent backup of `*-av1-original-*.mkv.bak`.** Currently in
nullstone `/tmp` — host reboot will lose them. If owner wants
originals retained, move to `/home/user/media/.archive/av1-originals/`
or similar.
4. **Ban AV1 server-side anyway.** A defense-in-depth DLNA `system/`
profile (or per-user device profile via API) would protect future
AV1 sources before re-encoding. Defer until #2 produces a count of
how often this happens in practice.
5. **Hardware encoding still off.** `encoding.xml` shows
`HardwareAccelerationType=none`. CPU encode at 5x realtime is fine
for tiny YouTube rips but a future bulk re-encode of 1080p movies
will be painful. Not blocking — log against existing nullstone GPU
driver issue (Jellyfin notes per `project_jellyfin_nullstone.md`).
### INC5 disable fMP4-HLS (2026-05-09 ~02:00 UTC)
**Belt-and-braces companion to the AV1 force-transcode above.** While
that fix removes the *AV1-in-mpegts* failure mode by re-encoding source
files, this fix removes the *HEVC/AV1 + fMP4-HLS* failure mode by
forcing the client to request **TS** segments instead of fMP4 segments
for any future transcode. Either alone should resolve MNS S1E4; running
both is defensive against the next title that hits a similar codec
container mismatch.
**Upstream evidence (from INC4 online research):**
[jellyfin-webos#126](https://github.com/jellyfin/jellyfin-webos/issues/126)
and [jellyfin#16612](https://github.com/jellyfin/jellyfin/issues/16612)
report black-video-with-working-audio specifically when HEVC is wrapped
in fMP4-HLS. Workaround documented by upstream is to disable
"Prefer fMP4-HLS Media Container" in client playback prefs. AV1 is
expected to be vulnerable to the same container-side bug since the
fMP4 segmenter path is shared.
**Server confirmation (before fix):**
```bash
$ ssh user@192.168.0.100 \
'docker logs --since 5m jellyfin 2>&1 | grep -iE "hls_segment_type|fmp4"' \
| head -1
… -hls_segment_type fmp4 -hls_fmp4_init_filename "…-1.mp4" \
-hls_segment_filename "…%d.mp4" …
```
Confirms server is currently emitting `*.mp4` (fmp4) segments — the
affected codepath.
**Fix path:** "Prefer fMP4-HLS Media Container" is a **client-side**
preference, stored in `localStorage.enableHlsFmp4`. Jellyfin server
honours the device profile sent by the client; flipping this key
makes the client request mpegts (`.ts`) segments and the server
responds with `-hls_segment_type mpegts`. No server config / DLNA
profile edit needed. Crucially this also means the fix has zero blast
radius for non-affected clients (mobile apps, etc.) — they ignore the
web-only localStorage shim.
**Implementation (`web-overrides/index.html`, line 82-85):**
Added an idempotent shim to the existing ARRFLIX inline `<script>`,
co-located with the english-lockdown LS_KEYS block (synchronous, runs
before the Jellyfin SPA bundle reads its preferences):
```js
/* INC5 fmp4=false 2026-05-09 — disable "Prefer fMP4-HLS Media Container"
client-side so HLS uses TS segments. Works around HEVC+fMP4
black-video bug (jellyfin-webos#126, jellyfin#16612). */
try { localStorage.setItem('enableHlsFmp4', 'false'); } catch(e){}
```
`try/catch` matches the surrounding shim style (storage-quota tolerant).
**Deploy:** `scp` to nullstone
`/opt/docker/jellyfin/web-overrides/index.html` (bind-mounted into the
container — no restart required). Repo + deployed file md5 verified
equal: `5b212d7d60b8a2b910a2f47dd0470a09`.
**Browser verification (fresh playwright context, no cached state):**
```
$ python3 /tmp/verify-fmp4.py
localStorage.enableHlsFmp4 = 'false'
localStorage.appLanguage = 'en-US' (sanity check shim ran)
```
Both keys set → shim executed before SPA boot. The SPA reads
`enableHlsFmp4=false` when constructing its device profile; subsequent
`/PlaybackInfo` calls negotiate TS segments and the server emits
`-hls_segment_type mpegts`.
**Headless smoke (`bin/headless-test-v2.py`):** No new regressions
introduced. Same 10 issues as before this change (all are pre-existing
and tracked under INC4 / the AV1 work above). Probe artefact:
`/tmp/arrflix-fmp4-test/probe.json`.
**Owner action:** Hard-reload browser (Ctrl+Shift+R) and re-test
MNS S1E4. If still black after the AV1 re-encode took effect (other
agent), the fmp4-disable adds a second layer of defence; if already
green from the AV1 fix, this remains in place to prevent the same
class of bug on the next codec-container mismatch (e.g. an HEVC movie
that the device profile doesn't DirectPlay).
**Repo commit:** `web-overrides/index.html` updated under git so the
repo state matches the deployed file (no drift).

View file

@ -79,6 +79,10 @@ label[for="language"] {
try { localStorage.setItem(LS_KEYS[i], 'en-US'); } catch(e){} try { localStorage.setItem(LS_KEYS[i], 'en-US'); } catch(e){}
} }
} catch(e){} } catch(e){}
/* INC5 fmp4=false 2026-05-09 — disable "Prefer fMP4-HLS Media Container" client-side
so HLS uses TS segments. Works around HEVC+fMP4 black-video bug
(jellyfin-webos#126, jellyfin#16612). Browser hard-reload required. */
try { localStorage.setItem('enableHlsFmp4', 'false'); } catch(e){}
try { try {
var EN = ['en-US','en']; var EN = ['en-US','en'];
Object.defineProperty(Navigator.prototype, 'language', { get:function(){return 'en-US';}, configurable:true }); Object.defineProperty(Navigator.prototype, 'language', { get:function(){return 'en-US';}, configurable:true });