Skip to content

fix(playback): probe like the player, continue lossless search, and defer artwork - #286

Merged
ajisth69 merged 2 commits into
Clash-Projects:mainfrom
shaktixdev:fix/playback-probe-upgrade-artwork
Oct 10, 2026
Merged

ajisth69 merged 2 commits into
Clash-Projects:mainfrom
shaktixdev:fix/playback-probe-upgrade-artwork

Conversation

@shaktixdev

@shaktixdev shaktixdev commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

After #285, a cached tap already started the tapped video in under 200 ms. Three gaps remained in the post-merge device log: the stream probe accepted a URL the player then rejected, a duration miss ended the lossless search, and theme/Discord artwork hit the network at tap time. A failed extract on the supervisor scope could also surface as an uncaught background exception.

This branch probes the way ExoPlayer opens the URL, keeps walking lossless candidates past a 35-second miss, and delays theme and Discord artwork by 3 seconds. A network or extraction failure stays inside playback and becomes a recoverable error. Crash reports now keep the full exception chain and any system trace Android attached.

Device log lastwave-diagnostics-1791575624609.txt (Nothing A001, app 4.2.4, this build) shows four cache-hit startups at 146–170 ms, HTTP 200 probes, a FLAC swap on every track, metadata starting about 3 seconds after the tap, and no crash.

Root cause

  • probePlayable sent Range: bytes=0-1. YouTube answered HTTP 206 for 2 bytes. ExoPlayer opened the same URL with no range and got HTTP 403. Playback invalidated the URL, extracted again, and the startup line was about 8 seconds.
  • scheduleQualityUpgrade called the addon with expectedDurationSeconds = null, so the duration ceiling never ran. The first title match could be a different edit. The player’s 35-second check then returned, and later candidates in that search were never tried. The track stayed on AAC.
  • MusicPlaybackService already waited until audible and then 3 seconds before artwork. ThemeRepository.updateNowPlayingArtwork and DiscordPresenceManager called artworkRepository.resolve on the now-playing change, which reached Deezer, iTunes, and Tidal at the tap.
  • resolveAudioStream runs on apiScope (SupervisorJob + async). An IOException when every client misses is stored for the waiter and also reported as an uncaught exception. Prefetch only caught CancellationException, so the same failure could kill the process. A resolve timeout is a TimeoutCancellationException, which the playback catch rethrew as cancellation, so the track never got a recoverable error.
  • Historical exit records for reason 4 (crash) wrote the reason code, RSS, and a one-line description. They did not write the cause chain or ApplicationExitInfo.getTraceInputStream().

Changes

  • Probe. NewPipe opens the URL with the player headers (Accept: audio/*,*/*;q=0.8, Accept-Encoding: identity, no byte range) and closes the body once the status is known. HTTP 403 or 410 rejects that itag. The next distinct URL is tried, up to 4. A duplicate URL in the same pass is skipped. CancellationException is rethrown. If all four fail, playback still gets YouTube audio URLs were rejected. InnerTubeMusicApi.probeStream is unchanged.
  • Lossless. The playing duration is passed into resolveLosslessTrackAudioStream. A stream that still misses the 35-second check is added to excludedLosslessUrls and resolved again, at most 3 times, with ensureActive() and the generation check between attempts. If none fit, the log is No upgrade stream found and YouTube keeps playing. The 35-second ceiling is unchanged.
  • Artwork. Theme and Discord use DeferredArtworkLookup. A cover already in memory is used immediately. Otherwise the previous job is cancelled, METADATA_DELAYED is logged, and resolve runs after 3 seconds only if that title+artist key is still current. A newer track logs METADATA_SKIPPED. Playback does not wait on this path. The playback-service artwork timer is unchanged.
  • Failed resolve. Expected network, HTTP, extractor, and timeout failures are contained on the extract scopes. Prefetch logs RESOLVE_FAILED and does not publish a URL. A timeout becomes IOException("Timed out resolving audio") so the player can show it and retry. A real skip still cancels.
  • Diagnostics. Each resolve logs one LastWaveStream line: clients tried, HTTP status, extractor error class, and whether fallback was exhausted. Signed URLs, cookies, and signature query parameters are stripped. Crash files include the full cause chain, the failing thread’s id and state, a short dump of other threads, and the system trace on a historical exit. If Android attached no trace, the file says so.
  • Tests. YouTubeProbeSelectionTest, UpgradeCandidateTest, DeferredArtworkLookupTest, ResolutionAttemptLogTest, CrashReportTextTest, and the added StreamResolveTest cases.

