ARRFLIX/docs/21-rick-and-morty-color-audit.md

21 KiB
Raw Permalink Blame History

21 — Rick and Morty Color / HDR Audit (Read-Only)

Status: read-only audit, executed 2026-05-08 against https://arrflix.s8n.ru (Jellyfin 10.10.3 on nullstone). Scope: diagnose why Rick and Morty looks "kind of gray / washed-out" while other titles render normally. No fixes applied. No state mutated. No transcode triggered.

Inputs: ffprobe via docker exec jellyfin /usr/lib/jellyfin-ffmpeg/ffprobe against on-disk media; Jellyfin REST /Items/{id}/PlaybackInfo, /System/Configuration/encoding, /Branding/Configuration (auth X-Emby-Token: ${JELLYFIN_API_TOKEN}); contrast probe against The Mandalorian as a known-good SDR title; review of CustomCss against the inventory in doc 14 §1b.


1. Executive summary

Confirmed root cause: the Rick and Morty release on disk is an HDR10 4K HEVC Main 10 (PQ / BT.2020) "AI Upscale" of an originally SDR animated show. Jellyfin classifies it as VideoRange=HDR VideoRangeType=HDR10 and forces the browser onto the transcode path (TranscodeReasons=ContainerNotSupported, AudioCodecNotSupported, SubtitleCodecNotSupported — every browser session triggers this). The encoding config has EnableTonemapping=false and HardwareAccelerationType=none, so ffmpeg software-decodes the HDR10 source, then h264-encodes without applying a tonemap, then the browser interprets the resulting BT.2020 PQ pixel data as plain BT.709 SDR. That mis-interpretation is the textbook signature of the washed-out grey look.

One-line remediation (lowest blast radius): in /System/Configuration/encoding, set EnableTonemapping=true (the algorithm bt2390 is already correctly selected) — this enables CPU tonemap on the existing software pipeline; CSS, hardware, and source files do not need to change.

