ryanheise / just_audio

Audio Player
1.03k stars 650 forks source link

Feature request: lazy fetching of audio URLs in ConcatenatingAudioSource #777

Open hacker1024 opened 2 years ago

hacker1024 commented 2 years ago

Is your feature request related to a problem? Please describe. The audio URLs provided by the API I'm using expire after a fairly short amount of time. The API provides a "queue" in the form of a list of identifier keys, and then the audio URL can be requested for playback using the identifier.

It'd be great to have a way to use the "queue" with a ConcatenatingAudioSource, taking advantage of its preloading capabilities (as well as the ability to tightly couple the ConcatenatingAudioSource with audio_service and only manage one queue).

Describe the solution you'd like A lambda based API would work nicely here.

class LazyUriAudioSource<T> {
  final T identifier;
  final Future<Uri> Function(T identifier) fetchUri;

  const LazyUriAudioService(this.identifier, this.fetchUri);
}
concatenatingAudioSource.add(LazyUriAudioSource(identifier, (identifier) => apiService.getMediaUri(identifier)));

When the audio source is played (or preloaded), fetchUri is called beforehand. This process is invisible to the end user, to whom it appears as if the regular audio file is buffering.

Describe alternatives you've considered This could be implemented through StreamAudioResponse, but it would be a lot easier not to have to implement the network I/O manually, and probably more performant to let the platform handle it as (I believe?) UriAudioSource does now.

The ConcatenatingAudioSource could also just be avoided for this use case, but that's disappointing for the reasons I mention above.

Additional context I'm happy to take a crack at implementing this myself if you'd be willing to include this feature.

ryanheise commented 2 years ago

Maybe it could be called DynamicAudioSource or MappingAudioSource. Not sure if "lazy" will always make sense since concatenating audio sources also have the option to load children eagerly or lazily, in which case, the laziness of this newly proposed audio source wouldn't really be lazily loaded in an eagerly loaded concatenating audio source.

I do wonder, what should this new audio source do if the mapping function rotated its URLs during a range request? i.e. the player requests the resource from the beginning, then the URL expires and gets rotated, then the player continues its thing making a range request for the end of the file without realising that a different resource is now being returned.

hacker1024 commented 2 years ago

I do wonder, what should this new audio source do if the mapping function rotated its URLs during a range request?

In my case, the URL won't expire if it's in use, so this isn't an issue. I was thinking more along the lines of the URL being cached, but I suppose that could always be implemented outside of just_audio.

I think a decent backend should still support any range request just fine, though - it is serving the same file, after all.

Not sure if "lazy" will always make sense since concatenating audio sources also have the option to load children eagerly or lazily, in which case, the laziness of this newly proposed audio source wouldn't really be lazily loaded in an eagerly loaded concatenating audio source.

I meant "lazy" as in the URL is loaded lazily when it is needed by the plugin, but that's a good point.

hacker1024 commented 2 years ago

On another note, this concept could be extended to allow postponed generation of any AudioSource - not just a URL. It would work differently, though - the AudioSource would just be generated once, instead of repeatedly like the URL could be.

ryanheise commented 2 years ago

From the platform player's perspective, each audio source is a URI. In the case of a StreamAudioSource, that is mapped to a URL served by the proxy. And the assumption is that each URI is stable, in that the resource it refers to will never change over the lifetime of the audio source.

This is no problem in your use case if each time the origin URL rotates out, the new origin URL still effectively refers to the very same resource. But this API could easily be misused by someone who thinks it would be a good idea to make it return a randomised song each request, like a jukebox. In that case, the platform player will be sending multiple range requests to fetch different parts of what it assumes is the same file, but if the mapping changes in the middle of doing that, then the platform player will fetch the head of one song and the tail of another song, and the decoder won't know how to decode what results.

I suppose this is fine so long as the documentation states clearly that the origin URI returned by this mapping function must always effectively point to the same resource. That is, even though the URI returned may be different each time, the actual data response should be stable.

hacker1024 commented 2 years ago

