@@ -174,6 +174,11 @@ pub struct HttpMediaSource {
174174 /// (radio servers resend the same title every interval) doesn't
175175 /// re-fire the event.
176176 last_title : Option < String > ,
177+ /// Writes what this source reads into the on-disk stream cache, when the
178+ /// caller asked for it and the body declared a length. `None` for radio
179+ /// (endless, so never complete) and whenever the cache is unusable — it
180+ /// is an optimisation, and its absence is never a playback failure.
181+ cache : Option < crate :: audio:: stream_cache:: CacheWriter > ,
177182}
178183
179184impl HttpMediaSource {
@@ -182,15 +187,15 @@ impl HttpMediaSource {
182187 /// 404 / 502 surfaces as `Err` instead of producing a `MediaSource`
183188 /// that would only fail at the first `probe()` call.
184189 pub fn open ( url : & str ) -> Result < Self , String > {
185- Self :: open_inner ( url, None , false )
190+ Self :: open_inner ( url, None , false , None )
186191 }
187192
188193 /// Open a streaming HTTP GET that also requests + de-interleaves ICY
189194 /// metadata, re-emitting `player:radio-metadata` through `icy.app`
190195 /// whenever the live `StreamTitle` changes. Falls back transparently
191196 /// to passthrough when the server ignores `Icy-MetaData: 1`.
192197 pub fn open_with_icy ( url : & str , icy : IcyContext ) -> Result < Self , String > {
193- Self :: open_inner ( url, Some ( icy) , false )
198+ Self :: open_inner ( url, Some ( icy) , false , None )
194199 }
195200
196201 /// Open a finite, range-capable file for **seekable** playback — a
@@ -200,10 +205,29 @@ impl HttpMediaSource {
200205 /// drive a real `format.seek`. Degrades to forward-only if the server
201206 /// doesn't answer ranges — playback still works, only scrubbing won't.
202207 pub fn open_seekable ( url : & str ) -> Result < Self , String > {
203- Self :: open_inner ( url, None , true )
208+ Self :: open_inner ( url, None , true , None )
204209 }
205210
206- fn open_inner ( url : & str , icy : Option < IcyContext > , want_seek : bool ) -> Result < Self , String > {
211+ /// [`Self::open_seekable`], additionally filling the on-disk stream cache
212+ /// from the bytes playback reads.
213+ ///
214+ /// Nothing extra is fetched and nothing is delayed: the cache is written
215+ /// from the blocks the decoder was going to read anyway, at their absolute
216+ /// offsets, so a seek leaves a hole rather than corrupting the file and
217+ /// the entry is simply never published.
218+ pub fn open_seekable_caching (
219+ url : & str ,
220+ target : crate :: audio:: stream_cache:: CacheTarget ,
221+ ) -> Result < Self , String > {
222+ Self :: open_inner ( url, None , true , Some ( target) )
223+ }
224+
225+ fn open_inner (
226+ url : & str ,
227+ icy : Option < IcyContext > ,
228+ want_seek : bool ,
229+ cache_target : Option < crate :: audio:: stream_cache:: CacheTarget > ,
230+ ) -> Result < Self , String > {
207231 // Offline short-circuit at the HTTP boundary itself. The decoder
208232 // already gates `LoadUrlAndPlay` on this before reaching here, but
209233 // guarding the source too makes it self-honouring for any future
@@ -269,6 +293,12 @@ impl HttpMediaSource {
269293 // `byte_len() == None` exactly as before, even on the rare stream
270294 // that sends a Content-Length, so symphonia never treats it as
271295 // finite.
296+ // Two different questions were sharing one variable. Seeking needs a
297+ // length AND ranges; caching needs only a length, because it is filled
298+ // by reading forward. Collapsing them meant a server that sends
299+ // `Content-Length` without `Accept-Ranges` — perfectly cacheable —
300+ // was never cached at all.
301+ let declared_len = len;
272302 let len = if seekable { len } else { None } ;
273303
274304 Ok ( Self {
@@ -285,6 +315,14 @@ impl HttpMediaSource {
285315 // actually parse blocks).
286316 icy : if metaint > 0 { icy } else { None } ,
287317 last_title : None ,
318+ // Only a body that declared its length can be cached: without one
319+ // there is no way to decide that every byte has been covered, and
320+ // a file we cannot call complete must never be offered as one.
321+ cache : cache_target. and_then ( |target| {
322+ declared_len. and_then ( |len| {
323+ crate :: audio:: stream_cache:: CacheWriter :: create ( & target. dir , & target. name , len)
324+ } )
325+ } ) ,
288326 } )
289327 }
290328
@@ -406,7 +444,17 @@ impl Read for HttpMediaSource {
406444 // byte cursor here for `SeekFrom::Current`.
407445 if self . metaint == 0 {
408446 let n = guard. read ( buf) ?;
447+ drop ( guard) ;
448+ let start = self . pos ;
409449 self . pos += n as u64 ;
450+ // Record where these bytes live, not merely that they arrived: a
451+ // seek reopens the body at another offset, so an append would
452+ // interleave two regions into one corrupt file.
453+ if n > 0 {
454+ if let Some ( cache) = self . cache . as_mut ( ) {
455+ cache. write_at ( start, & buf[ ..n] ) ;
456+ }
457+ }
410458 return Ok ( n) ;
411459 }
412460 if buf. is_empty ( ) {
0 commit comments