Acceptance criteria

  • A URL that answers 403 or 410 is not the one installed. The probe uses the player’s open (no bytes=0-1 range). The latest log shows HTTP 200 and no playback 403.
  • A duration miss does not end the lossless search. The latest log shows a FLAC swap on every played track, with no duration-mismatch abort.
  • Theme and Discord artwork log METADATA_DELAYED at the tap and METADATA_STARTED about 3 seconds later. Deezer, iTunes, and Tidal are not called at the tap.
  • Cache-hit [STARTUP] totalMs stays under 200 ms (146–170 ms in the latest log).
  • A resolve that fails for every client logs the clients, statuses, and fallback=exhausted, leaves a recoverable playback error, and does not kill the process. This log’s extracts all succeeded via NewPipe, so the failure path was not seen on device.
  • A historical reason-4 exit file includes the exception chain or the attached system trace.
  • ./gradlew :app:testDebugUnitTest --tests com.lastwave.app.data.music.YouTubeProbeSelectionTest --tests com.lastwave.app.playback.UpgradeCandidateTest --tests com.lastwave.app.data.artwork.DeferredArtworkLookupTest --tests com.lastwave.app.data.music.ResolutionAttemptLogTest --tests com.lastwave.app.diagnostics.CrashReportTextTest --tests com.lastwave.app.playback.resolve.StreamResolveTest passes.
  • ./gradlew :app:compileDebugKotlin and ./gradlew :app:assembleDebug succeed.

Performance validation

Measured on device after this build, log/lastwave-diagnostics-1791575624609.txt (Nothing A001, app 4.2.4, pid 31773, 2026-10-10 01:20–01:23). No persisted crash files.

Case totalMs Notes
Cache hit 153 cacheHit=true, then FLAC swap at 5.9 s
Cache hit 161 cacheHit=true, then FLAC 24/48 swap at 3.0 s
Cache hit 146 cacheMs=1, then FLAC 24/44.1 swap at 8.7 s
Cache hit 170 cacheHit=true, then FLAC 16/44.1 swap at 4.5 s
Cold prefetch extract 3120–3223 stage=newpipe, itag 140, probe HTTP 200. Direct clients logged Sign in to confirm you’re not a bot, then direct-fast=exhausted

The post-merge log lastwave-diagnostics-1791564509639.txt is the before case: HTTP 206 for 2 bytes, then ExoPlayer HTTP 403, startup totalMs=7975, and lossless duration rejects that left the track on AAC.

./gradlew :app:assembleDebug succeeded. APK: app/build/outputs/apk/debug/app-debug.apk.

Out of scope / remaining limits

  • A song that has never been resolved still needs one network extract. VISIONOS, ANDROID_VR, and TVHTML5 still return no playable URL (Sign in to confirm you’re not a bot), so that extract is NewPipe at about 2.7–3.2 s. Prefetch hides it for the next queue item.
  • A URL that returns 200 on the first open and 403 on a later byte range can still reach the player. This probe matches the first open and closes the body.
  • The 35-second duration ceiling is unchanged. If no candidate is inside it, YouTube keeps playing.
  • The first frame is still about 1.3 s after activity.content.set (01:20:33.754 → 01:20:35.065 in the startup trail). That stall was not changed.
  • InnerTubeMusicApi.probeStream still uses Range: bytes=0-1. The 403 in the post-merge log was the NewPipe path.

Test plan

  • Install the debug APK and play cached songs. Confirm [STARTUP] cacheHit=true and totalMs under 200.
  • Leave a lossy song playing with lossless enabled. Confirm a later FLAC swap at the same position.
  • Confirm theme artwork logs METADATA_DELAYED at the tap and METADATA_STARTED about 3 seconds later.
  • Skip several tracks inside 3 seconds and confirm METADATA_SKIPPED for the ones that were left.
  • Play a song that is not cached. Confirm one NewPipe extract, an HTTP 200 probe, and no second extract after a 403.
  • ./gradlew :app:testDebugUnitTest --tests com.lastwave.app.data.music.YouTubeProbeSelectionTest --tests com.lastwave.app.playback.UpgradeCandidateTest --tests com.lastwave.app.data.artwork.DeferredArtworkLookupTest --tests com.lastwave.app.playback.resolve.StreamResolveTest

…s, and defer artwork

A 2-byte probe accepted URLs the player then rejected, a duration miss stopped the lossless search, and theme/Discord artwork hit the network at tap time.
Keep the 10-second duration ceiling from the parallel-race change, and keep walking the next lossless candidate instead of stopping on the first miss.
@ajisth69
ajisth69 merged commit b6db40a into Clash-Projects:main Oct 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants