Skip to content
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

[Question] Build url #230

Closed
GioF71 opened this issue Jan 31, 2024 · 58 comments
Closed

[Question] Build url #230

GioF71 opened this issue Jan 31, 2024 · 58 comments

Comments

@GioF71
Copy link
Contributor

GioF71 commented Jan 31, 2024

Hello, I am upgrading the Tidal plugin for upmpdcli to the latest version 0.7.4 of your library.
Authentication mostly works, the navigation works perfectly just like with oauth2, but playback to the renderer fails.
My renderer is another instance of upmpdcli with mpd of course.

I don't know if this is related to this issue of yours.

What I am currently doing is trying to decode the mpd (not MusicPD this time) manifest, and I get a list of url. But none of those actually play, neither in MPD or in any other player I tried, including VLC.

AFAIK MPD should be able to play MPEG-DASH (see here) so I am probably doing something wrong.

Do you have any suggestion for this issue?

Thank you

@tehkillerbee
Copy link
Collaborator

I have not yet looked into this but from my limited understanding, it is necessary to parse the MPEG-DASH manifest to create a playback URL suitable for your player. However, some players (ffmpeg) should be able to play directly.

Have you tried a different player?

https://stackoverflow.com/questions/47056177/how-to-play-mpeg-dash-audio-streams-from-console

@GioF71
Copy link
Contributor Author

GioF71 commented Jan 31, 2024

No I only tried MPD and VLC, but I will try it asap.
But how do you create that url which ends in .mpd?
Thanks a lot

@tehkillerbee
Copy link
Collaborator

tehkillerbee commented Jan 31, 2024

I did just have success with this simple example, using the tidalapi.Stream as the input and then getting the URLs from the manifest.

    try:
        manifest = json.loads(base64.b64decode(stream.manifest))
    except json.decoder.JSONDecodeError:
        #return f'data:application/dash+xml;charset=utf-8;base64,{stream.manifest}'
        return base64.b64decode(stream.manifest).decode('utf-8')
    return manifest["urls"][0]

However, only one URL is returned and it's quality is not HI_RES so not sure whats causing that.

EDIT: The above returned a single URL simply because my loaded session was not a PKCE session after all.

Added some more details. My previous example worked simply because the manifest did not contain any dash+xml (i.e. the mpd file) media. So the above example should handle both cases. Parsing the manifest with python mpegdash also appears to work as expected.

@GioF71
Copy link
Contributor Author

GioF71 commented Jan 31, 2024

Hello, thank you again, but it seems to me that s.manifest once b64decoded is xml, not json, am I doing something wrong?

@tehkillerbee
Copy link
Collaborator

The output should be an MPD string with an XML declaration header? That is what I got.

@GioF71
Copy link
Contributor Author

GioF71 commented Jan 31, 2024

Hello, this is the code I am trying. It is derived from the simple pkce login file.
I have put your code in a get_url method, but it goes in the "except" and does not return a URL.
Can you take a look?
Thank you!

import tidalapi
from tidalapi import Quality
from pathlib import Path
import os
import json
import base64

from mpegdash.parser import MPEGDASHParser

if os.path.exists("tidal-session-pkce.json"):
    print("file exists")
else:
    print("file does not exist")

def get_url(stream):
    try:
        manifest = json.loads(base64.b64decode(stream.manifest))
    except json.decoder.JSONDecodeError:
        return f'data:application/dash+xml;base64,{base64.b64decode(stream.manifest)}'
    return manifest["urls"][0]

session_file1 = Path("tidal-session-pkce.json")

session = tidalapi.Session()
# Load session from file; create a new session if necessary
session.login_session_file(session_file1, do_pkce=True)

# Override the required playback quality, if necessary
# Note: Set the quality according to your subscription.
# Low: Quality.low_96k
# Normal: Quality.low_320k
# HiFi: Quality.high_lossless
# HiFi+ Quality.hi_res_lossless
session.audio_quality = Quality.hi_res_lossless.value

album = session.album("110827651") # Let's Rock // The Black Keys
tracks = album.tracks()
# list album tracks
for track in tracks:
    print(track.name)
    # MPEG-DASH Stream is only supported when HiRes mode is used!
    s = track.get_stream()
    url = get_url(s)
    print(f"URL=[{url}]")

@GioF71
Copy link
Contributor Author

GioF71 commented Jan 31, 2024

Of course I have the json file with the credentials, so the login_session_file has success

@tehkillerbee
Copy link
Collaborator

tehkillerbee commented Jan 31, 2024

Sorry, I edited my earlier example a while back.

You should change your get_url function to be the following

