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

Playing a remote file and cache the file at the same time #47

Open
ReaganRealones opened this issue Feb 27, 2020 · 126 comments
Open

Playing a remote file and cache the file at the same time #47

ReaganRealones opened this issue Feb 27, 2020 · 126 comments
Assignees
Labels
3 testing enhancement New feature or request

Comments

@ReaganRealones
Copy link

Any suggestion on how I could play a remote files and store the files in cache for faster playing next time? One option is to use Flutter cache manager to download it too when I play it but this would mean downloading twice i.e one from the remote url (for the audioplayer) and one to store in cache for next time.

How possible is it to play the remote file while caching it for faster playing the next time?

@ryanheise ryanheise self-assigned this Feb 27, 2020
@ryanheise
Copy link
Owner

I'm not sure of any easy way to do this. Some podcast players make it a binary choice: You either stream it, or you download it in advance.

@BruceZhang1993
Copy link

This feature seems great. The flutter_cache_manager plugin's getFile method will return a Stream of FileInfo. I wonder whether it's possible to achieve this feature.

@ryanheise ryanheise added 1 backlog enhancement New feature or request labels Apr 17, 2020
@lkho
Copy link
Contributor

lkho commented Jun 7, 2020

The flutter_cache_manager plugin's getFile method will return a Stream of FileInfo

The FileInfo stream only provides the progress of the download (while the file is still downloading), but not a data stream. You need to wait for the whole file to be downloaded to get a real file path.

BTW, if you really want to play a URL while it is still being downloaded, there is a CacheDataSourceFactory in ExoPlayer, which might serve this purpose. However, this is platform side implementation and not controlled by Flutter. If it was to be implemented in this library, it would be a huge feature and requires a lot of platform communications.

Another suggestion regarding ExoPlayer, is to implement a custom DataSource, which can then be intercepted by Flutter, and we can send binary buffers via platform channels to populate the DataSource. In this way you can handle all caching/downloading on Flutter's side and do whatever you want to fetch the buffers. But this sounds a bit over complicated, and also requires a huge API change.

@ryanheise
Copy link
Owner

I would eventually like to replace the current setUrl/setXYZ API by a setDataSource API which would make it a lot easier to integrate with custom and other ExoPlayer data sources. There are various other motivations for wanting to do this including gapless playback. So I would say this idea is certainly on the table, but not in the short term.

@ryanheise
Copy link
Owner

I'll leave this link here for later as a guide for how this could be done on the iOS side:

https://vombat.tumblr.com/post/86294492874/caching-audio-streamed-using-avplayer

@sachaarbonel
Copy link

I was reading the rxdart doc when I came across this feature that could become handy for this feature request

@ryanheise
Copy link
Owner

I am not sure since the downloading actually happens on the platform side of the plugin.

However, it just occurred to me that after recently implementing an HTTP proxy in the plugin, this may actually help, since now the Flutter (Dart) side of the plugin can intercept the bytes being downloaded from the server and I could potentially implement caching in the proxy.

@nuc134r
Copy link
Contributor

nuc134r commented Aug 8, 2020

As audio_service and just_audio gets more and more functional I gradually let go any thoughts of forking and implementing needed features myself as they eventually appear implemented by Ryan.

So one really important feature I plan for my app is realtime caching. Since http proxy appeared, could you please expose some interception API so that data can be written to disk while being downloaded?

Or since you're the mighty Ryan Heise, maybe you could make an API similar to album art cache, using flutter_cache_manager? I would be very happy.

Thank you for your work, you're the MVP.

@ryanheise
Copy link
Owner

@nuc134r hehe :-)

Unfortunately, it may be a while for me to get around to this, since there are some higher priority things that need to be implemented in audio_service first.

(That is an open invitation for anyone who wishes to submit a PR! It seems reasonable to add an option to the relevant AudioSource subclasses to cache the downloaded audio to a given file.)

@afkcodes
Copy link

@ryanheise exoplayer has some option to cache i guess its SimpleCache. will look into this.

@ryanheise
Copy link
Owner

