fix(cast): stop hoarding full-track buffers in the DLNA media server

register_audio inserted every cast track's complete bytes into the
entries map and NOTHING ever removed them — not stop, not disconnect;
only app shutdown dropped the server. A DLNA session therefore grew by
one full track per auto-advance (issue #550: 50 MB at launch to
4.5 GB, never released). Three changes:
- register_audio/register_file evict every other entry — only the
  track being cast is servable anyway.
- stop_if_cast and disconnect clear the entries outright, so stopping
  actually releases the buffers.
- MediaSource::Data is Arc-backed: the per-request entry clone no
  longer copies the whole track (it did, on every GET/HEAD/range
  request), and an in-flight response keeps its own Arc so eviction
  can never truncate an ongoing range request.

Fixes #550

cc
This commit is contained in:
vicrodh committed 2026-07-05 15:08:34 -06:00
1 parent 9dbf3be94d
commit 1261e98cc0
2 files changed
+36 -3

No files matched your search

+22 -2
View File
@@ -48,7 +48,10 @@ struct MediaEntry {
#[derive(Clone)] #[derive(Clone)]
enum MediaSource { enum MediaSource {
Data(Vec<u8>), // Arc so cloning an entry out of the map for an in-flight request never
// copies the whole track, and so eviction can't invalidate a response
// that is still being served (#550).
Data(std::sync::Arc<Vec<u8>>),
File(PathBuf), File(PathBuf),
} }
@@ -150,6 +153,14 @@ impl MediaServer {
} }
/// Get base URL (e.g., "http://192.168.1.100:8080") /// Get base URL (e.g., "http://192.168.1.100:8080")
/// Drop every registered entry (releases the full-track buffers). Called
/// on cast stop/disconnect so track bytes don't outlive the session (#550).
pub fn clear_entries(&self) {
if let Ok(mut entries) = self.entries.lock() {
entries.clear();
}
}
pub fn base_url(&self) -> String { pub fn base_url(&self) -> String {
self.base_url.clone() self.base_url.clone()
} }
@@ -164,10 +175,16 @@ impl MediaServer {
let entry = MediaEntry { let entry = MediaEntry {
content_type: content_type.to_string(), content_type: content_type.to_string(),
size: data.len() as u64, size: data.len() as u64,
source: MediaSource::Data(data), source: MediaSource::Data(std::sync::Arc::new(data)),
}; };
if let Ok(mut entries) = self.entries.lock() { if let Ok(mut entries) = self.entries.lock() {
// Only the track being cast is servable — evict everything else.
// Entries held full track bytes and NOTHING ever removed them, so
// a DLNA session grew by one full track per auto-advance until app
// exit (#550: 50 MB -> 4.5 GB). An in-flight response keeps its
// own Arc, so eviction never truncates an ongoing range request.
entries.retain(|k, _| *k == id);
entries.insert(id, entry); entries.insert(id, entry);
} }
@@ -209,6 +226,9 @@ impl MediaServer {
}; };
if let Ok(mut entries) = self.entries.lock() { if let Ok(mut entries) = self.entries.lock() {
// Same eviction rule as register_audio (#550): switching to a
// local file must also release the previous track's bytes.
entries.retain(|k, _| *k == id);
entries.insert(id, entry); entries.insert(id, entry);
} }
+14 -1
View File
@@ -355,6 +355,11 @@ impl SlintCastService {
inner.protocol = None; inner.protocol = None;
inner.connected_device_ip = None; inner.connected_device_ip = None;
inner.connected_device_name = None; inner.connected_device_name = None;
// Release the served track buffers with the session (#550); the
// server itself stays up for the next connect.
if let Some(server) = inner.media_server.as_ref() {
server.clear_entries();
}
inner.current_track_id = None; inner.current_track_id = None;
inner.is_playing = false; inner.is_playing = false;
inner.track_end_detected = false; inner.track_end_detected = false;
@@ -592,7 +597,15 @@ impl SlintCastService {
return Ok(false); return Ok(false);
} }
self.stop_renderer().await?; self.stop_renderer().await?;
self.inner.lock().await.is_playing = false; {
let mut inner = self.inner.lock().await;
inner.is_playing = false;
// Stop = the user is done with this track; release its bytes
// instead of holding them until the next cast/app exit (#550).
if let Some(server) = inner.media_server.as_ref() {
server.clear_entries();
}
}
self.push_connection_state().await; self.push_connection_state().await;
Ok(true) Ok(true)
} }