def get_url(stream):
    try:
        manifest = json.loads(base64.b64decode(stream.manifest))
    except json.decoder.JSONDecodeError:
        return base64.b64decode(stream.manifest).decode('utf-8')
    return manifest["urls"][0]

FYI, I am already working on extending the Stream class with this functionality and adding MPEG-DASH parsing.

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 1, 2024

Hello, thank you very much, this code works.
The upmpdcli author tells me that I should serve the mpd url to MusicPD, but I don't know how to get that URL. Is that available through the library?

@tehkillerbee
Copy link
Collaborator

tehkillerbee commented Feb 1, 2024

You should be able to feed music player daemon the manifest file for MPEG DASH streaming, i.e. the MPD XML, if your player supports it. Have you tried saving it as an mpd file?

This should also work if your ffmpeg installation has been built with the correct support:

ffmpeg -i tidal.mpd

If MPD expects a stream, you need to parse it further, eg to HLS format so it can be passed as an m3u8 file stream to your player. I got the HLS parsing working yesterday, based on some code snippets online, but it looks like there is a python library that can take care of it as well:

https://github.com/hyugogirubato/pydash2hls/tree/main

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 1, 2024

Hello, I will try saving the mpd to a file and play it, anyway Music Player Daemon expects a stream, I cannot assume it to be on the same host as the upmpdcli instance or that it can anyway access the file

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 1, 2024

Hello, I have successfully played the saved mpd file with ffmpeg, which is good!

However I couldn't generate the stream using the library. It fails when I load the xml manifest. Do you have a snippet of this conversion?

@tehkillerbee
Copy link
Collaborator

tehkillerbee commented Feb 3, 2024

@GioF71 Good to hear you had success.

Sorry, I have not had time to look at it yet due to work. But I will try to get my conversion code added on a separate tidalapi branch so you can test with it.

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 3, 2024

Hello @tehkillerbee thank you. The success is partial but it seems to be showing that it might be possible to stream hires also.
Thank you in advance for any help.
I am also looking forward to see hires working on your mopidy-tidal as well, that would be nice Tidal Connect alternative!

@tehkillerbee
Copy link
Collaborator

Yes, what I have now is a M3U8 File for the HLS Stream. It can be loaded fine using ffmpeg (mp4 container with flac) and the bitrate is indeed 192khz. I would assume this HLS media playlist should work for you.

Ill clean up my code asap and get you a branch you can try to test with. I still need to figure out how to integrate all this with Mopidy :)

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 5, 2024

Hello, thank you for sharing your progress. I look forward to enable the tidal plugin for upmpdcli to work with hires files!

@tehkillerbee
Copy link
Collaborator

tehkillerbee commented Feb 6, 2024

@GioF71 I have added my latest changes in this branch. This is still a work in progress but I think that is enough to get you started: python-tidal