CSS / theme is ruled out as a cause — CustomCss contains zero grayscale(, zero saturate(, zero hue-rotate( filters.


2. ffprobe table — Rick and Morty (Season 01)

All probes via docker exec jellyfin /usr/lib/jellyfin-ffmpeg/ffprobe -v error -select_streams v:0 ….

File Codec Profile Pix fmt color_space color_transfer color_primaries range W×H Bitrate Size HDR side-data
S01E01 — Pilot hevc Main 10 yuv420p10le bt2020nc smpte2084 (PQ) bt2020 pc 3840×2160 8.13 Mbit/s 1.34 GB none (no MasteringDisplay / CLL block)
S01E05 — Meeseeks and Destroy hevc Main 10 yuv420p10le bt2020nc smpte2084 bt2020 pc 3840×2160 7.97 Mbit/s 1.26 GB not present
S01E08 — Rixty Minutes hevc Main 10 yuv420p10le bt2020nc smpte2084 (BT.2020) (pc) 3840×2160 n/a 1.34 GB not present
S01E11 — Ricksy Business hevc Main 10 yuv420p10le bt2020nc smpte2084 bt2020 pc 3840×2160 8.86 Mbit/s 1.49 GB not present

Reading:

  • color_transfer=smpte2084 (a.k.a. ST 2084 / PQ) is the HDR10 transfer function. All R&M S01 episodes ship with HDR10 tagging.
  • color_primaries=bt2020 + color_space=bt2020nc are the BT.2020 wide-gamut primaries (the HDR colour space).
  • pix_fmt=yuv420p10le = 10-bit-per-component, 4:2:0 chroma sub- sampling. Required for HDR10 content.
  • color_range=pc = full-range (01023 for 10-bit) rather than the TV-range (64940) usually expected. This is unusual — most HDR10 Blu-ray / streaming sources are TV-range. PC-range mis-interpreted as TV-range is itself a contrast/saturation hit, layered on top of the HDR-as-SDR hit.
  • No HDR side-data (MasteringDisplayMetadata, ContentLightLevelMetadata) is present in any episode — the source declares HDR10 but ships without the static-metadata blocks that a proper HDR display or tonemapper would consume. This is a fingerprint of a fake HDR10 AI upscale (the file's own embedded title is "Rick and Morty - S01E01 - Pilot - 2160p HDR Ai Upscale -Mesc").
  • 4K x 24 fps x ~8 Mbit/s × 1320 s = file size matches container declaration, no surprises in muxing.
  • The poster art / show landing page itself is rendered by the SPA from JF's image cache (PNG / JPEG, sRGB) — those are not affected by HDR. Only the video element is washed-out.

2a. Comparison vs. The Mandalorian (known-good SDR)

File Codec Profile Pix fmt color_space color_transfer W×H Bitrate
Mandalorian S01E01 hevc Main 10 yuv420p10le bt709 bt709 1920×804 6.69 Mbit/s
Mandalorian S02E01 hevc Main 10 yuv420p10le bt709 bt709 (1920×…) n/a
Mandalorian S03E01 hevc Main 10 yuv420p10le bt709 bt709 1920×804 6.72 Mbit/s

Reading: Mandalorian is plain SDR BT.709 (the same colour space the browser's <video> defaults to assume). 10-bit pixels here are fine because the transfer is BT.709 SDR, not PQ — the browser / ffmpeg pipeline sees this and renders it correctly. This is the control sample that proves the difference is content-side, not config-side.


3. Jellyfin encoding config — relevant fields

Source: GET /System/Configuration/encoding.

Field Value Comment
HardwareAccelerationType "none" GPU is dead (host has no nvidia driver — see doc 13 finding 02). Every transcode is software ffmpeg.
EnableHardwareEncoding true No-op while HardwareAccelerationType=none.
EnableTonemapping false THE BUG. Software-tonemap is disabled. With HDR source + =none HW + tonemap off, the output is HDR pixels with no SDR conversion.
EnableVppTonemapping false Intel-VPP path, not relevant for CPU.
EnableVideoToolboxTonemapping false macOS path, not relevant.
TonemappingAlgorithm "bt2390" Good choice when enabled — the BT.2390 EETF is the modern recommendation. (hable is the legacy fallback; mobius and reinhard are alternatives.)
TonemappingMode "auto" Fine.
TonemappingRange "auto" Fine.
TonemappingDesat 0 Default.
TonemappingPeak 100 Target SDR peak nits — default.
TonemappingParam 0 Algorithm-specific; 0 = default.
EnableDecodingColorDepth10Hevc true 10-bit HEVC decode permitted.
H264Crf 23 h264 quality target for transcode output (default for JF).
H265Crf 28 h265 quality target (unused — AllowHevcEncoding=false).
AllowHevcEncoding false Cannot transcode-out as HEVC (forces h264 output).
AllowAv1Encoding false Cannot transcode-out as AV1.
EnableThrottling false Per doc 13 finding 03 — separate issue.
EnableSegmentDeletion false Per doc 13 finding 05 — separate issue.
MaxMuxingQueueSize 2048 Per doc 13 — separate issue.
EncoderAppPathDisplay /usr/lib/jellyfin-ffmpeg/ffmpeg Bundled jellyfin-ffmpeg, not host.
VaapiDevice /dev/dri/renderD128 Empty on host (no Intel iGPU on AMD Ryzen).
EncodingThreadCount -1 Auto = all cores.

Net: the one knob standing between "washed-out grey" and "correctly tonemapped SDR" is EnableTonemapping. The algorithm is already set correctly (bt2390). Flipping the bool to true is a single POST-able field-edit and applies to every future transcode.

3a. Live PlaybackInfo for R&M S01E01 (browser DeviceProfile)

Simulated browser PlaybackInfo (DeviceProfile: Chrome, h264 / aac / mp3 / ac3 / eac3, hls):

SupportsDirectPlay:    false
SupportsDirectStream:  false
SupportsTranscoding:   true
TranscodingSubProtocol: hls
TranscodingUrl:
  /videos/<id>/master.m3u8
    ?VideoCodec=h264
    &AudioCodec=aac,mp3,ac3,eac3
    &VideoBitrate=139616000
    &SegmentContainer=ts
    &hevc-level=150
    &hevc-videobitdepth=10
    &hevc-profile=main10
    &TranscodeReasons=
        ContainerNotSupported,
        AudioCodecNotSupported,
        SubtitleCodecNotSupported

Reading: every browser session for R&M is forced into transcode by three independent reasons (container mkv, audio truehd / ac3, subtitle pgs / ass — confirmed by MediaInfo). It's not just an HDR issue — the file cannot direct-play in any browser, so the transcode path is mandatory, and inside that path tonemap is currently off.

For comparison, an SDR Mandalorian episode would still hit the transcode path for the same container/audio reasons, but the tonemap-off flag wouldn't matter because the source is already BT.709.


4. Theme / CSS rule-out check

Inspected /Branding/Configuration → CustomCss (25 225 chars, full inventory in doc 14 §1b). Searched the live string for any filter / saturation / hue-rotate / opacity rule that could desaturate the video element or its container.

Filter pattern Matches in CustomCss Verdict
grayscale( 0
saturate( 0
hue-rotate( 0
sepia( 0
brightness( 0
contrast( 0
invert( 0
mix-blend-mode 0
filter: 0
backdrop-filter: 0
opacity: (on .itemBackdrop / video / .osdContainer) 0

Also checked the doc 14 §7 detail-page backdrop rules just landed (linear-gradient(90deg, rgba(0,0,0,0.95) 0%, …)) — that gradient is applied to .layout-desktop .backgroundContainer.withBackdrop, NOT to the <video> element. It tints the backdrop poster behind the detail-page header, not playback. Not the cause.

web-overrides/index.html (the bind-mounted critical-path style): no filter:, no mix-blend-mode, no animation that would alter video. ARRFLIX-SHIM JavaScript only touches document.title, favicon, and mypreferencesmenu drawer entries — does not touch playback DOM.

Theme / CSS rule-out: PASS. The greyness is in the pixel data delivered to the browser, not in any post-render CSS effect.


5. Source-file integrity rule-outs

Already visible in §2, but stated explicitly so each candidate root cause is closed:

Hypothesis Evidence Verdict
(a) HDR file + CPU tone-map All R&M S01 = HDR10 (smpte2084/bt2020). Encoding config EnableTonemapping=false, HardwareAccelerationType=none. CONFIRMED root cause.
(b) CSS filter on theme §4 shows zero filter/saturation rules. RULED OUT.
(c) Direct-play tag mismatch PlaybackInfo §3a shows SupportsDirectPlay=false — browser is on transcode path, no chance of DP-tag confusion. RULED OUT.
(d) Source is genuinely SDR but graded flat (wrong tags) ffprobe reports HDR10 tags consistently across 4 episodes, and Jellyfin agrees (VideoRangeType=HDR10). Title-string "2160p HDR Ai Upscale" confirms intent. RULED OUT — the source IS HDR10, just badly so.
(e) Container / bit-depth / browser HW-decode bit-crush Browser never receives the 10-bit HEVC because transcode is mandatory; output is 8-bit h264. So no client-side bit-depth issue is possible. RULED OUT.
(f) Missing Mastering Display / CLL metadata makes tonemap target unknown True — files have no static HDR metadata. Once tonemap is enabled, ffmpeg will fall back to defaults (peak 1000 nits, etc.) which is fine for cartoon AI-upscale content; better than no tonemap. NOT a blocker for the fix.
(g) color_range=pc (full-range) Full-range PC pixels reinterpreted as TV-range = an additional contrast bump. Tonemap filter handles range conversion. Subsumed by (a) — same fix.

6. Concrete remediation list (ranked: effort vs blast-radius)

Action: flip a single bool in encoding config.

PUT /System/Configuration/encoding
  EnableTonemapping = true
  (TonemappingAlgorithm already = "bt2390" — leave as-is)
  (TonemappingPeak  already = 100        — leave as-is)
  (TonemappingMode  already = "auto"     — leave as-is)
  (TonemappingRange already = "auto"     — leave as-is)
  (TonemappingDesat already = 0          — leave as-is)

(Or via UI: Dashboard → Playback → Transcoding → "Enable tone-mapping".)

Effect: every future HDR-source transcode applies BT.2390 EETF + gamut conversion (BT.2020 → BT.709) before h264 encoding. Output looks right in any SDR browser.

Cost: zero seek time, no restart needed.

Blast radius: low. Only HDR sources (currently: Rick and Morty S01) are affected. SDR sources (Mandalorian etc.) already have BT.709 tags so the tonemap filter is a no-op for them.

Caveat: software tonemap on a 4K HEVC source on the existing host load (doc 13 finding 01: load 11.4, swap 6.8 GiB) will add ~1.52× extra CPU per stream compared to a tonemap-off transcode. Pair this with doc 13 finding 03 (EnableThrottling=true) so a client-cancelled stream stops burning CPU; otherwise a stalled R&M playback will eat a core for 12 minutes (SegmentKeepSeconds=720).

Risk of "looks worse than expected": AI-upscale R&M has no real HDR — the wide-gamut tonemap will give a result that is more saturated than the original Adult Swim broadcast (cartoon flat colours pushed through BT.2020 round-trip), but visibly correct relative to current washed-out grey. If the operator wants the original cartoon look, remediation #3 below.

#2 — Pair tonemap-on with throttling-on (doc 13 finding 03)

Action: when applying #1, also set:

EnableThrottling      = true
EnableSegmentDeletion = true

Effect: caps wasted ffmpeg CPU after a client disconnects — already recommended in doc 13 audit, doubly important once we add tonemap overhead.

Cost: zero additional. Same UI page as #1.

#3 — Replace R&M with a properly-graded SDR release (highest fidelity, highest effort)

Action: swap the Rick.and.Morty.S01...2160p.HDR.Ai.Upscale-Mesc files for a native SDR encode (e.g., the original Adult Swim 1080p / WEB-DL releases or the 2160p SDR remasters where they exist).

Effect: zero tonemap cost (source is already BT.709), faster transcodes, files shrink ~3-4× (8 Mbit/s 4K HDR → ~2 Mbit/s 1080p SDR for a 22-min cartoon is plenty), consistent look with rest of library.

Cost: medium — re-acquisition + re-import + re-scan + 90 GB disk freed on /home which is currently 90% full (doc 13 finding 01).

Blast radius: medium. Watched-state and metadata stay (Sonarr will re-match by (2013) + episode index), but each episode item ID in JF will change → existing playback positions on R&M are lost.

#4 — Pre-transcode R&M S01 to SDR offline (middle-ground)

Action: run ffmpeg once (outside Jellyfin) with the same tonemap pipeline, write SDR-tagged HEVC files alongside, swap them in.

# Per episode (CPU intensive, ~1 hr per 22-min episode on this host):
ffmpeg -i in.mkv \
  -map 0:v:0 -map 0:a -map 0:s? \
  -vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=bt2390:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p10le" \
  -c:v libx265 -preset slow -crf 22 -profile:v main10 \
  -c:a copy -c:s copy out.mkv

Effect: Jellyfin no longer needs to tonemap on every play — files are SDR-tagged at rest. CPU at playback drops to a normal HEVC-software-decode-then-h264-software-encode (still no GPU but no extra tonemap stage).

Cost: ~11 hours wall-clock on the existing 12-core box for S01's 11 episodes (CPU-only HEVC encode); +20 GB during transcode, files end ~30% smaller than HDR originals.

Blast radius: medium-low. Rewrites only R&M — other library entries untouched. Item IDs change (same as #3).

#5 — Wait for GPU restoration, then enable VPP / NVENC tonemap

Action: once nvidia driver is back on the host (doc 13 finding 02), set:

HardwareAccelerationType = nvenc
EnableTonemapping        = true
EnableVppTonemapping     = true   (if Intel — N/A on Ryzen)
HardwareDecodingCodecs   = [hevc, h264, vc1]   (add hevc)

Effect: GPU does the HEVC decode + tonemap + h264 encode. No CPU load, real-time on 4K. This is the long-term right answer.

Cost: L (host driver work). Already on doc 13 fix-list.

Blast radius: large but already planned. Until GPU is back, do remediation #1.

  1. Apply #1 + #2 today (single Playback-settings page edit). Cost ~30 s of ops time, immediate visual fix on R&M, no media churn.
  2. Re-test R&M playback (see §7).
  3. If the tonemapped result still feels "wrong" because R&M is a cartoon and the AI-upscale's HDR is a fiction, go to #4 or #3 for the long-term cure.
  4. Park #5 behind the GPU restoration backlog.

7. Test plan to verify after fix

7a. Pre-fix baseline (capture now, before flipping the bool)

  1. Open https://arrflix.s8n.ru/web/#/details?id=324f75b84f394a5d9b0749c0679f23b9 in Chrome/Firefox on onyx.
  2. Hit Play. Pause at ~30 s in.
  3. Take a screenshot (full-window). File: evidence/21-pre-fix-rm-s01e01-30s.png.
  4. Note the visible characteristics: Rick's lab-coat (should be pure white but currently looks pale-grey), background green of the garage, skin tones.

7b. Apply remediation #1 + #2

UI path: Dashboard → Playback → Transcoding:

  • Enable "Tone-mapping"
  • Enable "Throttle transcodes"
  • Enable "Delete transcode segments"

(Or POST /System/Configuration/encoding directly with the three bools flipped.)

No restart needed — Jellyfin re-reads encoding.xml per request.

7c. Post-fix verification

  1. New playback session — close and reopen the browser tab so the SPA requests a fresh PlaybackInfo and a fresh master.m3u8 (existing in-flight transcode is locked to the pre-fix ffmpeg command line). Easiest: hard-reload (Ctrl-Shift-R) and re-click Play.
  2. Pause at the same ~30 s mark.
  3. Screenshot to evidence/21-post-fix-rm-s01e01-30s.png.
  4. Side-by-side compare the two images. Expectations:
    • Whites are noticeably whiter (lab coat, ship hull).
    • Saturation is higher (garage greens, sky blues, characters).
    • Black-level remains similar (or slightly deeper).
    • Skin tones look natural rather than greenish-grey.

7d. Server-side sanity checks (5 min after first post-fix play)

# Confirm tonemap is in the actual ffmpeg command line for this stream
ssh user@192.168.0.100 \
  "docker exec jellyfin ps -ef | grep ffmpeg | grep -E 'tonemap|zscale' | head"

# Expected: a process line containing `zscale=...:t=linear:...,tonemap=bt2390,...`
# If the line lacks `tonemap`, the encoding.xml change didn't apply or
# JF has a cached transcode session — bounce the container.

# Confirm HDR-aware filter graph fed only by HDR sources (Mandalorian
# should NOT have tonemap in its ffmpeg cmdline)
ssh user@192.168.0.100 \
  "docker exec jellyfin tail -200 /config/log/log_*.log | grep -E 'tonemap|smpte2084'"

7e. Negative test (other libraries unaffected)

Play one episode of:

  • Mandalorian (SDR BT.709) — should look identical pre/post.
  • Futurama / American Dad / Obi-Wan — same expectation (probe these if you want to be thorough; they're outside this audit's scope).

If any of these now look over-saturated or under-saturated post-fix, the tonemap is leaking onto SDR sources — open a bug, set TonemappingMode from auto to a stricter mode.

7f. Performance check (CPU is the operative resource)

While the post-fix R&M episode is playing:

ssh user@192.168.0.100 "uptime && top -bn1 -p \$(pgrep -f 'ffmpeg.*Rick.and.Morty' | head -1) | tail -5"
  • Expect ffmpeg to consume ~600900 % CPU (69 cores) on this host for a 4K HEVC→h264 + tonemap pipeline.
  • If load average climbs past 16 sustained or swap usage grows past baseline 6.8 GiB, escalate doc 13 finding 01 — pair with remediation #4 (pre-transcode the season) sooner rather than later.

7g. Long-term verification

A week after the fix, check:

# Number of 499 client-cancel events on jellyfin@docker
docker logs traefik --since 168h 2>&1 | grep '"jellyfin@docker"' | grep ' 499 ' | wc -l

Should be ≤ pre-fix baseline (currently 2 / hour, doc 13 finding 03). If it climbs after enabling tonemap (because the tonemap stage slowed transcodes enough to let the client time out), that's the trigger to invest in remediation #4 or #5.


8. What was NOT touched during this audit

  • No POST/PUT to /System/Configuration/encoding.
  • No POST to /System/Configuration/branding.
  • No docker exec jellyfin writes (read-only ls, cat, ffprobe).
  • No docker compose action, no container restart.
  • No file modification on /home/user/media/.
  • No transcode triggered (PlaybackInfo simulation only — that endpoint decides codec paths but does not start ffmpeg).

9. Sign-off

  • Auditor: s8n (audit pass, 2026-05-08)
  • Live config at audit time: Jellyfin 10.10.3, EnableTonemapping=false, HardwareAccelerationType=none, TonemappingAlgorithm=bt2390. CSS = Cineplex v1.0.6 + ARRFLIX brand layer (no greyscale filters).
  • Confirmed root cause: HDR10 source (R&M S01) + CPU-only pipeline + tonemap disabled = HDR pixels delivered as SDR = washed-out grey.
  • Recommended fix: flip EnableTonemapping=true (one Playback-settings checkbox) AND EnableThrottling=true + EnableSegmentDeletion=true (pair-finding from doc 13).
  • Next audit due: 2026-08-08 alongside doc 13's quarterly rotation, or sooner if a new HDR source lands in another library.