Hi @ashishfeels yes it has both custom headers and caching support, while iOS doesn't. Because I needed this to work on iOS, I ended up writing a proxy that lets me specify headers, and it would also allow me to do my own caching, and this is done in a way that would work consistently on both iOS and Android. If you take a look at the Dart code, you'll see that it is not actually too difficult to implement this solution, so either way, it is a matter of getting around to it.

@afkcodes
Copy link

@ryanheise sorry i was not using the plugin so didn't get the chance to go through, and now im moving to just_audio as i feel this plugin is home for audio related apps. thanks man, do you mean that the feature is already implemented or it still need some work. i will go through the code anyway

@ryanheise
Copy link
Owner

@ashishfeels , The caching side isn't implemented yet, but the proxy is, so it would be a short step from there to add a cache.

(Note that since iOS doesn't have these features, and "implementing your own proxy" is the usual approach iOS developers have to resort to to have them, so that becomes our lowest common denominator.)

@afkcodes
Copy link

afkcodes commented Aug 21, 2020

@ryanheise although your words are bit hard to understand you are pretty amazing. i just saw the headers code you mentioned still looking at it. probably will have to give it some more time. As i don't own a iOS/Mac i'm pretty much doomed.

@nuc134r
Copy link
Contributor

nuc134r commented Aug 21, 2020

@ashishfeels Ryan means that we need to implement caching using the proxy instead of ExoPlayer's mechanisms because on iOS (and web) there is no ExoPlayer. And that the course of this ship is to have features supported by all platforms so we better find solutions in common code which is Dart. And it already has a proxy implemented.

@afkcodes
Copy link

@nuc134r understood thanks mate.

@nuc134r
Copy link
Contributor

nuc134r commented Oct 13, 2020

Was thinking a lot about extending existing proxy to allow realtime caching (and even tried) and then stumbled upon #172. Considering that I want to provide data from disk on repeating requests, get notified of caching progress and have an index of cached data I figured that it would be better to set up my own proxy between API and just_audio.

Also I see it as a better approach in terms of plugin ecosystem architecture. I think I'm going to try to implement it and if I reach any success I'll update this issue.

@ryanheise
Copy link
Owner

@nuc134r that's an interesting thought to contemplate, although the plan is to eventually have caching support directly in the plugin as I think there are benefits in this case to having it tightly coupled. The proxy is required for other reasons, and it would not actually be difficult to tack caching onto the existing proxy, rather than have a situation where we have proxy upon proxy.

This was referenced Oct 26, 2020
@zxl777
Copy link

zxl777 commented Dec 23, 2020

@nuc134r
Copy link
Contributor

nuc134r commented Dec 23, 2020

Maybe using https://pub.dev/packages/flutter_cache_manager

@zxl777 That would make you have to either wait for file to download fully before playing or download file twice. It is discussed above.

Was thinking a lot about extending existing proxy to allow realtime caching (and even tried) and then stumbled upon #172. Considering that I want to provide data from disk on repeating requests, get notified of caching progress and have an index of cached data I figured that it would be better to set up my own proxy between API and just_audio.

Also I see it as a better approach in terms of plugin ecosystem architecture. I think I'm going to try to implement it and if I reach any success I'll update this issue.

I want to report that I got it working and currently finishing features described above to cover my own needs. When it's ready I'm going publish a package and link it here. Not sure if Ryan would like to integrate it into just_audio but will be easy to set it up yourself anyway:

final url = await _proxy.addUrl(track.url, track.key, meta: track);
_audioSource.add(ProgressiveAudioSource(url, tag: track));

And there are still performance issues in some cases that need to be addressed.

@ryanheise
Copy link
Owner

@nuc134r excellent work! As mentioned in my previous comment, I very much would like to support this feature directly in just_audio. The proxy exists within just_audio, and I can think of 3 features that this would enable directly within the plugin:

  1. Caching audio while downloading/playing.
  2. Serving audio directly from the asset bundle rather than writing the asset to file first (the current approach)
  3. Playing audio directly from a supplied stream of bytes (e.g. may be used for custom DRM solutions.)

@ryanheise
Copy link
Owner

I've just created a new branch called proxy_improvements which refactors the proxy code to make it more extensible. Using it, just_audio now streams assets directly from the bundle via the proxy, and there is now also a new StreamAudioSource for dynamically streaming data which also supports range requests (See #172 ).

Caching could potentially be plugged in by subclassing this class, however ultimately I would like to have such a class offered by just_audio, or alternatively add an option to UriAudioSource to request that the downloaded audio be cached.

The way that caching support should work is that it should support partial caching, such that if a byte range request was made, that partial segment of the audio file should be cached.

@nuc134r I've left the caching part unimplemented for now in case you would be interested in submitting a pull request for it.

Otherwise, I'd be interested to have a discussion here about what sort of features people would like in this caching function besides the caching itself.

@afkcodes
Copy link

yeah probably i didn't framed the question rightly. Basically is there a way where i can access the stored file and play it with other music players ?

@ryanheise
Copy link
Owner

@afkcodes I think that's covered in the documentation so if it's not, I would suggest opening a new issue for the documentation request.

@afkcodes
Copy link

will surely go through the docs and will update. you are awesome man.

@ryanheise
Copy link
Owner

Since I haven't heard of any big problems introduced by my latest changes, I will go ahead and push out a release in the next day.

@afkcodes
Copy link

afkcodes commented Nov 4, 2021

Sure this seems good also for got to mention about the docs its written how to specify the cache file.

@kmod-midori
Copy link
Contributor

Haven't got any bug reports about this from customers, lgtm.

@ryanheise
Copy link
Owner

Thanks, yes the docs describe that, although certainly there's a lot of room for improvement in the docs.

@afkcodes
Copy link

afkcodes commented Nov 4, 2021

An example would be aswesome :) Thanks man.

