Skip to main content

koan_core/audio/
streaming.rs

1//! `PartialFileSource` — a Read+Seek adapter over a file that is still downloading.
2//!
3//! The download thread writes the track to a `.part` file and publishes how far
4//! it has got in a `ByteFeed`; the Symphonia decoder reads the same file
5//! through a `PartialFileSource`, which blocks when the read position catches
6//! up to the write head. Playback starts long before the transfer finishes and
7//! seeking anywhere below the write head costs a `lseek`.
8//!
9//! Nothing is copied: holding the track in memory would cost as much RAM as the
10//! track is long — half a gigabyte for a nine-hour recording. The bytes are
11//! already on disk; the page cache is better at this.
12//!
13//! The open descriptor survives the download's final rename from `.part` to its
14//! cache path, so a transfer landing mid-playback changes nothing for a reader.
15
16use std::fs::File;
17use std::io::{self, Read, Seek, SeekFrom};
18use std::path::Path;
19use std::sync::Arc;
20use std::sync::atomic::Ordering;
21use std::time::{Duration, Instant};
22
23/// Longest a read may block waiting for bytes before the transfer counts as
24/// dead. A download that stops advancing must surface as an error, not park the
25/// decode thread forever holding the ring buffer producer.
26const STALL_LIMIT: Duration = Duration::from_secs(30);
27
28/// Where a download has got to, as the source needs to know it.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum StreamStatus {
31    /// Bytes are still arriving.
32    Downloading,
33    /// Every byte landed. Reads past the end are a clean EOF.
34    Complete,
35    /// The transfer died before delivering everything. Reads past the written
36    /// bytes fail rather than reporting EOF, which would silently truncate the
37    /// track and look like a short file.
38    Failed,
39}
40
41/// A `Read + Seek` view of a file that is still being written.
42pub struct PartialFileSource {
43    file: File,
44    pos: u64,
45    /// How many bytes the download has committed to disk so far.
46    ///
47    /// Whatever ends a transfer must call `ByteFeed::done` when it sets the
48    /// status: a read waiting for bytes is parked on the feed, and a download
49    /// that failed has no more bytes to wake it with.
50    bytes_written: Arc<crate::remote::downloads::ByteFeed>,
51    /// Total expected length, or 0 when the server sent no Content-Length.
52    total: u64,
53    status: Arc<dyn Fn() -> StreamStatus + Send + Sync>,
54    stall_limit: Duration,
55    /// Whether a read may wait at the write head for more of the download.
56    ///
57    /// Playback waits; probing does not. A container asked to describe itself
58    /// reads whatever it needs to, and Ogg needs its last page — so a probe
59    /// that waits waits for the whole transfer. Refusing instead turns that
60    /// into an immediate answer of "not from what has arrived", which is
61    /// something the caller can act on.
62    wait_for_bytes: bool,
63    /// Whether to state the advertised length.
64    ///
65    /// Saying nothing is what stops a container going looking for its tail:
66    /// Ogg reads its final page only when the source claims both a length and
67    /// seekability, and Symphonia scans for trailing metadata on the same
68    /// terms — which no partial file can satisfy. So this is what every
69    /// container mid-download ends up opened with, not only Ogg.
70    ///
71    /// It governs the end of the stream as well as the length, because they
72    /// have to be the same end. See `Seek`.
73    advertise_len: bool,
74    /// Whether `SeekFrom::End` answers for the whole file or for what has
75    /// arrived. Separate from `advertise_len` because Ogg needs the first
76    /// without the second — see `ProbeMode`.
77    whole_file_end: bool,
78}
79
80/// How much a probe may claim, and how far it may read.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum ProbeMode {
83    /// State the length. A container that can describe itself from the bytes
84    /// already downloaded does, and the result is fully seekable.
85    Full,
86    /// State no length, so a container that would go looking for its tail
87    /// settles for what it can read from the front. Opens immediately, and
88    /// stays seekable within what has arrived for anything that describes its
89    /// frames from the front. What it gives up is whatever only the tail could
90    /// give — for Ogg that is the duration, and with it seeking at all.
91    ///
92    /// The end is what has arrived. FLAC bisects between its first frame and
93    /// the end it is given, so an end it cannot reach sends every probe into
94    /// bytes that are not on disk.
95    Lengthless,
96    /// The same, except that the end stays the whole file's.
97    ///
98    /// Ogg takes the end it is handed as the end of the *stream*. Handed the
99    /// write head it reports a track that is already over — nought
100    /// milliseconds — and the decode thread reaches the end of it in a second
101    /// and moves on to the next, over and over, so a large Opus file
102    /// downloading never plays at all. It cannot seek mid-download under either
103    /// answer; this is the difference between playing and not.
104    LengthlessWholeEnd,
105}
106
107impl PartialFileSource {
108    /// Open `path` for playback: reads wait at the write head for the download
109    /// to catch up. `bytes_written` is the download's own counter and `total`
110    /// its advertised length, 0 when it sent none.
111    ///
112    /// `mode` must be whatever the probe settled on. A container opened without
113    /// a length to describe itself has to be decoded without one too — given a
114    /// length it goes looking for its tail all over again, and this time on the
115    /// decode thread, where the cost is silence rather than a busy player.
116    pub fn open(
117        path: &Path,
118        bytes_written: Arc<crate::remote::downloads::ByteFeed>,
119        total: u64,
120        status: Arc<dyn Fn() -> StreamStatus + Send + Sync>,
121        mode: ProbeMode,
122    ) -> io::Result<Self> {
123        let mut source = Self::with_stall_limit(path, bytes_written, total, status, STALL_LIMIT)?;
124        source.advertise_len = mode == ProbeMode::Full;
125        source.whole_file_end = mode != ProbeMode::Lengthless;
126        Ok(source)
127    }
128
129    /// Open for a probe: never waits at the write head, so a container that
130    /// cannot describe itself from what has arrived says so at once.
131    pub fn open_for_probe(
132        path: &Path,
133        bytes_written: Arc<crate::remote::downloads::ByteFeed>,
134        total: u64,
135        status: Arc<dyn Fn() -> StreamStatus + Send + Sync>,
136        mode: ProbeMode,
137    ) -> io::Result<Self> {
138        let mut source = Self::with_stall_limit(path, bytes_written, total, status, STALL_LIMIT)?;
139        source.wait_for_bytes = false;
140        source.advertise_len = mode == ProbeMode::Full;
141        source.whole_file_end = mode != ProbeMode::Lengthless;
142        Ok(source)
143    }
144
145    fn with_stall_limit(
146        path: &Path,
147        bytes_written: Arc<crate::remote::downloads::ByteFeed>,
148        total: u64,
149        status: Arc<dyn Fn() -> StreamStatus + Send + Sync>,
150        stall_limit: Duration,
151    ) -> io::Result<Self> {
152        Ok(Self {
153            file: File::open(path)?,
154            pos: 0,
155            bytes_written,
156            total,
157            status,
158            stall_limit,
159            wait_for_bytes: true,
160            advertise_len: true,
161            whole_file_end: true,
162        })
163    }
164
165    /// Bytes known to be readable — what the download has written, or the whole
166    /// file once it has landed.
167    fn available(&self) -> u64 {
168        let written = self.bytes_written.load(Ordering::Acquire);
169        match (self.status)() {
170            StreamStatus::Complete => self.file.metadata().map(|m| m.len()).unwrap_or(written),
171            _ => written,
172        }
173    }
174
175    /// Read straight from the file, tolerating a short read at the write head:
176    /// `bytes_written` is published by the downloader as it goes and the data
177    /// behind it can lag by a moment.
178    fn read_available(&mut self, buf: &mut [u8], limit: u64) -> io::Result<usize> {
179        let to_read = (limit as usize).min(buf.len());
180        self.file.read(&mut buf[..to_read]).inspect(|n| {
181            self.pos += *n as u64;
182        })
183    }
184}
185
186impl Read for PartialFileSource {
187    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
188        if buf.is_empty() {
189            return Ok(0);
190        }
191
192        let deadline = Instant::now() + self.stall_limit;
193        loop {
194            let available = self.available();
195            if available > self.pos {
196                let n = self.read_available(buf, available - self.pos)?;
197                if n > 0 {
198                    return Ok(n);
199                }
200                // The counter ran ahead of what is visible on disk. Fall
201                // through and wait rather than reporting a false EOF.
202            }
203
204            match (self.status)() {
205                StreamStatus::Failed => {
206                    return Err(io::Error::new(
207                        io::ErrorKind::BrokenPipe,
208                        "stream download failed before delivering the whole track",
209                    ));
210                }
211                // Everything landed and there is nothing past `pos`: real EOF.
212                StreamStatus::Complete if available <= self.pos => return Ok(0),
213                StreamStatus::Complete => {}
214                StreamStatus::Downloading => {
215                    // A server that sent a Content-Length has delivered it all.
216                    if self.total > 0 && available >= self.total && self.pos >= self.total {
217                        return Ok(0);
218                    }
219                }
220            }
221
222            if !self.wait_for_bytes {
223                return Err(io::Error::new(
224                    io::ErrorKind::UnexpectedEof,
225                    "past what the download has delivered",
226                ));
227            }
228            if Instant::now() >= deadline {
229                return Err(io::Error::new(
230                    io::ErrorKind::TimedOut,
231                    "stream download stalled",
232                ));
233            }
234            // Woken by the download itself, and by it finishing or failing.
235            // The deadline is the giving-up clock rather than a look-again
236            // one: this thread is not scheduled between one chunk and the next.
237            self.bytes_written.wait_past(available, deadline);
238        }
239    }
240}
241
242impl Seek for PartialFileSource {
243    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
244        let target: i64 = match pos {
245            SeekFrom::Start(n) => n as i64,
246            SeekFrom::Current(n) => self.pos as i64 + n,
247            // The end has to be the same end `byte_len` describes. A source
248            // that states a length is asked about the whole file and answers
249            // for it; one that states none has only what has arrived, and
250            // answering with the advertised total there sends a reader
251            // bisecting into bytes that are not on disk yet — which is the
252            // whole file's worth of waiting for a FLAC seeked mid-download.
253            SeekFrom::End(n) => {
254                let len = if self.whole_file_end && self.total > 0 {
255                    self.total
256                } else {
257                    self.available()
258                };
259                len as i64 + n
260            }
261        };
262
263        if target < 0 {
264            return Err(io::Error::new(
265                io::ErrorKind::InvalidInput,
266                "seek before beginning of stream",
267            ));
268        }
269
270        self.pos = self.file.seek(SeekFrom::Start(target as u64))?;
271        Ok(self.pos)
272    }
273}
274
275// Symphonia requires MediaSource: Read + Seek + Send + Any
276impl symphonia::core::io::MediaSource for PartialFileSource {
277    fn is_seekable(&self) -> bool {
278        // Backward seeks and forward seeks below the write head are a `lseek`
279        // on a file that is already there. A forward seek past it lands on a
280        // read that blocks until the bytes arrive, which is the honest
281        // behaviour — callers clamp to `seekable_ms` to avoid asking.
282        //
283        // Saying no would cost more than it saved: a reader told a stream is
284        // unseekable does not stop seeking, it walks the whole file to the
285        // target instead, and cannot go backwards at all.
286        true
287    }
288
289    fn byte_len(&self) -> Option<u64> {
290        (self.advertise_len && self.total > 0).then_some(self.total)
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use std::io::Write;
297    use std::sync::atomic::AtomicU8;
298
299    use symphonia::core::io::MediaSource;
300
301    use super::*;
302
303    /// A file plus the counter and status a download would publish, so a test
304    /// can advance either independently.
305    struct Fixture {
306        _dir: tempfile::TempDir,
307        path: std::path::PathBuf,
308        written: Arc<crate::remote::downloads::ByteFeed>,
309        status: Arc<AtomicU8>,
310    }
311
312    const DOWNLOADING: u8 = 0;
313    const COMPLETE: u8 = 1;
314    const FAILED: u8 = 2;
315
316    impl Fixture {
317        fn new() -> Self {
318            let dir = tempfile::tempdir().unwrap();
319            let path = dir.path().join("track.opus.part");
320            File::create(&path).unwrap();
321            Self {
322                _dir: dir,
323                path,
324                written: crate::remote::downloads::ByteFeed::new(),
325                status: Arc::new(AtomicU8::new(DOWNLOADING)),
326            }
327        }
328
329        /// Append bytes and publish them, as the downloader does per chunk.
330        fn push(&self, chunk: &[u8]) {
331            let mut f = std::fs::OpenOptions::new()
332                .append(true)
333                .open(&self.path)
334                .unwrap();
335            f.write_all(chunk).unwrap();
336            f.flush().unwrap();
337            self.written.advance(chunk.len() as u64);
338        }
339
340        fn set(&self, status: u8) {
341            self.status.store(status, Ordering::Release);
342            // What ends a transfer says so, the way the downloader does — a
343            // reader blocked for bytes that will never come is waiting on the
344            // feed, not on the status.
345            self.written.done();
346        }
347
348        fn status_fn(&self) -> Arc<dyn Fn() -> StreamStatus + Send + Sync> {
349            let status = self.status.clone();
350            Arc::new(move || match status.load(Ordering::Acquire) {
351                COMPLETE => StreamStatus::Complete,
352                FAILED => StreamStatus::Failed,
353                _ => StreamStatus::Downloading,
354            })
355        }
356
357        fn source(&self, total: u64) -> PartialFileSource {
358            self.source_with_stall(total, STALL_LIMIT)
359        }
360
361        fn source_with_stall(&self, total: u64, stall: Duration) -> PartialFileSource {
362            let status = self.status.clone();
363            PartialFileSource::with_stall_limit(
364                &self.path,
365                self.written.clone(),
366                total,
367                Arc::new(move || match status.load(Ordering::Acquire) {
368                    COMPLETE => StreamStatus::Complete,
369                    FAILED => StreamStatus::Failed,
370                    _ => StreamStatus::Downloading,
371                }),
372                stall,
373            )
374            .unwrap()
375        }
376    }
377
378    #[test]
379    fn reads_what_has_landed() {
380        let fx = Fixture::new();
381        fx.push(b"hello streaming world");
382        fx.set(COMPLETE);
383
384        let mut out = Vec::new();
385        fx.source(21).read_to_end(&mut out).unwrap();
386        assert_eq!(out, b"hello streaming world");
387    }
388
389    #[test]
390    fn read_stops_at_the_write_head_then_resumes() {
391        let fx = Fixture::new();
392        fx.push(b"abcd");
393        let mut src = fx.source(10);
394
395        let mut first = [0u8; 8];
396        assert_eq!(src.read(&mut first).unwrap(), 4);
397        assert_eq!(&first[..4], b"abcd");
398
399        // The rest arrives while the reader is blocked on it.
400        std::thread::spawn({
401            let path = fx.path.clone();
402            let written = fx.written.clone();
403            move || {
404                std::thread::sleep(Duration::from_millis(20));
405                let mut f = std::fs::OpenOptions::new()
406                    .append(true)
407                    .open(&path)
408                    .unwrap();
409                f.write_all(b"efghij").unwrap();
410                f.flush().unwrap();
411                written.advance(6);
412            }
413        });
414
415        let mut rest = [0u8; 8];
416        let n = src.read(&mut rest).unwrap();
417        assert_eq!(&rest[..n], b"efghij");
418    }
419
420    #[test]
421    fn seeks_freely_below_the_write_head() {
422        let fx = Fixture::new();
423        fx.push(b"0123456789");
424        let mut src = fx.source(1_000_000);
425
426        assert_eq!(src.seek(SeekFrom::Start(5)).unwrap(), 5);
427        let mut out = [0u8; 3];
428        src.read_exact(&mut out).unwrap();
429        assert_eq!(&out, b"567");
430
431        // Backwards, into bytes already read — no re-download, no buffer.
432        assert_eq!(src.seek(SeekFrom::Start(1)).unwrap(), 1);
433        src.read_exact(&mut out).unwrap();
434        assert_eq!(&out, b"123");
435
436        assert_eq!(src.seek(SeekFrom::Current(-2)).unwrap(), 2);
437    }
438
439    #[test]
440    fn seek_from_end_uses_the_advertised_length() {
441        let fx = Fixture::new();
442        fx.push(b"0123456789");
443        let mut src = fx.source(10);
444
445        assert_eq!(src.seek(SeekFrom::End(0)).unwrap(), 10);
446        assert_eq!(src.seek(SeekFrom::End(-3)).unwrap(), 7);
447
448        let mut out = [0u8; 3];
449        src.read_exact(&mut out).unwrap();
450        assert_eq!(&out, b"789");
451    }
452
453    #[test]
454    fn a_lengthless_source_ends_where_the_download_does() {
455        // The end has to agree with `byte_len`. FLAC seeks by bisecting
456        // between its first frame and `SeekFrom::End(0)`, so answering with the
457        // advertised total aims the search at bytes that are not on disk yet:
458        // every probe of the range waits at the write head, and a seek into a
459        // half-downloaded track spends the stall limit before failing.
460        let fx = Fixture::new();
461        fx.push(b"0123456789");
462
463        let mut src = PartialFileSource::open(
464            &fx.path,
465            fx.written.clone(),
466            1_000,
467            fx.status_fn(),
468            ProbeMode::Lengthless,
469        )
470        .unwrap();
471        assert_eq!(src.byte_len(), None);
472        assert_eq!(src.seek(SeekFrom::End(0)).unwrap(), 10);
473
474        // Stating a length means answering for the whole of it.
475        let mut src = PartialFileSource::open(
476            &fx.path,
477            fx.written.clone(),
478            1_000,
479            fx.status_fn(),
480            ProbeMode::Full,
481        )
482        .unwrap();
483        assert_eq!(src.seek(SeekFrom::End(0)).unwrap(), 1_000);
484    }
485
486    /// Ogg's half of the same question, and the opposite answer.
487    ///
488    /// Handed the write head as the end, Ogg reports a stream that is already
489    /// over: a large Opus file downloading opened, said nought milliseconds,
490    /// and the decode thread finished it and moved on, over and over, so it
491    /// never played at all.
492    #[test]
493    fn an_ogg_source_still_ends_at_the_whole_file() {
494        let fx = Fixture::new();
495        fx.push(b"0123456789");
496        let mut src = PartialFileSource::open(
497            &fx.path,
498            fx.written.clone(),
499            1_000,
500            fx.status_fn(),
501            ProbeMode::LengthlessWholeEnd,
502        )
503        .unwrap();
504        // Still no length: the point of opening this way is that Ogg does not
505        // go looking for a tail that has not arrived.
506        assert_eq!(src.byte_len(), None);
507        // But the end it is told about is the file's, not the download's.
508        assert_eq!(src.seek(SeekFrom::End(0)).unwrap(), 1_000);
509    }
510
511    #[test]
512    fn a_landed_download_ends_at_the_whole_file() {
513        // The bound moves with the transfer, so it stops bounding anything
514        // once every byte is there.
515        let fx = Fixture::new();
516        fx.push(b"0123456789");
517        fx.set(COMPLETE);
518
519        let mut src = PartialFileSource::open(
520            &fx.path,
521            fx.written.clone(),
522            10,
523            fx.status_fn(),
524            ProbeMode::Lengthless,
525        )
526        .unwrap();
527        assert_eq!(src.seek(SeekFrom::End(0)).unwrap(), 10);
528    }
529
530    #[test]
531    fn seek_before_start_errors() {
532        let fx = Fixture::new();
533        fx.push(b"hello");
534        assert!(fx.source(5).seek(SeekFrom::Current(-1)).is_err());
535    }
536
537    #[test]
538    fn failed_download_errors_instead_of_reporting_eof() {
539        let fx = Fixture::new();
540        fx.push(b"partial");
541        fx.set(FAILED);
542        let mut src = fx.source(1000);
543
544        let mut out = [0u8; 7];
545        src.read_exact(&mut out).unwrap();
546        assert_eq!(&out, b"partial");
547
548        // Past the written bytes: an error, never a clean EOF — Ok(0) here
549        // would end the track early and look like a short file.
550        assert_eq!(
551            src.read(&mut out).unwrap_err().kind(),
552            io::ErrorKind::BrokenPipe
553        );
554    }
555
556    #[test]
557    fn failure_wakes_a_blocked_reader() {
558        let fx = Fixture::new();
559        let mut src = fx.source(1000);
560
561        let (status, written) = (fx.status.clone(), fx.written.clone());
562        std::thread::spawn(move || {
563            std::thread::sleep(Duration::from_millis(20));
564            status.store(FAILED, Ordering::Release);
565            written.done();
566        });
567
568        let mut out = [0u8; 8];
569        assert_eq!(
570            src.read(&mut out).unwrap_err().kind(),
571            io::ErrorKind::BrokenPipe
572        );
573    }
574
575    #[test]
576    fn a_probe_reads_only_what_has_arrived() {
577        // The first attempt must fail at the write head rather than wait: a
578        // container that would go looking for its tail has to be detected, not
579        // waited for.
580        let fx = Fixture::new();
581        fx.push(b"0123456789");
582        let mut src = PartialFileSource::open_for_probe(
583            &fx.path,
584            fx.written.clone(),
585            1_000,
586            fx.status_fn(),
587            ProbeMode::Full,
588        )
589        .unwrap();
590
591        let mut out = [0u8; 10];
592        src.read_exact(&mut out).unwrap();
593        assert_eq!(
594            src.read(&mut out).unwrap_err().kind(),
595            io::ErrorKind::UnexpectedEof,
596            "past the write head is an answer, not a wait"
597        );
598    }
599
600    #[test]
601    fn playback_reads_wait_for_what_has_not_arrived() {
602        // And the second attempt does wait, which is what lets a container
603        // whose audio starts further in than the streaming threshold — a FLAC
604        // with a large padding block, most of them — be opened at all.
605        let fx = Fixture::new();
606        fx.push(b"0123456789");
607        let mut src = PartialFileSource::open(
608            &fx.path,
609            fx.written.clone(),
610            1_000,
611            fx.status_fn(),
612            ProbeMode::Lengthless,
613        )
614        .unwrap();
615
616        let mut out = [0u8; 10];
617        src.read_exact(&mut out).unwrap();
618
619        std::thread::spawn({
620            let path = fx.path.clone();
621            let written = fx.written.clone();
622            move || {
623                std::thread::sleep(Duration::from_millis(20));
624                let mut f = std::fs::OpenOptions::new()
625                    .append(true)
626                    .open(&path)
627                    .unwrap();
628                f.write_all(b"abcde").unwrap();
629                f.flush().unwrap();
630                written.advance(5);
631            }
632        });
633
634        let n = src.read(&mut out).unwrap();
635        assert_eq!(&out[..n], b"abcde", "it waited rather than giving up");
636    }
637
638    #[test]
639    fn stalled_download_times_out() {
640        // A download with a Content-Length that never arrives: the read must
641        // give up rather than park the decode thread forever.
642        let fx = Fixture::new();
643        let mut src = fx.source_with_stall(1000, Duration::from_millis(20));
644        let mut out = [0u8; 8];
645        assert_eq!(
646            src.read(&mut out).unwrap_err().kind(),
647            io::ErrorKind::TimedOut
648        );
649    }
650
651    #[test]
652    fn completion_ends_the_read_at_the_true_length() {
653        // A chunked transfer reports no total; completion is what says the file
654        // is whole, and its length on disk is what is readable.
655        let fx = Fixture::new();
656        fx.push(b"chunked");
657        fx.set(COMPLETE);
658
659        let mut out = Vec::new();
660        fx.source(0).read_to_end(&mut out).unwrap();
661        assert_eq!(out, b"chunked");
662    }
663
664    #[test]
665    fn survives_the_part_file_being_renamed() {
666        // The download's final act is a rename. A reader that already has the
667        // file open must not notice.
668        let fx = Fixture::new();
669        fx.push(b"0123456789");
670        let mut src = fx.source(10);
671
672        let mut out = [0u8; 4];
673        src.read_exact(&mut out).unwrap();
674        assert_eq!(&out, b"0123");
675
676        std::fs::rename(&fx.path, fx.path.with_extension("")).unwrap();
677        fx.set(COMPLETE);
678
679        let mut rest = Vec::new();
680        src.read_to_end(&mut rest).unwrap();
681        assert_eq!(rest, b"456789");
682    }
683
684    #[test]
685    fn byte_len_is_the_advertised_length_only() {
686        let fx = Fixture::new();
687        assert_eq!(fx.source(42).byte_len(), Some(42));
688        // No Content-Length: the length is genuinely unknown, and claiming one
689        // would have Symphonia compute a duration from it.
690        assert_eq!(fx.source(0).byte_len(), None);
691    }
692
693    #[test]
694    fn is_seekable_true() {
695        let fx = Fixture::new();
696        assert!(fx.source(0).is_seekable());
697    }
698}