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}