@ryanheise
Copy link
Owner

There is an example: example/lib/example_caching.dart, but I assume you mean an example specifically of dealing with the underlying cache file? I just can't think of a simple example to demonstrate for that, and I think the current example demonstrates the more common use case. Note that you can either pass the cache file into the constructor if you want to choose where it gets stored, or you can leave the default which is to let just_audio choose a location. In either case, you can access the cacheFile getter afterwards to get whatever cache file is being used and do whatever you like with that file.

I haven't put any doc comments on the cacheFile property, but I think it is obvious enough what it is for the time being until I take the time to write descriptions of everything.

@afkcodes
Copy link

afkcodes commented Nov 4, 2021

Sure there's no hurry in that we will eventually update the docs when everything is ready . i will also help in that for sure.

@ryanheise
Copy link
Owner

Don't get too excited though :-) The documentation for the cacheFile field will be something like this:

  /// The cache file.
  final Future<File> cacheFile;

@ryanheise
Copy link
Owner

Since I haven't heard of any big problems introduced by my latest changes, I will go ahead and push out a release in the next day.

This fix has landed in 0.9.17.

@SalahAdDin
Copy link

@ryanheise
I am having this problem now:
Screen Shot 2021-11-28 at 17 44 51

@ryanheise
Copy link
Owner

@SalahAdDin I will need you to file a new bug report and provide details requested in the bug template, because unfortunately just that screenshot is not enough information for me to investigate.

@SalahAdDin
Copy link

@ryanheise is here, thank you.

@ryanheise
Copy link
Owner

Hi everyone, the fix/cache_headers branch fixes a bug when using headers in combination with LockCachingAudioSource. It also refactors the HTTP code which will affect any app that uses any of the HTTP proxy features, including also StreamAudioSource and headers with any other audio source.

Before I publish the next release, you might like to test the branch and let me know below whether it works or causes problems. If all is well, I will publish it within the next few days.

@lau1944
Copy link

lau1944 commented Oct 22, 2022

Hope this helps
https://pub.dev/packages/just_audio_cache

@ryanheise
Copy link
Owner

The fix/cache_headers branch has now been merged and published in release 0.9.30.

@mhassan772
Copy link

mhassan772 commented Jan 31, 2023

One thing I am not clear on how it work is the limit size of the cache. Like how does the cache get cleared? Is it automatic? Or do I have to do that myself? I am worried I would use this feature and after sometime the users will have their storage exhausted by our app. Can someone please help explain it to me? Thanks!

@ryanheise
Copy link
Owner

@mhassan772 , this has been documented in the API, and for people who don't want to read the documentation and just want to ask a person for the answer, you can try StackOverflow. It is an organised Q&A database where if your question has already been asked and answered, you don't need to make someone answer the same question more than once. It's a good way to save people from duplicating their effort.

