-
Notifications
You must be signed in to change notification settings - Fork 108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: SiriusXM provider #989
Conversation
return prov | ||
|
||
|
||
# noinspection PyUnusedLocal |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
none of our linters should require this ? what editor are you using for Python ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PyCharm Professional. It showed mass
, instance_id
, action
, values
as unused params w/warnings. I wasn't sure if I could just remove them or should just ignore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You may never remove them but instead you inform the linter that it may be ignored.
We use the linters that are configured in the pre-commit config
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We all use VSCode btw ;-)
audio_format=AudioFormat( | ||
sample_rate=44100, | ||
channels=2, | ||
bit_depth=32, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it is very, very unlikely that the bit_depth is 32 bits (that is only used in recording studios)
I think you meant 16 bits here ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I thought the same thing. I pulled this from VLC media/codec info. I had never seen 32-bit + 44.1KHz in the wild before. Usually it would be like 96KHz or 192KHz when people are going overboard on bit depth.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well, you would hear mangled audio if its set to the wrong bit depth so you will know soon enough :-)
yield await self._client.get_segment(playlist_path) | ||
|
||
async def get_audio_stream(self, stream_details: StreamDetails, seek_position: int = 0) -> AsyncGenerator[bytes, None]: | ||
async for chunk in self.get_playlist_items_generator(stream_details): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is not going to work because as far as I can see the "get_playlist_items_generator" returns audio file URLs and not raw audio bytes... You will have to fetch the segment through the http client and then stream the content.
It could be as simple as this:
from music_assistant.server.helpers.audio import get_http_stream
async for playlist_chunk_url in self.get_playlist_items_generator(stream_details):
async for chunk in get_http_stream(
self.mass, playlist_chunk_url, streamdetails
):
yield chunk
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way the sxm-client works (when you're not relying on their HTTP proxy endpoint) is that you request a playlist for a channel which returns an m3u8 file as a string. Here's a fragment of a random one I grabbed:
#EXTM3U
#EXT-X-VERSION:1
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:104665
#EXT-X-ALLOW-CACHE:NO
#EXT-X-KEY:METHOD=AES-128,URI="key/1"
#EXT-X-PROGRAM-DATE-TIME:2023-12-23T14:02:23.561+00:00
#EXTINF:10,
AAC_Data/cnn/HLS_cnn_256k_v3/cnn_256k_1_122350543561_00104665_v3.aac
#EXT-X-PROGRAM-DATE-TIME:2023-12-23T14:02:33.312+00:00
#EXTINF:10,
AAC_Data/cnn/HLS_cnn_256k_v3/cnn_256k_1_122350553312_00104666_v3.aac
#EXT-X-PROGRAM-DATE-TIME:2023-12-23T14:02:43.064+00:00
#EXTINF:10,
AAC_Data/cnn/HLS_cnn_256k_v3/cnn_256k_1_122350563064_00104667_v3.aac
...etc
While you can get the root URL from the client (a CDN or something), the HTTP requests that you'd make require session tokens and a bunch of other stuff. Their client handles that for you. So you make a request like this:
bytes = await sxm_client.get_segment('AAC_Data/cnn/HLS_cnn_256k_v3/cnn_256k_1_122350543561_00104665_v3.aac')
and you get back the bytes from the http response ... where the request had all of the session-related magic taken care of behind the scenes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah yes, that sounds like exactly what you need then!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I was trying to achieve here is to get music assistant to basically pull the next segment as needed but not try to download them all. Like can I yield the bytes of file 1, then yield the bytes of file 2 when music assistant needs them, etc.? This is what I was trying to do here. Also do you know if I would need to break these segments down into smaller chunks & yield those?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, you just need to make sure to grab each segment, yield chunks and then grab next segment yield chunks and so on. We have some helpers in the audio.py module to help with parsing the m3u file if you want that.
and yes, you need to yield the smaller chunks otherwise it will all be loaded in memory
Basically you just reimplement the "get_segment" function from the lib but instead of returning the resp.content, you just yield chunks from the resp.iter_any or resp.iter_chunked
So something along these lines within the get_audio_stream:
playlist_file = get_sirius_playlist_m3u8_file
playlist_segments = parse_playlist_into_the segment_parts
for playlist_segment in playlist_segments:
url = urllib.parse.urljoin(await self.get_hls_root(), path)
res = await self._session.get(url, params=self._token_params())
if res.status_code == 403:
raise SegmentRetrievalException(
"Received status code 403 on segment, renew session"
)
if res.is_error:
self._log.warn(f"Received status code {res.status_code} on segment")
return None
for chunk in res.iter_any():
yield chunk
You could try first by just using the entire contents from the get_segment function btw, maybe its not that big
It turns out I needed to setup that http endpoint to get the sxm-client ~working. I was running into ffmpeg errors trying to calculate loudness and it looks like those segments I thought I was getting back as aac or m4a were actually hls or something along those lines. I verified with ffprobe that the segment data I was getting back was invalid. With the latest commit, by passing the m3u8 path as
I haven't had any luck with getting One other quick question... There is a callback from the sxm-client where I can be notified when the currently playing track changes. Is there any way I can update the footer of the web UI? I assumed this would be in/around |
Exceptions for tasks can sometimes bubble up in a somewhat strange fashion with asyncio. In your case you can see the error MusicProvider.get_audio_stream' was never awaited which means something crashed. The code was porbbaly still calling the get_audio_stream in your provider Sure that happened too when you had set the "direct" attribute of the streamdetails. if you fill streamdetails with the "direct" attribute it means we're not going to intercept with the stream but just feed the url into ffmpeg directly and let that deal with it. we do that for mpeg dash streams for example and also for local files. You can set the streamdetails.streamtitle if you want for the updated track details from that callback. But those are small finetune details later. First focus on getting playback working. I have no way to test this to know what is going on but its getting harder to follow now that we're also spinning up a webserver to grab the audio bits. I still dont get why we cant just replicate what that webserver is doing and do that directly but for now its maybe good to use this at starting point. |
OK, so I had a quick look and it looks like there's a lot more needed to get this working due to the whole HLS stuff. That is, if you want to feed the audio chunks yourself (in the get_audio_stream) but for now I think its totally fine to use that webserver wrapper for it. Unfortunately I cant test this myself (maybe I can test with a VPN?!) otherwise I could maybe help you trace the issue. From what I can see it should work fine, if that m3u8 playlist is also working in VLC so maybe this is ffmpeg related. |
Sorry for the mega late reply! We had some serious bugs that needed to taken care of. |
hey @scottt732 just wondering if you have made any more progress? |
Here is a different project that I use with my AmpliPi, it's a more recently written interface to SiriusXM, perhaps there's something useful in it that would help with this: https://github.com/vszander/Amplipi/blob/main/python/sxm.py |
Superseded by #1730 |
An attempt at a provider for SiriusXM based on sxm-client (source, docs, example player)
This requires a valid SiriusXM streaming account (https://www.siriusxm.com/plans) and doesn't attempt to get around any restrictions they put in place. You can get 1 stream per account at a time.
Browsing is working, but I'm having trouble getting the audio to play properly. My python is rusty and I'm pretty new to async/await/async generators in python so... go easy on me or feel free to add commits ;-)