For mopidy-tidal, I decided to create a local HLS m3u8 playlist (file://xx) and pass it to mopidy instead of the file URL. Everything works as expected so far when testing. Ofc. it requires the correct GStreamer plugins, i.e. sudo apt-get install gstreamer1.0-plugins-bad. It ain't pretty, as it is also still a WIP: mopidy-tidal

Of course there is still the challenge of generating the pkce enabled login file, without copying it in manually as I have done now.

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 7, 2024

Hello, thank you. I tried the pkce_login.py, but the resulting files are all empty. Am I doing something wrong?

@tehkillerbee
Copy link
Collaborator

@GioF71 Same album as in the example? I will check it later today and get back to you. I did some last minute changes that perhaps broke the example code...

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 7, 2024

Hello, yes, I did not change anything other than the path of my credentials file!
Thank you!

@tehkillerbee
Copy link
Collaborator

@GioF71 Looks like the token refresh code was broken (I did get the same issue when the pkce session was somehow not valid anymore and I got a non MPEG-DASH stream). So this is the reason you get no playlist - there is no MPEG-DASH MPD available.

I have fixed this on the before mentioned branch. Maybe you'll need to login again to make sure the correct access token is used.

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 7, 2024

ok I have tried again, now the playlist are generated.
However, both mpd and ffplay report errors:

MPD:

ffmpeg/mov,mp4,m4a,3gp,3g2,mj2: could not find corresponding trex (id 1)
ffmpeg/mov,mp4,m4a,3gp,3g2,mj2: could not find corresponding track id 0
ffmpeg/mov,mp4,m4a,3gp,3g2,mj2: trun track id unknown, no tfhd was found
ffmpeg/mov,mp4,m4a,3gp,3g2,mj2: error reading header
exception: Failed to decode https://sp-ad-fa.audio.tidal.com/mediatracks/some_chars/10.mp4?token=some_chars; avformat_open_input() failed: Invalid data found when processing input

ffplay:

[mov,mp4,m4a,3gp,3g2,mj2 @ 0xd6500630] could not find corresponding trex (id 1)
[mov,mp4,m4a,3gp,3g2,mj2 @ 0xd6500630] could not find corresponding track id 0
[mov,mp4,m4a,3gp,3g2,mj2 @ 0xd6500630] trun track id unknown, no tfhd was found
[mov,mp4,m4a,3gp,3g2,mj2 @ 0xd6500630] error reading header
https://sp-ad-fa.audio.tidal.com/mediatracks/some_chars/10.mp4?token=some_chars: Invalid data found when processing input

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 7, 2024

If I try the playlist with ffplay, I get:

[hls @ 0xd6400630] Skip ('#EXT-X-VERSION:3')  0KB sq=    0B f=0/0
[hls @ 0xd6400630] Opening 'https://sp-ad-fa.audio.tidal.com/mediatracks/some_chars/0.mp4?token=some_chars' for reading
[https @ 0xd641d5a0] Protocol 'https' not on whitelist 'file,crypto,data'!
[hls @ 0xd6400630] Failed to open segment 0 of playlist 0

How do you play those playlists?

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 7, 2024

tried to install but there is no difference :-(

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 7, 2024

HA, I added -protocol_whitelist file,http,https,tcp,tls,crypto to ffplay command line, and now it plays. Is any of your selected tracks 24/192?

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 7, 2024

set ffplay to play with alsa and it plays @192 kHz, that's really good progress. No clue how to convince mpd to play though
Thank you!

@tehkillerbee
Copy link
Collaborator

Yep, that must be added as well for that error to go away. Yep 192kHz/24bit playback works for me.

Input #0, hls, from 'dash_77646169_77646173.m3u8':sq=    0B f=0/0   
  Duration: 00:05:41.55, start: 0.000000, bitrate: 0 kb/s
...
  Stream #0:0: Audio: flac (fLaC / 0x43614C66), 192000 Hz, stereo, s32 (24 bit), 4645 kb/s (default)
    Metadata
    ....

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 7, 2024

For playback to mpd, the author of upmpdcli told me I had to pass the url of the "MPD" (not music player daemon). Infact that played in ffmpeg. Is there a way to get that as a URL from your library?

@tehkillerbee
Copy link
Collaborator

tehkillerbee commented Feb 7, 2024

Problem is you do not get an actual URL in the response from Tidal. In the response (json), you get a manifest that must be decoded to get you the MPD (MPEG-DASH) XML. From that, the HLS media playlist can be constructed for playback. Some players can process this string directly, it seems.

But I assume MPD (the daemon) can work with playlists too? So perhaps you can just generate a local playlist and pass that instead? That is what I did with mopidy and that worked just fine.

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 7, 2024

The problem is that I cannot write anything to mpd from upmpdcli (as a media server).
I might ask the author if I can serve a playlist.
But even if this is possible, the problem would then be that for one song I would have a playlist (so multiple tracks). This breaks every assumption upmpdcli (as a renderer) is based on.

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 7, 2024

Anyway mpd does not decode the individual URLs anyway, so at the moment this is the first hurdle

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 7, 2024

When I use BubbleUPnP own "Local and Cloud" with Tidal, the resulting URL for mpd is a plain flac. These is some processing on the BubbleUPnP app and/or BubbleUPnP server, that makes this possible. And of course the mpd+upmpdcli (renderer) stack in this case plays the track without any issue. It just 'sees' a regular flac over http.
I wonder if this is something I can implement in the tidal plugin on upmpdcli.

@tehkillerbee
Copy link
Collaborator

When I use BubbleUPnP own "Local and Cloud" with Tidal, the resulting URL for mpd is a plain flac.

I assume BubbleUPnP only works for lower qualities - not HiRes?

The tidalapi plugin can already give you a direct flac URL for playback, but only for <HiRes. For HiRes, only MPEG-DASH stream is available. If you try calling .get_url() when configured for HiRes playback, you will not get any URL. But when using low/high/lossless, you will get MP4 or FLAC (non HiRes of course).

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 7, 2024

When I use BubbleUPnP own "Local and Cloud" with Tidal, the resulting URL for mpd is a plain flac.

I assume BubbleUPnP only works for lower qualities - not HiRes?

It works for hires since november of last year, see here

The tidalapi plugin can already give you a direct flac URL for playback, but only for <HiRes. For HiRes, only MPEG-DASH stream is available. If you try calling .get_url() when configured for HiRes playback, you will not get any URL. But when using low/high/lossless, you will get MP4 or FLAC (non HiRes of course).

An alternative options come to my mind, I could leverage those urls, cache joined files, and serve them, that should work, what do you think?

@tehkillerbee
Copy link
Collaborator

cache joined files

I would expect this requires buffering before playback can be started so maybe that's not ideal. But I don't see why not. I think that is the same way it's done with some of the tidal downloader tools so perhaps you could take a look at them.

Or perhaps look at gapless upnp? https://forum.wiimhome.com/threads/gapless-mode-setnexturi-issues-for-upnp-interface.478/

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 8, 2024

MPD already tries to acquire the next song to play in advance, iirc 20 seconds. So it can play gaplessly.
But for this to work properly, I would need to build the combined files in time, that might be challenging for long tracks.

@tehkillerbee
Copy link
Collaborator

What I mean is you already have a list of URLs for each track so perhaps they could be queued for (gapless) playback. Then you avoid buffering the whole track.

Out of curiosity, how does MPD/upmpdcli deal with live radio m3u8 (HLS) playlists with multiple segments? Surely they can be played back with MPD without any issues?

It is the same principle here used by the playlist presented from tidalapi. It consists of segments, not individual tracks, so it should be handled as such by MPD (daemon), exactly as when playing back a radio stream. I am surprised if this is not possible to do with MPD, perhaps ask the developer for advice?

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 8, 2024

Hello, I have tried 'serving' the m3u8 file via http, and VLC plays the whole track without a problem.
However, when I try to open the url using mpd, multiple items are enqueued instead of one, and anyway they won't play, because of a "unsupported uri scheme error".

MPD has a playlist plugin here but m3u8 does not seem to be among the supported formats.

Maybe that could be the issue, because if I try to add the first url in VLC, it doesn't work as well.
Is there a chance to support any of the playlist formats that mpd supports?

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 8, 2024

Sorry I was wrong, the m3u8 files work perfectly. I was using Cantata to add the playlist, but it was probably doing something instead of feeding the m3u8 directly to mpd. I tried with mpc and it worked!

pi@pi-dac-player:~ $ MPD_PORT=6601 mpc add http://192.168.1.173:8299/dash_77646169_77646170.m3u8
pi@pi-dac-player:~ $ MPD_PORT=6601 mpc play
http://192.168.1.173:8299/dash_77646169_77646170.m3u8
[playing] #1/1   0:00/0:00 (0%)
volume:100%   repeat: off   random: off   single: off   consume: off
pi@pi-dac-player:~ $ 

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 8, 2024

Hello, looks like the latest version on the branch you prepared fails with this error:

77646170: 'The Golden Age' by 'Beck'
MimeType:application/dash+xml
Traceback (most recent call last):
  File "/home/power/git/audio/python-tidal/examples/pkce_login.py", line 53, in <module>
    stream.bit_depth,
AttributeError: 'Stream' object has no attribute 'bit_depth'

I am using the version from a few commits before (c03864a, says "Added link to documentation" in the message, and this works.
Am I wrong? Thank you!

BTW I currently have one dev version of upmpdcli which works perfectly. I am not providing metadata like sample rate and bitrate, but it plays just fine. Also it works on Logitech Media Server

@tehkillerbee
Copy link
Collaborator

tehkillerbee commented Feb 8, 2024

@GioF71 Great to hear you got it working!

Hmm I cannot replicate this. Are you sure you are not using an older version of tidalapi but with the latest script?

I added the metadata in cf42bc so the error hints that an older version is used.

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 8, 2024

Yes it's definitely possibile... I double checked but I must have done something wrong.
Anyway, is a release coming soon? :-)

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 8, 2024

I confirm I was using a previous version in my python environment, this is solved now, thank you again

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 8, 2024

Another thing, the upmpdcli author asks me if we can try and save (then serve) the mpd file instead of the m3u8 files, does it sound possible to you? Thank you again!

@tehkillerbee
Copy link
Collaborator

tehkillerbee commented Feb 8, 2024

@GioF71 Sure, it's already possible.

stream = track.get_stream()
manifest = stream.get_stream_manifest()
data = manifest.get_manifest_data()
with open("{}_{}.mpd".format(album_id, track.id), "w") as my_file:
    my_file.write(data)

But I will probably move the get_stream_manifest function into the Stream class instead since that makes more sense. So eventually, this would be a way to get the mpd file:

stream = track.get_stream()
data = stream.get_manifest_data()
with open("{}_{}.mpd".format(album_id, track.id), "w") as my_file:
    my_file.write(data)

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 9, 2024

Hello, thank you. I tried serving the mpd file instead of hls, and mpd (the player) works much better, allowing to seek the track, which did not work when using hls. I also see the bitdepth, sample rate and even bitrate (this is probably calculated by mpd because I did not provide it yet).

On the other hand, now Logitech Media Server refuses to play the stream. I can't find any log there. I see on the gui that the type is "application/octet-stream".
Do I need to set a different mimetype when serving the mpd?

@tehkillerbee
Copy link
Collaborator

tehkillerbee commented Feb 10, 2024

Good to hear. Yes I switched to using the mpd manifest directly, gstreamer doesn't mind and it works well in mopidy-tidal.

Regarding logitech media server, I am not sure. A possible explanation could be that the mimetype is not dash+xml for that album, resulting in you passing on a non mpd manifest to LMS. I added an extra check to mopidy-tidal to avoid that problem, in case the user is using oauth authentication.

What type of authentication are you using? Pkce?

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 10, 2024

Yes, PKCE of course. With oauth2, LMS works.
I also tried a WiiM pro as a UPnP renderer, and it's the same: it works with oauth2, not with pkce

@tehkillerbee
Copy link
Collaborator

@GioF71 Odd. Since it works with oauth2, it must be related to the MPD manifest. Have you tried stripping the XML header before passing on the manifest?

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 13, 2024

I thought it was related to the ability to play a mpeg-dash stream... but I didn't try to strip xml header. But do you mean to remove just the following part?

<?xml version='1.0' encoding='UTF-8'?>

@tehkillerbee
Copy link
Collaborator

It could very well be that is the issue, as I do not think all players support MPD manifests. But that doesn't explain where application/octet-stream comes from, that must be after parsing. Perhaps the container used in the MPD manifest is not supported. Can you see what codec is used (flac/m4a) for playback?

Yes, you could try removing it as a test, although I am not sure it will have any effect.

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 21, 2024

Hello, I didn't try with the stripped xml header yet...

Instead I have been busy updating the upmpdcli plugin for hires support.
I have prepared a preview docker image and described the installation process here.

A few things are missing before creating a release:

  1. Add cleanup routine for temporary mpd files older than a certain time delta (I believe 1h might be a good default)
  2. Update upmpdcli documentation
  3. Use a tagged version of your library

However it is working quite well for me since a few days now, I am looking forward to hear if anybody else wants to try this.

Thank you again @tehkillerbee for your great work!

@tehkillerbee
Copy link
Collaborator

@GioF71 Sounds great. Good to hear you have made progress so far.

  1. Regarding temporary mpd files - why do you save more than one file at a time? For caching purposes?
  2. Naturally :)
  3. An official PyPi release is imminent, when testing has completed. It is very helpful that you have been testing it as well so we can iron out any bugs that I may have introduced.

How did you handle PKCE login?

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 27, 2024

About question one: the plugin receives a request for the url of the next track a few seconds before it actually gets played. To that request I must answer with a mimetype and a url which can offer the mpd file, so I need to temporarily store the file. No caching, it's just a two-step process.

About pkce login, I have prepared python file which shows the challenge message, and, if successful, will displays a generated json file as well as a set of env variables for the container.
If the user chooses the former, he just need to put the file in the correct location.
If the user chooses the latter, he just need to set those variables (same contents of the json file).

In the second case, the json file is created anyway and subsequently used.

The repo I have linked should make things even easier. The get-credential script will automatically place the file in the correct location, by running a docker run command using a mountpoint to the same directory that is used by the docker-compose file.

Now the challenge for me is to document the process so it can be easily understandable.

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 28, 2024

About the file storing: I need to save the file because the http server is not controlled by the plugin, so I cannot create the file in real time.

@GioF71
Copy link
Contributor Author

GioF71 commented Feb 28, 2024

Hello again, can you confirm that getting the stream information (bit_depth, sample_rate etc) are requests that hit the playwall api? I get http 429 errors if I try to get these metadata too frequently.
By too frequently, I mean that this happens when I just try to show an album with many tracks, let's say 40 is a critical number from my observations. Sometimes even less, there must be a max number of request in a given unit of time.
Take this album for example: https://listen.tidal.com/album/112926479

Or am I doing something wrong?

Thank you

@tehkillerbee
Copy link
Collaborator

See my answer here: #233 as this is not really related to this issue.

@tehkillerbee
Copy link
Collaborator

Closing this issue now, as I think we got all the issues sorted. Feel free to reopen if necessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants