fix(offline-cache): warm L1 memory cache after v2 CMAF decrypt
Live testing showed v2 offline tracks had a 3-6s gap on gapless transitions even after the track_id + spawn_blocking fixes, because the decrypt itself takes 5-7 seconds for a typical 60-200MB FLAC (disk read of encrypted segments.bin + AES-CTR per-frame decrypt is slower than the track's natural tail). By the time load_cmaf_bundle returns, the previous track has ended and the player's gapless engine has been dropped. Workable only second time: once a track's decrypted bytes are in L1 memory cache, subsequent accesses are ~60ms. That's what happened on the second transition we observed — prefetched tracks hit L1 for free, so gapless landed in time. Fix: after successfully decrypting a v2 bundle, insert the resulting plain-FLAC bytes into the AudioCache (L1) before handing them to the player. Applies to three call sites: - v2_play_track offline cache branch → warm on first play - v2_play_next_gapless CMAF v2 branch → warm on gapless success - v2_library_play_track CMAF v2 branch → warm on Library click Next access to the same track (replay, scrub, gapless re-queue, upcoming-track peek) is now a <100ms L1 hit regardless of the v2 cache_format the row has. The decrypt cost is paid once per track per session, not once per access. This still leaves the "first play of an offline track" taking 5-7s before audio starts. That's a separate optimization — candidates are (a) offline-track prefetch that pre-decrypts upcoming tracks in the background, (b) storing decrypted FLAC in L2 disk cache so cold restarts don't have to decrypt, (c) speeding up the decrypt itself. cc
This commit is contained in:
2 files changed
+16
No files matched your search
@@ -480,6 +480,7 @@ pub async fn v2_library_play_track(
|
|||||||
library_state: State<'_, LibraryState>,
|
library_state: State<'_, LibraryState>,
|
||||||
bridge: State<'_, CoreBridgeState>,
|
bridge: State<'_, CoreBridgeState>,
|
||||||
offline_cache: State<'_, crate::offline_cache::OfflineCacheState>,
|
offline_cache: State<'_, crate::offline_cache::OfflineCacheState>,
|
||||||
|
app_state: State<'_, AppState>,
|
||||||
runtime: State<'_, RuntimeManagerState>,
|
runtime: State<'_, RuntimeManagerState>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
runtime
|
runtime
|
||||||
@@ -551,6 +552,8 @@ pub async fn v2_library_play_track(
|
|||||||
qobuz_track_id
|
qobuz_track_id
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
// Warm L1 so subsequent access (replay, gapless) is instant.
|
||||||
|
app_state.audio_cache.insert(qobuz_track_id as u64, audio_data.clone());
|
||||||
let bridge = bridge.get().await;
|
let bridge = bridge.get().await;
|
||||||
bridge
|
bridge
|
||||||
.player()
|
.player()
|
||||||
|
|||||||
@@ -473,6 +473,12 @@ pub async fn v2_play_next_gapless(
|
|||||||
"[V2/GAPLESS] Track {} from OFFLINE cache (CMAF v2)",
|
"[V2/GAPLESS] Track {} from OFFLINE cache (CMAF v2)",
|
||||||
track_id
|
track_id
|
||||||
);
|
);
|
||||||
|
// Warm L1 with the decrypted bytes so subsequent
|
||||||
|
// accesses (re-gapless, replay, scrub) skip the
|
||||||
|
// disk-read + decrypt and hit memory directly.
|
||||||
|
// Without this, every offline-cache gapless attempt
|
||||||
|
// re-does 5-7s of I/O + AES work.
|
||||||
|
app_state.audio_cache.insert(track_id, audio_data.clone());
|
||||||
player
|
player
|
||||||
.play_next(audio_data, track_id)
|
.play_next(audio_data, track_id)
|
||||||
.map_err(RuntimeError::Internal)?;
|
.map_err(RuntimeError::Internal)?;
|
||||||
@@ -925,6 +931,13 @@ pub async fn v2_play_track(
|
|||||||
// Keep as fallback — don't discard, network might fail
|
// Keep as fallback — don't discard, network might fail
|
||||||
low_quality_fallback = Some(audio_data);
|
low_quality_fallback = Some(audio_data);
|
||||||
} else {
|
} else {
|
||||||
|
// Warm L1 with the decrypted bytes so the next
|
||||||
|
// access (replay, gapless re-queue, scrub) skips
|
||||||
|
// the 5-7s disk + AES decrypt round-trip. Without
|
||||||
|
// this, every offline-cache play_next_gapless fails
|
||||||
|
// to land in time and the player's gapless engine
|
||||||
|
// has already been dropped.
|
||||||
|
app_state.audio_cache.insert(track_id, audio_data.clone());
|
||||||
player
|
player
|
||||||
.play_data(audio_data, track_id)
|
.play_data(audio_data, track_id)
|
||||||
.map_err(RuntimeError::Internal)?;
|
.map_err(RuntimeError::Internal)?;
|
||||||
|
|||||||
Reference in new issue
Block a user