Skip to content

Respect disabled audio passthrough codecs - #1377

Open
markusmo3 wants to merge 2 commits into
Moonfin-Client:mainfrom
markusmo3:main
Open

Respect disabled audio passthrough codecs#1377
markusmo3 wants to merge 2 commits into
Moonfin-Client:mainfrom
markusmo3:main

Conversation

@markusmo3

@markusmo3 markusmo3 commented Sep 1, 2026

Copy link
Copy Markdown

Summary

Disabling an audio passthrough option did not previously remove that codec from Moonfin's Jellyfin device profile.

This was particularly visible on Android TV with Media3 or mpv, which use software audio decoding. Because those players can decode many codecs locally, Moonfin continued advertising EAC3 as direct-play compatible even when EAC3 passthrough was disabled. Jellyfin therefore selected Direct Play and delivered the original EAC3 bitstream instead of transcoding it to the configured fallback codec.

This is problematic when the downstream HDMI audio chain cannot reliably handle EAC3 passthrough, even though the playback device itself reports that it can decode or pass through EAC3.

The device profile now treats passthrough-disabled codecs as unsupported for direct play. Jellyfin can consequently keep the video stream unchanged while transcoding only the audio stream to the selected fallback codec.

This also avoids a separate Media3 playback problem observed with the affected EAC3 MKV. When Jellyfin sends the original EAC3 stream, Media3 fails during playback with:

IllegalStateException: No valid varint length mask found

Switching to the mpv legacy engine allows the file to start, but EAC3 handling and audio-track switching remain unreliable. The Fire TV itself supports Dolby Digital Plus output, so this is not simply a lack of EAC3 device capability.

This PR does not attempt to fix the Media3 parser or playback error directly. Instead, it ensures that users who disable EAC3 passthrough do not receive the problematic EAC3 stream in the first place. Jellyfin transcodes EAC3 to the configured fallback codec, such as AC3, while leaving the video untouched.

Example:

Source:
H.264 video + EAC3 5.1 audio

Moonfin:
EAC3 passthrough: disabled
Audio fallback codec: AC3

Result:
Video: copied unchanged
Audio: EAC3 -> AC3
Playback: Direct Stream / audio-only transcode

The change applies consistently to AC3, EAC3, DTS/DCA, and TrueHD/MLP codec variants.

AI Disclaimer

As i dont know the codebase i had help from AI and reviewed the changes it did. Manual testing also confirmed that what AI did works.

Related Issues

  • Related to #

Type of Change

  • Bug fix
  • New feature
  • Refactor
  • Performance improvement
  • UI/UX update
  • Documentation update
  • Build/CI change
  • Other (describe):

Changes Made

  • Prevent passthrough-disabled codecs from being advertised as direct-play compatible.
  • Apply this behavior to players with universal software audio decoding.
  • Keep the configured fallback codec available as a Jellyfin transcoding target.
  • Add regression tests for disabled passthrough codecs and audio-only transcoding.

Platform

  • Android
  • iOS
  • tvOS
  • Web
  • macOS
  • Windows
  • Linux
  • All / Shared code

Testing

  • Tested on emulator / simulator
  • Tested on physical device
  • Manual testing completed
  • Not tested (explain why):

Test Steps

  1. Install the modified Moonfin Android TV build on a Fire TV Stick 4K Max.
  2. Set the audio fallback codec to AC3.
  3. Enable AC3 passthrough and disable EAC3 passthrough.
  4. Play an H.264 + EAC3 5.1 MKV.
  5. Confirm in Jellyfin that the video is copied directly and only the audio is transcoded from EAC3 to AC3.
  6. Confirm that the EAC3 stream is not delivered to the player.

Screenshots (if applicable)

Not applicable.

Checklist

  • Code builds successfully
  • Code follows project style and conventions
  • No unnecessary commented-out code
  • No new warnings introduced

@RadicalMuffinMan

Copy link
Copy Markdown
Contributor

Thanks for this, and for the write up. The Media3 crash you found is real and worth chasing and I appreciate you flagging the AI assistance up front.

I can't take this change though because of what the flags mean by the time they reach the profile builder. The backends don't pass the user's toggles, they pass prefs.resolve*PassthroughEnabled(), and that resolves through three modes with auto as the default:

case AudioPassthroughMode.disabled: return false;
case AudioPassthroughMode.auto:     return capabilityOf(detectedAudioCapabilities);
case AudioPassthroughMode.manual:   return get(pref);

In auto, false means "this HDMI chain can't bitstream it", not "the user asked not to receive it". And the native probe only exists on Android TV and tvOS so everywhere else detectedAudioCapabilities falls back to AudioCapabilityProfile.optimistic(), which is decode-everything and passthrough-nothing.

Building the profile through the real resolve path on a stock install with nothing changed in settings:

default audioPassthroughMode = auto

main:     aac, aac_latm, ac3, alac, dca, dts, eac3, flac, mlp, mp2, mp3, opus,
          pcm_alaw, pcm_mulaw, pcm_s16le, pcm_s20le, pcm_s24le, truehd, vorbis
this PR:  aac, aac_latm, alac, flac, mp2, mp3, opus, pcm_alaw, pcm_mulaw,
          pcm_s16le, pcm_s20le, pcm_s24le, vorbis

Three realistic cases:

auto, no probe (Windows, macOS, Linux, Web, phones, Tizen, webOS)
   dropped: ac3, eac3, dts, dca, truehd, mlp
auto, Android TV into a soundbar that does AC3 and EAC3 only
   dropped: dts, dca, truehd, mlp
passthrough mode "disabled"
   dropped: ac3, eac3, dts, dca, truehd, mlp

So on defaults every desktop, web, phone and Smart TV user would stop direct playing anything with a Dolby or DTS track, and the server would transcode audio on most surround files. The last line is its own problem: "passthrough disabled" is meant to mean decode locally to PCM, not make the server re-encode.

It also turns 12 existing tests red, including "a detected 2ch speaker route no longer restricts direct play (the AAC 5.1 transcode bug)", which is a regression we already fixed once. The test this PR removes, never transcodes for audio: every supported codec is advertised across routes, toggle states and failed capability probes, was there on purpose.

I looked at whether scoping the rule to manual mode would be enough and it isn't. Entering manual seeds every unwritten toggle from the same detected profile, so on a machine with no probe you get:

pref_passthrough_ac3       = false
pref_passthrough_eac3      = false
pref_passthrough_dts_core  = false
pref_passthrough_dts_hd    = false
pref_passthrough_truehd    = false

which means a Windows user who just opens audio settings and switches to manual would lose all six codecs immediately.

The real issue is that one boolean is being asked two different questions. "Can this HDMI chain take the bitstream" and "do not send me this codec at all" are separate intents, and the passthrough toggles only answer the first.

Two things I'd rather do instead, and I'm happy for you to take either:

  1. The Media3 crash. No valid varint length mask found comes out of the EBML reader, which means the parser is reading at an offset it shouldn't be. I can't diagnose it from the message. Could you attach the MKV, or if it's too large, mkvinfo output plus the full cause chain from the log? There's already a targeted retry chain in Media3VideoView.onPlayerError for cases like this, so a per-file recovery has an obvious home once we know what the parser is tripping on.

  2. The setting you actually want. "Let the server convert codecs my device can't bitstream", off by default, that only subtracts from the profile when someone turns it on. That gives you exactly the EAC3 to AC3 behaviour you described without changing anything for people on auto. It's a small, self-contained piece of work if you want it.

@markusmo3

Copy link
Copy Markdown
Author

Thanks for your quick and detailed answer!
I understand fully why you reject the PR as is. I will work on both features you mentioned to get this working.

Here is the mkvinfo in the meantime:

