aschey / stream-download-rs

A Rust library for streaming media content from a remote location
https://crates.io/crates/stream-download
Apache License 2.0
31 stars 8 forks source link

Receiving stream header updates #58

Closed yuvadm closed 3 months ago

yuvadm commented 3 months ago

Possibly a dupe of #18 - is is possible to receive updates when stream headers change?

e.g. in media streams I'd like to poll stream.header("icy-title") for the currently playing song, and notice when it changes.

aschey commented 3 months ago

Hey @yuvadm, thanks for your issue. Yeah, providing a callback interface to query the stream info periodcally should be pretty easy. I've been meaning to add that for download progress. I can include a reference to the stream in there as well. Just curious though, do you have an example of a stream that uses icy-title? I don't know much about Icecast, but I don't see a reference to that header in this document and my understanding was that you need to parse the current song info from the stream itself rather than the headers. The termusic project has an example of that here. Let me know if there's a stream I can test that uses icy-title and I can see if it works.

yuvadm commented 3 months ago

I believe SomaFM streams use icy-title (although not entirely sure), maybe try e.g. https://ice5.somafm.com/groovesalad-128-mp3 ?

aschey commented 3 months ago

I don't believe so. It has icy-name which has the stream name, but not the song title.

❯ http GET https://ice5.somafm.com/groovesalad-128-mp3 Icy-MetaData:1
HTTP/1.0 200 OK
Access-Control-Allow-Headers: Origin, Accept, X-Requested-With, Content-Type, Icy-MetaData
Access-Control-Allow-Methods: GET, OPTIONS, SOURCE, PUT, HEAD, STATS
Access-Control-Allow-Origin: *
Cache-Control: no-cache, no-store
Connection: Close
Content-Type: audio/mpeg
Date: Fri, 31 May 2024 18:15:40 GMT
Expires: Mon, 26 Jul 1997 05:00:00 GMT
Server: Icecast 2.4.0-kh15
icy-br: 128
icy-genre: Ambient Chill
icy-metaint: 16000
icy-name: Groove Salad: a nicely chilled plate of ambient beats and grooves. [SomaFM]
icy-notice1: <BR>This stream requires <a href="http://www.winamp.com/">Winamp</a><BR>
icy-notice2: SHOUTcast Distributed Network Audio Server/Linux v1.9.5<BR>
icy-pub: 0
icy-url: http://somafm.com
yuvadm commented 3 months ago

Hmm, then I might be misunderstanding something.

I know for sure that this stream updates song titles as they change, how might that be implemented?

Edit: ok this might just be mpeg metadata within the stream, this is how mpv displays it:

$ mpv --msg-level=all=info https://ice5.somafm.com/groovesalad-128-mp3
 (+) Audio --aid=1 (mp3 2ch 44100Hz)
AO: [pipewire] 44100Hz stereo 2ch floatp
File tags:
 icy-title: Normandie - Moon and Sun (Sine Remix)
aschey commented 3 months ago

Take a look at the link I posted in my first comment here. There's Icecast-specific metadata in the stream itself. You can adapt that code to your needs and wrap the stream reader returned by stream-download with something like that to parse the metadata.

Thinking about it some more, it may be nice to add something like this as a storage provider since this is probably a common use case. I'll take a look and see if it makes sense to add that as on optional feature.

yuvadm commented 3 months ago

@aschey interesting suggestion, I tried to take that code and add a impl Seek for it. Something naive like:

impl<T: Read + Seek, F: Fn(&str)> Seek for FilterOutIcyMetadata<T, F> {
    fn seek(&mut self, relative_position: SeekFrom) -> std::io::Result<u64> {
        self.inner.seek(relative_position)
    }
}

doesn't seem to cut it. Any ideas how to easily do that?

aschey commented 3 months ago

If you don't need to support seeking (and you probably don't for a radio stream), you can wrap it in a ReadOnlySource like they do here. If you do need seek to work for some reason, you would probably need to clear the state of remaining_bytes on seek and restart it on the next read.

yuvadm commented 3 months ago

huh, weird. I was under the impression that I can't connect a ReadOnlySource - https://github.com/RustAudio/rodio/issues/580

This is what got me to use stream-download-rs in the first place :)

aschey commented 3 months ago

Are you using Rodio with the default features? Try with default-features = false, features - ["symphonia-all"]. The crash in your issue there is because Rodio tries to instantiate a separate decoder for each audio type and seeks back to the beginning if it fails until it finds a valid one. If you only enable Symphonia, it will skip all that and let Symphonia figure out the correct format.

One caveat though, Symphonia requires a field on the source that tells it whether it's seekable or not. Rodio currently hardcodes this to true which means Symphonia may try to perform a seek operation during instantiation if you hit certain cases. So far in my testing, this hasn't come up though. It would probably be good for Rodio to have a custom constructor for Symphonia that allows passing in a different source with is_seekable = false though.

aschey commented 3 months ago

Alright, I've been successfully nerdsniped :smile:. I made an Icecast metadata parser as a separate crate: https://github.com/aschey/icy-metadata. I took a pass at seek support here. Seeking from the end of a stream doesn't make sense conceptually so that will still return an error, but other types of seeks should work. I'll add some examples and publish it once I do some more testing.

yuvadm commented 3 months ago

heh :rofl: glad I managed to pique your interest! Great to see this coming along, I'm still struggling to understand the basics of hooking everything together, but will happily await some properly tested examples in that crate for reference. Thanks!

aschey commented 3 months ago

Understandable, audio streaming is deceptively complicated. Seeking ended up being more difficult than I thought. I have it partially working for seeks less than 2x the metadata interval. I'll publish it once I implement the rest of the logic there. In the meantime, I added an example here if you want to add the repo as a git dependency and give it a try. If you use Rodio like I mentioned above (default-features = false, features - ["symphonia-all"]), it shouldn't trigger any seek requests.

yuvadm commented 3 months ago

@aschey ok this approach definitely does the trick, I'm using the new crate and following your example and things are working! FWIW I don't require seeking on the actual stream, but of course it would be nice to have for more elaborate use cases.

aschey commented 3 months ago

Nice, good to hear. Yeah, I could maybe see a feature where you let people go back one song or something. Crate is now published here https://crates.io/crates/icy-metadata. Seeking should be functional with a few minor limitations, explained here https://docs.rs/icy-metadata/latest/icy_metadata/struct.IcyMetadataReader.html.

aschey commented 3 months ago

I'm gonna go ahead and close this now, feel free to open another issue if anything else comes up.