Repository navigation
Conversation
baptleduc
force-pushed
the
fix/dlna-cast-audio
branch
from
October 22, 2026 22:25
6e63bc7 to
a2cf508
Compare
baptleduc
force-pushed
the
fix/dlna-cast-audio
branch
2 times, most recently
from
October 22, 2026 22:52
c1537da to
7b46e7c
Compare
Casting to a DLNA TV plays the picture without sound. The cast sheet passes a null StreamInfo to castStreamInfo(), so the video + AAC audio HLS path never runs and the renderer gets the player's current URI, a video-only stream. Pass the streams the player already resolved instead, and the player's duration, in place of the StreamInfo. The audio list must be the unprocessed one: processAudioStreams() keeps one stream per track, usually Opus, and casting only accepts AAC. The pre-muxed streams are passed too, so the fallback without AAC still has sound. Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Baptiste Le Duc <baptiste.leduc@etik.com>
Some renderers reject SetAVTransportURI with UPnP error 701 while they hold a previous session, e.g. after the app restarted mid-cast. Casting then fails until the TV is restarted. Send Stop to the target renderer right before SetAVTransportURI, ignoring its failure when the renderer is idle. Some renderers only finish stopping after answering, so wait up to five seconds for GetTransportInfo to report STOPPED or NO_MEDIA_PRESENT. The Stop request moves to stop(device), next to play(device), and stop() uses it too. Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Baptiste Le Duc <baptiste.leduc@etik.com>
Casts start slowly and stay in the lowest quality. The master playlist lists variants from the lowest resolution up and renderers start on the first one. Each media playlist is a single whole-file segment, so they can never switch. Registering each variant also costs a blocking HEAD request. Pass only the best variant, and keep it H.264: filtering on mp4 lets AV1 through, which is all YouTube offers above 1080p and most renderers cannot decode. Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Baptiste Le Duc <baptiste.leduc@etik.com>
A cast only starts once the renderer has downloaded the whole video, since each HLS media playlist is a single whole-file segment. YouTube's adaptive streams are fragmented MP4 with a segment index (sidx), and the ItagItem carries the byte ranges of the init segment and of that index. Load the index with Media3's DashUtil.loadChunkIndex() and emit one EXT-X-BYTERANGE segment per entry, with EXT-X-MAP for the init segment. The proxy already forwards Range requests. Without ranges or a loadable index, the single-segment playlist stays. The playlist text and the range checks are covered by unit tests. Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Baptiste Le Duc <baptiste.leduc@etik.com>
…in startCast() castTo() takes every HLS and pre-muxed parameter, then chooses between them again after castStreamInfo() already did. Register the chosen stream in castStreamInfo()'s coroutine and pass its URL to a suspending startCast() that only drives the renderer. This prepares trying several formats in turn. No functional changes. Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Baptiste Le Duc <baptiste.leduc@etik.com>
Renderers do not say which HLS formats they play, so one that cannot play segmented fMP4 fails the cast with no fallback. Try these modes in order: SEGMENTED segmented fMP4 HLS, starts in seconds WHOLE_FILE single-segment HLS, downloads the whole file first MUXED best pre-muxed stream, else the current player URL A mode fails when SetAVTransportURI is rejected or, for SEGMENTED, when the renderer is not PLAYING within 10 seconds. Failed modes are remembered per renderer until the app exits, and skipped. Modes a video cannot provide, or whose streams fail to register, are skipped without being held against the renderer. SEGMENTED requires both video and audio segment indexes and fails rather than degrading to one segment, which would miss the timeout and blame the renderer. A new cast or stop() cancels a fallback in progress. A failing Play no longer fails the cast: renderers that buffer before answering time it out, then play. Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Baptiste Le Duc <baptiste.leduc@etik.com>
Renderers that decode AV1 never get more than 1080p, as only H.264 is cast. Add a SEGMENTED_AV1 mode ahead of SEGMENTED, used when an AV1 stream goes above the best H.264 one. It is a separate mode rather than a second variant because renderers can fail playlists that mix codecs: an LG OLED55C5 plays H.264 or AV1 alone but stays in TRANSITIONING when both are listed. A renderer that cannot play AV1 misses the start timeout once, falls back to H.264, and skips AV1 from then on. Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Baptiste Le Duc <baptiste.leduc@etik.com>
Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Baptiste Le Duc <baptiste.leduc@etik.com>
baptleduc
force-pushed
the
fix/dlna-cast-audio
branch
from
October 22, 2026 23:11
7b46e7c to
292cf6c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
DLNA casts played without sound, started slowly and stayed in low quality. One commit per fix:
Stopand waits for the TV to stop.I'm not used to Kotlin or mobile apps, so I wrote this with an AI assistant (Claude), as the
Assisted-by:trailers say. I tested each step on a real TV. Please review it like any other change; I'll answer questions or rework what doesn't fit.Related issue
None.
Change type
Validation
./gradlew :app:assembleGithubDebug./gradlew :app:testGithubDebugUnitTest(4,368 tests, 0 failures)./gradlew ktlintCheck,python3 .github/scripts/nightly_notes.py --check)Test device and Android version: Google Pixel 10 Pro, Android 17, casting to an LG OLED55C5 (webOS).
The video plays with sound in 4K, and the TV requests the first segment about 2 s after the cast starts.
Risk and compatibility
DLNA casting only. No preference, database or permission change.
A TV that can't play a step takes up to about 20 s to fall back, once per app run.
Only tested on one TV.
Known issue, not fixed here: with Flow in the background, Android 17 cuts the app's network and the cast stalls. A foreground service while casting would fix it; I can do that in a follow-up.
The change does not introduce secrets, private data, or unexpected telemetry.
No new user-facing text.
No dependency changes.
No Room schema change.
No breaking changes.
🤖 Generated with Claude Code