calne-ca / subsonic-java-client

A Java Client for the Subsonic API
GNU General Public License v3.0
2 stars 0 forks source link

New URLOfStream method (Problems with stream() method) #1

Closed BigB84 closed 3 years ago

BigB84 commented 3 years ago

Problem: Unable to play InputStream of file with API method, problems with UI binding Solution: (Temp) Write stream to file then play it

Description: I'm trying to make Linux Client based on this API with JavaFX (it's not published yet) I have problems with InputStream returned by method:

net.beardbot.subsonic.client.api.media.MediaService.stream(String id)

When I pass it to the https://github.com/goxr3plus/java-stream-player there are problems with properly transcoded (mp3 or wav) stream doesn't support mark/reset (Fixed by creating BufferedInputStream), music plays, however I'm unable to create bindings with slider etc. because It seems that It doesn't offer such features.

I decided to try with something more core like javax.sound.sampled.Clip I got exception: java.lang.IllegalArgumentException: Audio data < 0 when I try to pass there mentioned InputStream

Finally I managed to play file by wring stream to file (javafx.Media doesn't support Streams). But it's not the point to bypass it. Streaming is much more efficient (e.g. memory: especially when we want lossless files) and better for file security.

Java has some core constraints when it comes to media, like javafx.Media (Which only worked for me) doesn't support InpuStream or Codecs representing libraries are badly documented/has their own limits.

Well, actually we do streaming using http(s) and I checked mentioned stream() method. It creates URL (Line 53).

Do you think that creating net.beardbot.subsonic.client.api.media.MediaService.URLOfStream(String id) is good idea? With it we can pass the URL to the chosen Media Library and let it create proper stream with all functionalities like seeking and UI binding methods.

I'm not really experienced so if you find another solution that's probably my lack of knowledge, then please give me a hint. I've spent hours trying to figure out what's going on, reading tons of docs.

Thanks in advance :)

calne-ca commented 3 years ago

As you mentioned, the stream method should return a BufferedInputStream instead. I'll fix this.

As for providing a non-stream option, I think this is a good idea to also support clients that cannot handle streams. I'll add such a method.

Though I don't believe that you'll be able to seek through an audio input stream anyway since this would require the Subsonic server to support range request, which it doesn't as far as I know. I think usually a client will implement seeking of streams by downloading the file in the background and working with a local, partial file. This way seeking forward simply means waiting for the download to reach a specific point and then seek to the position in the local file. Seeking backwards would only require to seek through the local file without waiting for the download since you've already downloaded this part of the audio stream.

I'll make the mentioned changes and release a new version. I'll comment on this issue as soon as I've released the new version.

BigB84 commented 3 years ago

I think usually a client will implement seeking of streams by downloading the file in the background and working with a local, partial file. This way seeking forward simply means waiting for the download to reach a specific point and then seek to the position in the local file. Seeking backwards would only require to seek through the local file without waiting for the download since you've already downloaded this part of the audio stream.

Right, I'll try to figure out the proper way of implementing this. It may require setting up proxy like said: https://stackoverflow.com/questions/12701249/getting-access-to-media-player-cache/18627606#18627606. Something like this: https://github.com/danikula/AndroidVideoCache

I'll add such a method.

Thanks! :) I was so curious and impatient how it'd work that I quickly wrote extended class that did exactly the same but returns URL. It worked, however MediaPlayer.getTotalDuration() now returns bad value something around 3 and a half hour, independently in each file (of course value got from event, setOnReady()).

I'll make the mentioned changes and release a new version. I'll comment on this issue as soon as I've released the new version.

Thanks! :)

calne-ca commented 3 years ago

I've implemented the changes and the following works now (not merged / released yet):

StreamPlayer streamPlayer = new StreamPlayer();

// Play via InputStream
streamPlayer.open(subsonic.media().stream(songId));
streamPlayer.play();

// Play via AudioInputStream from URL
var url = subsonic.media().streamUrl(songId);
streamPlayer.open(AudioSystem.getAudioInputStream(url));
streamPlayer.play();

The following still doesn't work:

// Play directly via URL
streamPlayer.open(subsonic.media().streamUrl(songId));
streamPlayer.play();

This seems to be an issue with the implementation of Java's AudioSystem class. When providing an URL StreamPlayer relies on AudioSystem to determine the audio format. This is implemented in the AudioSystem class by invoking a getAudioFileFormat method in all available AudioFileReaders. A UnsupportedAudioFileException is expected if the reader doesn't support the file format, the problem is that the FlacAudioFileReader from jflac-codec (which comes with StreamPlayer) creates an InputStream by using the URL::openStream() method. This cannot work though, since it will create a ChunkedInputStream (because of chunked encoding in the HTTP response) which does not have mark support. Since the FlacAudioFileReader requires mark support though, it throws an IOException and everything fails. It's debatable who's at fault here. jflac, for its unsafe creation of the input stream, AudioSystem for not handling other exceptions correctly or StreamPlayer for trying to determine the audio format from an URL instead of an InputStream in the first place. Either way it doesn't seem to work. For StreamPlayer this isn't really an issue, since you can also just pass the InputStream.

Right, I'll try to figure out the proper way of implementing this. It may require setting up proxy like said: https://stackoverflow.com/questions/12701249/getting-access-to-media-player-cache/18627606#18627606. Something like this: https://github.com/danikula/AndroidVideoCache

You could also look into the source code of UltraSonic, which is a full fledged subsonic client android app, if you're looking for a solution with Android's MediaPlayer.

For example you can look into the StreamProxy class and MediaPlayer wrapper class:

I was so curious and impatient how it'd work that I quickly wrote extended class that did exactly the same but returns URL. It worked, however MediaPlayer.getTotalDuration() now returns bad value something around 3 and a half hour, independently in each file (of course value got from event, setOnReady())

I don't really have that much experience with Android. Did you do it like described here? I'm not sure how MediaPlayer extracts the duration from an audio stream. Usually the duration of a stream can only be determined if the stream contains metadata data frames (e.g. ID3 frames) in addition to the audio data frames. I am not sure if a) Subsonic includes those frames and b) MediaPlayer is able to exract the duration from these frames. If you don't get it working maybe look into the UltraSonic source code to see if it works there.

Though, usually you would fetch the song meta data from subsonic before starting the stream anyway, so you could also use the duration from the subsonic meta data. Here's an example:

var searchResult = subsonic.searching().search3("Caravan Palace", 
        SearchParams.create().artistCount(0).albumCount(0));

var song = searchResult.getSongs().get(0);
var duration = song.getDuration();
var streamUrl = subsonic.media().streamUrl(song.getId());

var mediaPlayer = new MediaPlayer();
mediaPlayer.setDataSource(streamUrl);
BigB84 commented 3 years ago

I've implemented the changes and the following works now (not merged / released yet)

Great! :) I'll use them when they got merged.

You could also look into the source code of UltraSonic, which is a full fledged subsonic client android app, if you're looking for a solution with Android's MediaPlayer.

Right, It's good idea to study their solutions, unfortunately I have no experience with android. However I've read about JVM differences (I'm not sure whether I can easily use android libs in regualr java maven project). Target device is PinePhone and maybe Librem 5 with aarch64 Linux distros (so just regular .jar >= openjdk 16, then distro specific packaging).

Did you do it like described here?

I use javafx.scene.media.MediaPlayer with javafx.scene.Media which seems to be different than androids one. I'm looking forward to find another solution which could allow me to use other codecs especially flac and ogg

Though, usually you would fetch the song meta data from subsonic before starting the stream anyway, so you could also use the duration from the subsonic meta data.

Your example helped me to solve the problem with binding. :) Previously I tried to obtain total duration from MediaPlayer.getTotalDuration, now It looks like:

// Initialized before
Subsonic subsonic; 
Child child;

MediaService ms = new MediaService(subsonic);
URL url = ms.URLOfStream(child.getId()); // This is new URL returning method
Media media = new Media(url.toURI().toString()); // This is Media class that we pass to the MediaPlayer
MediaPlayer mPlayer = new MediaPlayer(media);

// Global declaration of totalduration for further use
private static double totalduration;

// Part of method that sets bindings to statusSlider, playedTimeLabel, leftTimeLabel

// (...)
// Slider
   mplayer.setOnReady(() -> {
      totalduration = child.getDuration() * 1000; // in mills
      statusSlider.setMax(totalduration);
    });
// (...)

Now I can stream the audio to the MediaPlayer with proper binding without downloading the file.

Next thing is setting up cache and playing audio from it. I think I lack some important knowledge so I'll try to read more and study some other java subsonic clients solutions.

Thanks a lot for quick response and detailed explanations :)

calne-ca commented 3 years ago

The changes have been merged and a new version 0.2.0 has been released. It may take some moments before it is available in maven central. This issue has been closed.

I use javafx.scene.media.MediaPlayer with javafx.scene.Media which seems to be different than androids one. I'm looking forward to find another solution which could allow me to use other codecs especially flac and ogg

Ah, I assumed you meant the Android media player. Ignore what I've said regarding the android media player then.

Your example helped me to solve the problem with binding. :)

I'm glad to hear that. If you encounter any other issues with this library, feel free to open another issue.