@AlvaroRojas
Copy link

AlvaroRojas commented May 20, 2023

Hello, I noticed that if I add the header 'range': 'bytes=0-' to get the full response from the host it will throw exceptions when fetching data, had to do the following modification in the function _fetch():

if (response.statusCode != 200 && response.statusCode != 206) { httpClient.close(); throw Exception('HTTP Status Error: ${response.statusCode}'); }

Is that something that can be added in the official release?
Thanks!

@ryanheise
Copy link
Owner

I'm not clear on what you're adding to where to cause the crash, since nowhere in LockCachingAudioSource is it intended for the application to override the range request headers. Those are to be managed entirely by LockCachingAudioSource.

@AlvaroRojas
Copy link

AlvaroRojas commented May 20, 2023

Hi, let me give you more context.
I create a LockCachingAudioSource like this:

LockCachingAudioSource(
             'mytesturlendpoint',
              cacheFile: 'filenametest',
              headers: {'range': 'bytes=0-'},
            );

I can create a fork and give you a complete working example if you need but basically, I need to use the range header or else the endpoint will throttle my request and the connection will timeout before the file is downloaded, if I send the header, the endpoint server will not throttle my request and the file will download at maximum speed almost instantly.

Thanks!

@ryanheise
Copy link
Owner

ryanheise commented May 21, 2023

As mentioned, LockCachingAudioSource is not intended for the range request headers to be overridden by the app, these should be managed entirely by LockCachingAudioSource. There are two types of requests made in _fetch, one is the request for the whole file, and the other is a range request for a part of the file which is needed for two reasons: 1) so that the player can fetch the duration from a different byte range in the file that hasn't been downloaded yet, and 2) so that the player can serve seek requests to ranges that haven't been downloaded yet. The range request headers therefore need to be managed differently for different requests internally, which is unlike the headers parameter which is applied to all requests equally. The headers parameter should not be used to mess with this, it should be managed by LockCachingAudioSource.

If you need a feature like this, it needs to be built into the class.

However, I would like to be more clear on this before building this into the class. My understanding of the spec is that you should first receive the Accept-Ranges: bytes response header from the initial request before submitting range requests. If you can link me to an official source that says otherwise, I'd feel more comfortable about implementing this.

@AlvaroRojas
Copy link

Hi, thanks for the discussion.

You are totally right, the spec mentions a inital HEAD request should be performed to check if the host accepts range requests (in my case I know it always does), maybe my use case is a corner case to this, but unless I specify the range for all requests to the audio endpoint (that I don't control) it will serve the file with throttled speed which causes the httpClient to time out afer some KB are downloaded.
That's why my LockCachingAudioSource instances should all have range request even if its bytes=0- to ask the server to serve the full file at full speed.
The full range header is very common apparently and even chrome will send by default bytes=0- if the endpoint support ranges.
Again this might be a corner case and I could definitely just modify the if condition to include 206 responses along with 200.

Thanks for all the work you do!

@ryanheise
Copy link
Owner

Sounds like two changes are in order for LockCachingAudioSource:

  1. prevent apps from overriding the range request headers
  2. insert a HEAD request before the initial GET request to determine whether the server supports range requests, and if it does, set a range of bytes=0- on the initial GET request.

Would that work for you?

@AlvaroRojas
Copy link

Yes, that would fit my use case perfectly.
Thanks for hearing our feedback.

@emersonsiega
Copy link

@ryanheise Can I use the LockCachingAudioSource with m3u8 files?
In my tests, I always receive this error:

Unhandled Exception: Null check operator used on a null value           just_audio.dart:2008

From just_audio.dart:

  /// Starts the server.
  Future<dynamic> start() async {
    _running = true;
    _server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
    _server.listen((request) async {
      if (request.method == 'GET') {
        final uriPath = _requestKey(request.uri);
        final handler = _handlerMap[uriPath]!; // ERROR IS THROWN HERE - LINE 2008
        handler(this, request);
      }
    }, onDone: () {
      _running = false;
    }, onError: (Object e, StackTrace st) {
      _running = false;
    });
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3 testing enhancement New feature or request
Projects
None yet
Development

No branches or pull requests