+ EBML head
|+ EBML version: 1
|+ EBML read version: 1
|+ Maximum EBML ID length: 4
|+ Maximum EBML size length: 8
|+ Document type: matroska
|+ Document type version: 4
|+ Document type read version: 2
+ Segment: size 6605933153
|+ Seek head (subentries will be skipped)
|+ EBML void: size 4025
|+ Segment information
| + Timestamp scale: 1000000
| + Multiplexing application: libebml v1.4.5 + libmatroska v1.7.1
| + Writing application: mkvmerge v97.0 ('You Don't Have A Clue') 64-bit
| + Duration: 01:37:18.496000000
| + Date: 2026-07-05 13:38:36 UTC
| + Segment UID: 0x3b 0x30 0xda 0xd4 0x12 0x80 0xb4 0x61 0x6e 0xbc 0xf7 0x2b 0x84 0xb7 0x33 0x74
|+ Tracks
| + Track
|  + Track number: 1 (track ID for mkvmerge & mkvextract: 0)
|  + Track UID: 6966991593493412378
|  + Track type: video
|  + "Lacing" flag: 0
|  + Language: und
|  + Codec ID: V_MPEG4/ISO/AVC
|  + Codec's private data: size 57 (H.264 profile: High @L4.0)
|  + Default duration: 00:00:00.041708333 (23.976 frames/fields per second for a video track)
|  + Language (IETF BCP 47): und
|  + Video track
|   + Pixel width: 1920
|   + Pixel height: 800
|   + Display width: 1920
|   + Display height: 800
| + Track
|  + Track number: 2 (track ID for mkvmerge & mkvextract: 1)
|  + Track UID: 10853331408322348990
|  + Track type: audio
|  + Language: ger
|  + Codec ID: A_EAC3
|  + Default duration: 00:00:00.032000000 (31.250 frames/fields per second for a video track)
|  + Language (IETF BCP 47): de
|  + Name: German
|  + Audio track
|   + Sampling frequency: 48000
|   + Channels: 6
| + Track
|  + Track number: 3 (track ID for mkvmerge & mkvextract: 2)
|  + Track UID: 7105423202675488733
|  + Track type: audio
|  + "Default track" flag: 0
|  + Codec ID: A_EAC3
|  + Default duration: 00:00:00.032000000 (31.250 frames/fields per second for a video track)
|  + Language (IETF BCP 47): en
|  + Name: English
|  + Audio track
|   + Sampling frequency: 48000
|   + Channels: 6
| + Track
|  + Track number: 4 (track ID for mkvmerge & mkvextract: 3)
|  + Track UID: 5307748467350555968
|  + Track type: subtitles
|  + "Lacing" flag: 0
|  + Language: ger
|  + Codec ID: S_TEXT/UTF8
|  + Language (IETF BCP 47): de
|  + Name: German (Forced)
| + Track
|  + Track number: 5 (track ID for mkvmerge & mkvextract: 4)
|  + Track UID: 14928881321231538713
|  + Track type: subtitles
|  + "Default track" flag: 0
|  + "Lacing" flag: 0
|  + Language: ger
|  + Codec ID: S_TEXT/UTF8
|  + Language (IETF BCP 47): de
|  + Name: German
| + Track
|  + Track number: 6 (track ID for mkvmerge & mkvextract: 5)
|  + Track UID: 14984273622282358908
|  + Track type: subtitles
|  + "Default track" flag: 0
|  + "Lacing" flag: 0
|  + Codec ID: S_TEXT/UTF8
|  + Language (IETF BCP 47): en
|  + Name: English (SDH)
|+ EBML void: size 1209
|+ Cluster

and some info i could extract from jellyfin:

Container: mkv
Size: 6.2 GiB

Video
Title: 1080p H264 SDR
Codec: H264
AVC: Yes
Profile: High
Level: 40
Resolution: 1920x800
Aspect ratio: 2.40:1
Anamorphic: No
Interlaced: No
Framerate: 23.976025
Bitrate: 9051 kbps
Bit depth: 8 bit
Video range: SDR
Video range type: SDR
Color space: bt709
Color transfer: bt709
Color primaries: bt709
Pixel format: yuv420p
Ref frames: 1
NAL: 4

Audio
Title: German - Dolby Digital+ - 5.1 - Default
Language: deu
Codec: EAC3
AVC: No
Layout: 5.1
Channels: 6 ch
Bitrate: 640 kbps
Sample rate: 48000 Hz
Default: Yes
Forced: No
External: No

Subtitle
Title: German (Forced) - Default - SUBRIP
Language: deu
Codec: SUBRIP
AVC: No
Default: Yes
Forced: No
External: No

Audio
Title: English - Dolby Digital+ - 5.1
Language: eng
Codec: EAC3
AVC: No
Layout: 5.1
Channels: 6 ch
Bitrate: 640 kbps
Sample rate: 48000 Hz
Default: No
Forced: No
External: No

Subtitle
Title: German - SUBRIP
Language: deu
Codec: SUBRIP
AVC: No
Default: No
Forced: No
External: No

Subtitle
Title: English (SDH) - SUBRIP
Language: eng
Codec: SUBRIP
AVC: No
Default: No
Forced: No
External: No

@markusmo3
markusmo3 force-pushed the main branch 2 times, most recently from 7e9bbd3 to a6cf7e3 Compare September 3, 2026 09:15
@markusmo3

Copy link
Copy Markdown
Author

Hi again @RadicalMuffinMan,

i implemented the two features you mentioned.

In the first commit is the new opt-in flag to "Server transcode unsupported audio". I had issues here understanding the full flow of _isAudioCodecAllowed with the following call to _hlsAudioCodecsForFallback. If you find my solution insufficient, maybe you can point me in the right direction. It works as advertised though.

If we set server transcode to true and enable AC3 passthrough, but disable EAC3 passthrough, this is the resulting startup

2026-09-03T15:07:29.350781 INFO  [media] Playback decision for "REDACTED": transcode (AudioCodecNotSupported) — H264/EAC3 MKV @ 120000000bps
{"timestamp":"2026-09-03T15:07:29.350148","itemId":"7950577c9acfdd16c9972557daa35013","itemName":"REDACTED","backend":"Media3PlayerBackend","mediaSourceId":"7950577c9acfdd16c9972557daa35013","playMethod":"transcode","transcodingReasons":["AudioCodecNotSupported"],"directPlayVerdict":{"requestedByClient":true,"offeredByServer":false,"sourceBitrate":10331554},"selectedAudioStreamIndex":2,"selectedSubtitleStreamIndex":3,"container":"MKV","mediaType":"video","videoCodec":"H264","videoProfile":"High","videoLevel":"40","videoRange":"SDR","videoRangeCapabilities":{"hdr10":true,"hdr10Plus":false,"dolbyVision":true,"dolbyVisionEl":false,"dvProfile5":true,"dvProfile7":false,"dvProfile8":true},"videoCodecCapabilities":{"avc":true,"avcLevel":52,"avcHigh10":false,"avcHigh10Level":0,"hevc":true,"hevcLevel":153,"hevcMain10":true,"hevcMain10Level":153,"av1":true,"av1Main10":true},"audioCodec":"EAC3","audioProfile":"","audioChannels":"6","subtitleCodec":"SUBRIP","subtitleTracks":[{"index":3,"language":"deu","title":"German (Forced) - Default - SUBRIP","codec":"SUBRIP","isExternal":false,"isForced":false,"isHearingImpaired":false,"deliveryMethod":"External","deliveredExternally":true,"backendTrackId":1},{"index":4,"language":"deu","title":"German - SUBRIP","codec":"SUBRIP","isExternal":false,"isForced":false,"isHearingImpaired":false,"deliveryMethod":"External","deliveredExternally":true,"backendTrackId":2},{"index":5,"language":"eng","title":"English (SDH) - SUBRIP","codec":"SUBRIP","isExternal":false,"isForced":false,"isHearingImpaired":false,"deliveryMethod":"External","deliveredExternally":true,"backendTrackId":3}],"allowedAudioCodecs":["AAC","AAC_LATM","AC3","ALAC","FLAC","MP2","MP3","OPUS","PCM_ALAW","PCM_MULAW","PCM_S16LE","PCM_S20LE","PCM_S24LE","VORBIS"],"hlsMpegTsAudioCodecs":["AAC","AC3","MP2","MP3"],"hlsFmp4AudioCodecs":["AAC","AC3","ALAC","FLAC","MP3","OPUS"],"advertisedMaxAudioChannels":"","transcodingMaxAudioChannels":[],"passthroughMode":"manual","passthroughCodecs":["ac3"],"downmixToStereo":false,"prefMaxAudioChannels":0,"ffmpegDecoder":{"available":true,"version":"Lavc60.3.100","supportsTrueHd":true},"audioSpdifCodecs":[],"audioCapabilities":{"canDecodeAc3":true,"canDecodeEac3":true,"canDecodeDts":false,"canDecodeDtsHd":false,"canDecodeTrueHd":true,"canDecodeFlac":true,"canPassthroughAc3":true,"canPassthroughEac3":true,"canPassthroughDts":false,"canPassthroughDtsHd":false,"canPassthroughTrueHd":false,"maxPcmChannels":8,"activeRouteType":"hdmi","routeSupportsHdAudio":false,"supportsAc3":true,"supportsDts":false,"supportsTrueHd":false,"supportsPcm":true,"supportsAac":true},"activeRouteType":"hdmi","routeSupportsHdAudio":false,"deviceAudioCapabilities":{"canDecodeAc3":true,"canDecodeEac3":true,"canDecodeDts":false,"canDecodeDtsHd":false,"canDecodeTrueHd":true,"canDecodeFlac":true,"canPassthroughAc3":true,"canPassthroughEac3":true,"canPassthroughDts":false,"canPassthroughDtsHd":false,"canPassthroughTrueHd":false,"maxPcmChannels":8,"activeRouteType":"hdmi","routeSupportsHdAudio":false,"supportsAc3":true,"supportsDts":false,"supportsTrueHd":false,"supportsPcm":true,"supportsAac":true},"maxStreamingBitrate":120000000}
2026-09-03T15:07:29.351014 DEBUG [playback] Bringup: opening (Media3PlayerBackend, item 7950577c9acfdd16c9972557daa35013, transcode)

The second commit is an automatically working error catch adn fallback to audio transcode. After some more indepth analysis of my mkv i together with the ai came to this conclusion:

            // Some MKVs contain trailing zero padding after the last real
            // cluster while the Segment size extends to EOF. Media3 then reads
            // 0x00 as an EBML length byte, but it has no leading 1-bit and is
            // therefore not a valid varint length mask. A server audio
            // transcode rewrites the stream without that malformed tail, so
            // allow one retry for this specific parser failure.
            PlaybackException.ERROR_CODE_IO_UNSPECIFIED ->
                if (errorIsNoValidVarintLengthMaskFound(error)) {
                    if (audioFallbackAttempted) null else "unsupported_audio"
                } else {
                    null
                }

With this fallback in place there is a quick reload of the stream if we happen to fall into the varint problem and we try to transcode audio only. Here is a log of that happening

2026-09-03T15:07:57.755580 INFO  [media] Playback decision for "REDACTED": directPlay — H264/EAC3 MKV @ 120000000bps
{"timestamp":"2026-09-03T15:07:57.754993","itemId":"7950577c9acfdd16c9972557daa35013","itemName":"REDACTED","backend":"Media3PlayerBackend","mediaSourceId":"7950577c9acfdd16c9972557daa35013","playMethod":"directPlay","transcodingReasons":[],"directPlayVerdict":{"requestedByClient":true,"offeredByServer":true,"sourceBitrate":10331554},"selectedAudioStreamIndex":2,"selectedSubtitleStreamIndex":3,"container":"MKV","mediaType":"video","videoCodec":"H264","videoProfile":"High","videoLevel":"40","videoRange":"SDR","videoRangeCapabilities":{"hdr10":true,"hdr10Plus":false,"dolbyVision":true,"dolbyVisionEl":false,"dvProfile5":true,"dvProfile7":false,"dvProfile8":true},"videoCodecCapabilities":{"avc":true,"avcLevel":52,"avcHigh10":false,"avcHigh10Level":0,"hevc":true,"hevcLevel":153,"hevcMain10":true,"hevcMain10Level":153,"av1":true,"av1Main10":true},"audioCodec":"EAC3","audioProfile":"","audioChannels":"6","subtitleCodec":"SUBRIP","subtitleTracks":[{"index":3,"language":"deu","title":"German (Forced) - Default - SUBRIP","codec":"SUBRIP","isExternal":false,"isForced":false,"isHearingImpaired":false,"deliveryMethod":"Embed","deliveredExternally":false,"backendTrackId":1},{"index":4,"language":"deu","title":"German - SUBRIP","codec":"SUBRIP","isExternal":false,"isForced":false,"isHearingImpaired":false,"deliveryMethod":"Embed","deliveredExternally":false,"backendTrackId":2},{"index":5,"language":"eng","title":"English (SDH) - SUBRIP","codec":"SUBRIP","isExternal":false,"isForced":false,"isHearingImpaired":false,"deliveryMethod":"Embed","deliveredExternally":false,"backendTrackId":3}],"allowedAudioCodecs":["AAC","AAC_LATM","AC3","ALAC","DCA","DTS","EAC3","FLAC","MLP","MP2","MP3","OPUS","PCM_ALAW","PCM_MULAW","PCM_S16LE","PCM_S20LE","PCM_S24LE","TRUEHD","VORBIS"],"hlsMpegTsAudioCodecs":["AAC","AC3","DTS","EAC3","MP2","MP3"],"hlsFmp4AudioCodecs":["AAC","AC3","ALAC","DTS","EAC3","FLAC","MP3","OPUS"],"advertisedMaxAudioChannels":"","transcodingMaxAudioChannels":[],"passthroughMode":"auto","passthroughCodecs":["ac3","eac3"],"downmixToStereo":false,"prefMaxAudioChannels":0,"ffmpegDecoder":{"available":true,"version":"Lavc60.3.100","supportsTrueHd":true},"audioSpdifCodecs":[],"audioCapabilities":{"canDecodeAc3":true,"canDecodeEac3":true,"canDecodeDts":false,"canDecodeDtsHd":false,"canDecodeTrueHd":true,"canDecodeFlac":true,"canPassthroughAc3":true,"canPassthroughEac3":true,"canPassthroughDts":false,"canPassthroughDtsHd":false,"canPassthroughTrueHd":false,"maxPcmChannels":8,"activeRouteType":"hdmi","routeSupportsHdAudio":false,"supportsAc3":true,"supportsDts":false,"supportsTrueHd":false,"supportsPcm":true,"supportsAac":true},"activeRouteType":"hdmi","routeSupportsHdAudio":false,"deviceAudioCapabilities":{"canDecodeAc3":true,"canDecodeEac3":true,"canDecodeDts":false,"canDecodeDtsHd":false,"canDecodeTrueHd":true,"canDecodeFlac":true,"canPassthroughAc3":true,"canPassthroughEac3":true,"canPassthroughDts":false,"canPassthroughDtsHd":false,"canPassthroughTrueHd":false,"maxPcmChannels":8,"activeRouteType":"hdmi","routeSupportsHdAudio":false,"supportsAc3":true,"supportsDts":false,"supportsTrueHd":false,"supportsPcm":true,"supportsAac":true},"maxStreamingBitrate":120000000}
2026-09-03T15:07:57.755800 DEBUG [playback] Bringup: opening (Media3PlayerBackend, item 7950577c9acfdd16c9972557daa35013, directPlay)
2026-09-03T15:07:57.862500 DEBUG [media] Media3: DoVi P7 policy strip (P7 decoder false, P8 decoder true, DoVi display false)
2026-09-03T15:07:58.224664 DEBUG [media] Media3: buffer target 64MB (heap limit 192MB, lowRam=false)
2026-09-03T15:07:58.239585 DEBUG [media] Media3: player rebuilt for new source (surfaceview, sdk 28)
2026-09-03T15:07:58.257304 DEBUG [media] Media3 state: playing=false buffering=true pos=0ms ahead=0ms
2026-09-03T15:07:58.424329 ERROR [media] Media3 [LoadTask]: Unexpected exception loading stream java.lang.IllegalStateException: No valid varint length mask found
2026-09-03T15:07:58.425902 ERROR [media] Media3 [ExoPlayerImplInternal]: Playback error g5.k: Source error
2026-09-03T15:07:58.467581 ERROR [media] Media3 player error:  Source error caused by k: Source error <- q: Unexpected IllegalStateException: No valid varint length mask found <- IllegalStateException: No valid varint length mask found
2026-09-03T15:07:58.488200 ERROR [media] Media3 error:  ERROR_CODE_IO_UNSPECIFIED Source error caused by k: Source error <- q: Unexpected IllegalStateException: No valid varint length mask found <- IllegalStateException: No valid varint length mask found
2026-09-03T15:07:58.513033 DEBUG [playback] Bringup: resolving (Media3PlayerBackend, item 7950577c9acfdd16c9972557daa35013)
2026-09-03T15:07:58.543823 DEBUG [network] → POST https://[REDACTED]/Sessions/Playing/Stopped
2026-09-03T15:07:58.544220 DEBUG [network] → POST https://[REDACTED]/Items/7950577c9acfdd16c9972557daa35013/PlaybackInfo?userId=1d02d75c51454956922291855bc95e16&audioStreamIndex=2&subtitleStreamIndex=3&mediaSourceId=7950577c9acfdd16c9972557daa35013&maxStreamingBitrate=120000000
2026-09-03T15:07:58.764174 DEBUG [network] → POST https://[REDACTED]/Sessions/Playing/Stopped
2026-09-03T15:07:58.790756 DEBUG [network] ← 200 POST https://[REDACTED]/Items/7950577c9acfdd16c9972557daa35013/PlaybackInfo?userId=1d02d75c51454956922291855bc95e16&audioStreamIndex=2&subtitleStreamIndex=3&mediaSourceId=7950577c9acfdd16c9972557daa35013&maxStreamingBitrate=120000000
2026-09-03T15:07:58.793804 INFO  [media] Playback decision for "REDACTED": transcode (AudioCodecNotSupported) — H264/EAC3 MKV @ 120000000bps
{"timestamp":"2026-09-03T15:07:58.793101","itemId":"7950577c9acfdd16c9972557daa35013","itemName":"REDACTED","backend":"Media3PlayerBackend","mediaSourceId":"7950577c9acfdd16c9972557daa35013","playMethod":"transcode","transcodingReasons":["AudioCodecNotSupported"],"directPlayVerdict":{"requestedByClient":false,"offeredByServer":false,"sourceBitrate":10331554},"selectedAudioStreamIndex":2,"selectedSubtitleStreamIndex":3,"container":"MKV","mediaType":"video","videoCodec":"H264","videoProfile":"High","videoLevel":"40","videoRange":"SDR","videoRangeCapabilities":{"hdr10":true,"hdr10Plus":false,"dolbyVision":true,"dolbyVisionEl":false,"dvProfile5":true,"dvProfile7":false,"dvProfile8":true},"videoCodecCapabilities":{"avc":true,"avcLevel":52,"avcHigh10":false,"avcHigh10Level":0,"hevc":true,"hevcLevel":153,"hevcMain10":true,"hevcMain10Level":153,"av1":true,"av1Main10":true},"audioCodec":"EAC3","audioProfile":"","audioChannels":"6","subtitleCodec":"SUBRIP","subtitleTracks":[{"index":3,"language":"deu","title":"German (Forced) - Default - SUBRIP","codec":"SUBRIP","isExternal":false,"isForced":false,"isHearingImpaired":false,"deliveryMethod":"External","deliveredExternally":true,"backendTrackId":1},{"index":4,"language":"deu","title":"German - SUBRIP","codec":"SUBRIP","isExternal":false,"isForced":false,"isHearingImpaired":false,"deliveryMethod":"External","deliveredExternally":true,"backendTrackId":2},{"index":5,"language":"eng","title":"English (SDH) - SUBRIP","codec":"SUBRIP","isExternal":false,"isForced":false,"isHearingImpaired":false,"deliveryMethod":"External","deliveredExternally":true,"backendTrackId":3}],"allowedAudioCodecs":["AAC","AAC_LATM","AC3","ALAC","DCA","DTS","FLAC","MLP","MP2","MP3","OPUS","PCM_ALAW","PCM_MULAW","PCM_S16LE","PCM_S20LE","PCM_S24LE","TRUEHD","VORBIS"],"hlsMpegTsAudioCodecs":["AAC","AC3","DTS","MP2","MP3"],"hlsFmp4AudioCodecs":["AAC","AC3","ALAC","DTS","FLAC","MP3","OPUS"],"advertisedMaxAudioChannels":"","transcodingMaxAudioChannels":[],"passthroughMode":"auto","passthroughCodecs":["ac3","eac3"],"downmixToStereo":false,"prefMaxAudioChannels":0,"ffmpegDecoder":{"available":true,"version":"Lavc60.3.100","supportsTrueHd":true},"audioSpdifCodecs":[],"audioCapabilities":{"canDecodeAc3":true,"canDecodeEac3":true,"canDecodeDts":false,"canDecodeDtsHd":false,"canDecodeTrueHd":true,"canDecodeFlac":true,"canPassthroughAc3":true,"canPassthroughEac3":true,"canPassthroughDts":false,"canPassthroughDtsHd":false,"canPassthroughTrueHd":false,"maxPcmChannels":8,"activeRouteType":"hdmi","routeSupportsHdAudio":false,"supportsAc3":true,"supportsDts":false,"supportsTrueHd":false,"supportsPcm":true,"supportsAac":true},"activeRouteType":"hdmi","routeSupportsHdAudio":false,"deviceAudioCapabilities":{"canDecodeAc3":true,"canDecodeEac3":true,"canDecodeDts":false,"canDecodeDtsHd":false,"canDecodeTrueHd":true,"canDecodeFlac":true,"canPassthroughAc3":true,"canPassthroughEac3":true,"canPassthroughDts":false,"canPassthroughDtsHd":false,"canPassthroughTrueHd":false,"maxPcmChannels":8,"activeRouteType":"hdmi","routeSupportsHdAudio":false,"supportsAc3":true,"supportsDts":false,"supportsTrueHd":false,"supportsPcm":true,"supportsAac":true},"maxStreamingBitrate":120000000}
2026-09-03T15:07:58.794095 DEBUG [playback] Bringup: opening (Media3PlayerBackend, item 7950577c9acfdd16c9972557daa35013, transcode)
2026-09-03T15:07:58.794735 DEBUG [network] ← 204 POST https://[REDACTED]/Sessions/Playing/Stopped
2026-09-03T15:07:58.890174 DEBUG [media] Media3: DoVi P7 policy strip (P7 decoder false, P8 decoder true, DoVi display false)
2026-09-03T15:07:58.951513 DEBUG [network] ← 204 POST https://[REDACTED]/Sessions/Playing/Stopped

Feel free to leave any comments or suggestions. I really want to see this in the main source even if the error might be "rare". It happened with 3 MKVs for me now.

@RadicalMuffinMan

Copy link
Copy Markdown
Contributor

Thanks for turning this around so quickly and for the detailed logs. I checked out the branch and ran device_profile_builder_test.dart and playback_manager_stale_session_test.dart, 88 test pass, never transcodes for audio is untouched now, and transcodeUnsupportedAudio defaults to false everywhere so a stock install advertises exactly what it did before. That was the important part.

The varint fallback in the second commit is good work and I want it. It walks the cause chain, matches only that one parser failure, retries once through audioFallbackAttempted and resets per source, and your log shows the whole loop doing what it should. That commit solves your EAC3 files on its own with no profile change and no new setting.

Which is why I'd rather not take the first commit. It still keys off the wrong signal just for fewer people now. The comment you put on the new gate has it exactly right:

// This opt-in setting is separate from passthrough mode: false means the
// route cannot bitstream the codec, not that the player cannot decode it.
if (transcodeUnsupportedAudio && !_isAudioCodecPassthroughEnabled(...)

and then the condition drops the codec precisely when passthrough is false. It also sits above the universalAudioDecode return, so it overrides the case where the player decodes everything in software. Built through the real path for a desktop user on auto with no probe, which is every Windows, macOS, Linux, web and phone install:

toggle off: aac, aac_latm, ac3, alac, dca, dts, eac3, flac, mlp, mp2, mp3,
            opus, pcm_alaw, pcm_mulaw, pcm_s16le, pcm_s20le, pcm_s24le,
            truehd, vorbis
toggle on : aac, aac_latm, alac, flac, mp2, mp3, opus, pcm_alaw, pcm_mulaw,
            pcm_s16le, pcm_s20le, pcm_s24le, vorbis
lost      : ac3, dca, dts, eac3, mlp, truehd

So someone who ticks a box labelled "Server transcode unsupported audio" loses all six and gets a server audio transcode on every surround file while FFmpeg would have decoded all six locally. Nothing in that list is unsupported, and the label promises something else.

If you want to keep the setting, the condition that matches its name is !_isAudioCodecDecodeSupported(codec, capabilityProfile), placed after the universalAudioDecode return rather than before it. That way it only sends work to the server for codecs this player genuinely cant play which is what a user ticking that box is asking for.

My preference is to land the varint commit on its own and leave the profile alone. Your files play either way once the fallback is in

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

✅ Build Successful

All platform builds passed. You can download the test artifacts below.

Platform Status Artifact
Android ✅ Passed Moonfin_Android_v* + Moonfin_AndroidTV_v*
iOS ✅ Passed Moonfin_iOS_v*_unsigned.ipa
macOS ✅ Passed Moonfin_macOS_v*.dmg
Windows x64 ✅ Passed Moonfin_Windows_v*.exe
Windows ARM64 ✅ Passed Moonfin_WindowsARM64_v*.exe
Linux x64 ✅ Passed Moonfin_Linux_v* (deb/rpm/AppImage/snap/flatpak/tar.gz)
Linux ARM64 ✅ Passed Moonfin_LinuxARM64_v* (deb/rpm/AppImage/snap/flatpak/tar.gz)
Property Value
Commit 062a4bf
Workflow run Build #1258

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants