seek/
seek.rs

1use std::error::Error;
2use std::num::NonZeroUsize;
3use std::time::Duration;
4
5use icy_metadata::{IcyHeaders, IcyMetadataReader, RequestIcyMetadata};
6use rodio::source::SeekError;
7use stream_download::http::HttpStream;
8use stream_download::http::reqwest::Client;
9use stream_download::source::DecodeError;
10use stream_download::storage::bounded::BoundedStorageProvider;
11use stream_download::storage::memory::MemoryStorageProvider;
12use stream_download::{Settings, StreamDownload};
13
14#[tokio::main]
15async fn main() -> Result<(), Box<dyn Error>> {
16    let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?;
17    let sink = rodio::Sink::connect_new(stream_handle.mixer());
18
19    // We need to add a header to tell the Icecast server that we can parse the metadata embedded
20    // within the stream itself.
21    let client = Client::builder().request_icy_metadata().build()?;
22
23    let stream =
24        HttpStream::new(client, "https://ice6.somafm.com/insound-128-mp3".parse()?).await?;
25
26    let icy_headers = IcyHeaders::parse_from_headers(stream.headers());
27    println!("Icecast headers: {icy_headers:#?}\n");
28    println!("content type={:?}\n", stream.content_type());
29
30    // buffer 5 seconds of audio
31    // bitrate (in kilobits) / bits per byte * bytes per kilobyte * 5 seconds
32    let prefetch_bytes = icy_headers.bitrate().unwrap() / 8 * 1024 * 5;
33
34    let reader = match StreamDownload::from_stream(
35        stream,
36        // use bounded storage to keep the underlying size from growing indefinitely
37        BoundedStorageProvider::new(
38            MemoryStorageProvider,
39            // be liberal with the buffer size, you need to make sure it holds enough space to
40            // prevent any out-of-bounds reads
41            NonZeroUsize::new(512 * 1024).unwrap(),
42        ),
43        Settings::default().prefetch_bytes(prefetch_bytes as u64),
44    )
45    .await
46    {
47        Ok(reader) => reader,
48        Err(e) => return Err(e.decode_error().await)?,
49    };
50    sink.append(
51        rodio::Decoder::builder()
52            .with_seekable(true)
53            .with_data(IcyMetadataReader::new(
54                reader,
55                // Since we requested icy metadata, the metadata interval header should be present
56                // in the response. This will allow us to parse the metadata within
57                // the stream
58                icy_headers.metadata_interval(),
59                // Print the stream metadata whenever we receive new values
60                |metadata| println!("{metadata:#?}\n"),
61            ))
62            .build()?,
63    );
64
65    let handle = tokio::task::spawn_blocking(move || {
66        println!("seeking in 5 seconds\n");
67        std::thread::sleep(Duration::from_secs(5));
68        println!("seeking to beginning\n");
69        sink.try_seek(Duration::from_secs(0))?;
70        std::thread::sleep(Duration::from_secs(5));
71        println!("seeking to 10 seconds\n");
72        sink.try_seek(Duration::from_secs(10))?;
73        sink.sleep_until_end();
74        Ok::<_, SeekError>(())
75    });
76    handle.await??;
77    Ok(())
78}