In terms of implementing this:

I've been looking into where to store the fetchUri functions for later use by the plugin. I think the existing _audioSources map in AudioPlayer would do nicely - with one problem.

AudioSources are added to _audioSources in the _registerAudioSource function, but as far as I can tell, they're never removed during the life of the AudioPlayer. If the identifier or fetchUri closure uses a lot of memory, this could be a rather large memory leak.

Do you have any ideas on how to solve this problem? My initial thought is to have each platform report when its finished with an AudioSource.

ryanheise commented 2 years ago

Perhaps these should be deallocated on stop or dispose but in practice even in the current code, audio source objects are lightweight anyway. If a custom audio source has its own internal payload, that audio source could provide its own method to release that payload.

kirankumbhar commented 1 year ago

Any progress or plans on implementing this? I need this to queue the songs of the album where URL of the song is signed URL and expires after certain time

ryanheise commented 1 year ago

There are two pull requests available now to try (#779 and #800), but I'm thinking of a 3rd approach that might be a better approach long term. For now, you will need to pick one of the available pull requests, noting that the first one is implemented precisely in native code and may not work on all platforms, while the second one is implemented approximately in Dart and should be cross platform.

kirankumbhar commented 1 year ago

@ryanheise Thank you for pointing out to ResolvingAudioSource #800. It works in my case perfectly. Also thank you for the amazing library!

synapticvoid commented 9 months ago

Hi @ryanheise ,

I have exactly the same use case: my audio files come from an URI that must be resolved just before playing them. They are signed and, for security reasons, I have to keep the validity window as small as possible.

The #800 does what I need. I've noticed that it's waiting to be merged since Sept. 2022, is there a particular reason? Can I do anything to make it advance?

Thanks again for your awesome work, just_audio works so well!

ryanheise commented 9 months ago

Hi @synapticvoid you can see #779 for a discussion of the various approaches. I'm not able to commit to one direction or the other at this time since I think the ideal solution would also involve a new platform architecture.

However, the solution you linked to is possible to implement without direct support inside the plug-in because it is just a subclass of stream audio source.

synapticvoid commented 9 months ago

Thanks for your quick response. I read the #779, and you're right, there are deeper consequences for this feature. Unfortunately, I cannot use the #800 solution, as it involves redefining internal methods, specifically:

  @override
  AudioSourceMessage _toMessage() {
    return ProgressiveAudioSourceMessage(
        id: _id, uri: _uri.toString(), headers: headers, tag: tag);
  }

_toMessage() is not available outside just_audio. I took the time to study the whole AudioSource class hierarchy, and everything is (understandably) locked inside the library. Or maybe I missed something?

ryanheise commented 9 months ago

Why do you need _toMessage?

synapticvoid commented 9 months ago

In my custom AudioSource, I'll have to resolve the signed URI when the audio is about to be played. In what I understood, the _toMessage() override is implicitly called when the _player retrieves the real data. Therefore, I have to have access to this function to inject my resolved URL.

ryanheise commented 9 months ago

That is not one of the viable solutions that were offered to you above.

ryanheise commented 9 months ago

I think I get your question now, in #800 the author has overridden the _toMessage() method. As far as I can tell, this is unnecessary, even in #800 itself. You do not need to override the _toMessage() method in order to use StreamAudioSource.

synapticvoid commented 9 months ago

Oh I see. I'll try it out to see how it works. Thanks again for your quick responses!

canxin121 commented 4 months ago

Is there a way to modify the AudioSource Uri of the index specified in the ConcatenatingAudioSource so that I can update the Uri of the next song by listening to the _player.currentIndex to achieve laziness to get the music link.

I tried to remove and then insert, but this resulted in an Index Error (tested in Android)and then infinite seek to the next music.

Chaphasilor commented 4 months ago

You could probably append the updated AudioSource behind the next source, and then remove the original next source. To visualize:

A-B-C-D
↓
A-B-B*-C-D
↓
A-B*-C-D

That might work without resulting in index errors?