just_audio
just_audio copied to clipboard
`LockCachingAudioSource` does not recover from network errors
Which API doesn't behave as documented, and how does it misbehave?
When calling LockCachingAudioSource.request()
after a network error occurs, no bytes will be transferred.
Minimal reproduction project https://github.com/agersant/just_audio/tree/just-audio-issue-594
To Reproduce (i.e. user steps, not code) Steps to reproduce the behavior:
- Clone the
just-audio-issue-594
branch of the repository above. - Run the example app under
just_audio_background/example
. - Tap the
Prefetch
button. Observe the debug console messages indicating data being received. - While data is being received, turn off the internet connection being used by the device (using the Android quick settings UI at the top of the screen).
- Observe debug messages about the download failure (and an unhandled exception call stack).
- Tap the
Prefetch
button again. - Observe the debug console and notice no messages about data being transferred.
Error messages
I/flutter (14120): #0 _HttpIncoming.listen.<anonymous closure> (dart:_http/http_impl.dart:443:7)
I/flutter (14120): #1 _invokeErrorHandler (dart:async/async_error.dart:45:24)
I/flutter (14120): #2 _HandleErrorStream._handleError (dart:async/stream_pipe.dart:269:9)
I/flutter (14120): #3 _ForwardingStreamSubscription._handleError (dart:async/stream_pipe.dart:157:13)
I/flutter (14120): #4 _rootRunBinary (dart:async/zone.dart:1452:47)
I/flutter (14120): #5 _CustomZone.runBinary (dart:async/zone.dart:1342:19)
I/flutter (14120): #6 _CustomZone.runBinaryGuarded (dart:async/zone.dart:1252:7)
I/flutter (14120): #7 _BufferingStreamSubscription._sendError.sendError (dart:async/stream_impl.dart:360:15)
I/flutter (14120): #8 _BufferingStreamSubscription._sendError (dart:async/stream_impl.dart:378:7)
I/flutter (14120): #9 _BufferingStreamSubscription._addError (dart:async/stream_impl.dart:280:7)
I/flutter (14120): #10 _SyncStreamControllerDispatch._sendError (dart:async/stream_controller.dart:737:19)
I/flutter (14120): #11 _StreamController._addError (dart:async/stream_controller.dart:615:7)
I/flutter (14120): #12 _StreamController.addError (dart:async/stream_controller.dart:569:5)
I/flutter (14120): #13 _HttpParser._
Expected behavior
No exception callstack should appear in the log, and the download should restart or resume when tapping Prefetch
.
Screenshots
https://user-images.githubusercontent.com/817256/142405368-a52b0f42-9b07-4d29-962f-f5dd5ee00347.mp4
Desktop (please complete the following information):
- OS: Windows 10
- Browser N/A
Smartphone (please complete the following information):
- Device: Pixel 5 emulator
- OS: Android API level 31
Flutter SDK version
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 2.5.3, on Microsoft Windows [Version 10.0.19042.1348], locale en-US)
[!] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
X cmdline-tools component is missing
Run `path/to/sdkmanager --install "cmdline-tools;latest"`
See https://developer.android.com/studio/command-line for more details.
X Android license status unknown.
Run `flutter doctor --android-licenses` to accept the SDK licenses.
See https://flutter.dev/docs/get-started/install/windows#android-setup for more details.
[√] Chrome - develop for the web
[√] Android Studio (version 2020.3)
[√] VS Code (version 1.62.2)
[√] Connected device (3 available)
! Doctor found issues in 1 category.
Additional context
For some reason, the debugger doesn't seem to catch the exception in just_audio
code with this minimal repro. In my full app, I was able to determine that https://github.com/ryanheise/just_audio/blob/1c6598c0f3589baac4194e71b4d2885205f58ef4/just_audio/lib/just_audio.dart#L3009 was the victim.
Further debugging of the minimal repro seems to indicate that the cleanup code (https://github.com/ryanheise/just_audio/blob/1c6598c0f3589baac4194e71b4d2885205f58ef4/just_audio/lib/just_audio.dart#L2899) never runs. Also note that this catchError
handler does not return a HttpClientResponse
, which is illegal.
Thanks in advance for the help!
I am also seeing similar issues on ios. After being in background for a while, when coming back, it seems the proxy server has crashed or is no longer running. This results in error PlayerException: (-1004) Could not connect to the server.
which I believe is because the http proxy has errored, and is not recovered.
Is there more to the stack trace that traces back to just_audio plugin code? I'll be able to insert some detection and recovery code at that point.
I have two:
PlayerException: (-1004) Could not connect to the server.
File "just_audio.dart", line 778, in AudioPlayer._load
File "<asynchronous suspension>"
File "just_audio.dart", line 1346, in AudioPlayer._setPlatformActive.setPlatform
PlayerException: (-1004) Could not connect to the server.
File "just_audio.dart", line 778, in AudioPlayer._load
File "<asynchronous suspension>"
File "just_audio.dart", line 708, in AudioPlayer.load
File "<asynchronous suspension>"
File "just_audio.dart", line 683, in AudioPlayer.setAudioSource
File "<asynchronous suspension>"
File "player_task.dart", line 146, in MyAudioHandler.skipToQueueItem
File "<asynchronous suspension>"
File "playerstate.dart", line 417, in PlayerCommandPlaySongInAlbum.reduce
File "<asynchronous suspension>"
File "store.dart", line 455, in Store._processAction_Async
Sadly I have not been able to reproduce on simulator.
Hmm, unfortunately neither of those point to the proxy.
I am guessing that an onError
and/or onDone
handler should probably be added on on line 1924 to handle this scenario, and then automatically set _running
back to false
so that it can be restarted when next needed.
encountered same bug.
I am also seeing similar issues on ios. After being in background for a while, when coming back, it seems the proxy server has crashed or is no longer running. This results in error
PlayerException: (-1004) Could not connect to the server.
which I believe is because the http proxy has errored, and is not recovered.
any solution?
@Blacksith , the latest progress is in my previous comment. That is, I have an idea of what is going on, but haven't taken a crack at it yet. You are welcome to help experiment to make it happen sooner.
@Blacksith , the latest progress is in my previous comment. That is, I have an idea of what is going on, but haven't taken a crack at it yet. You are welcome to help experiment to make it happen sooner.
I will also try to see what the problem could be. The only thing I know now is that this error is relevant only on ios release build, in the debug build I did not find such an error
Hmm, unfortunately neither of those point to the proxy.
I am guessing that an
onError
and/oronDone
handler should probably be added on on line 1924 to handle this scenario, and then automatically set_running
back tofalse
so that it can be restarted when next needed.
tried adding: _server.doOnError((p0, p1) { _running = false; }); _server.doOnDone(() { _running = false; }); but the error is still there, maybe I got it wrong?
I was supposing to add the onError
and onDone
handlers to the _server.listen
call.
I was supposing to add the
onError
andonDone
handlers to the_server.listen
call.
_server.listen((request) async {
if (request.method == 'GET') {
final uriPath = _requestKey(request.uri);
final handler = _handlerMap[uriPath]!;
handler(request);
}
_server.doOnError((p0, p1) {
_running = false;
});
_server.doOnDone(() {
_running = false;
});
});
Like this? This not woking too...
I'm not at my computer right now but from memory the documentation for listen says that it takes these callbacks directly as parameters.
I'm not at my computer right now but from memory the documentation for listen says that it takes these callbacks directly as parameters.
can you help with that? Because I don't understand how I need to do it right
_server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
_server.listen((request) async {
if (request.method == 'GET') {
final uriPath = _requestKey(request.uri);
final handler = _handlerMap[uriPath]!;
handler(request);
}
}, onError: (){
_running = false;
});
if I do as described in the documentation - I keep getting PlatformException(error, No active stream to cancel, null, null)
I found a solution. I added on line 778 _proxy.start(); After that everything started to work. I don't claim this is the solution to the problem, but it worked for me.
It works better with this patch:
_server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
_server.listen((request) async {
if (request.method == 'GET') {
final uriPath = _requestKey(request.uri);
final handler = _handlerMap[uriPath]!;
handler(request);
}
},
onDone: () {
_running = false;
},
onError: (Object o, StackTrace st) {
_running = false;
});
Now it can recover to playing again half of the time, the other half the app just crashes directly, though that may be because of missing error handling on my own part.
I have faced the same issue on iOS release on physical device. PlayerException: (-1004) Could not connect to the server.
Any suggestion to resolve the issue.
@AmmarZY we are figuring this out together, please feel welcome to join in and try out the numerous suggestions above as a starting point. You will need to make these edits to a local copy of just_audio, or alternatively you could copy the source of LockCachingAudioSource
into your own app with a slightly different name, and then experiment with the above changes in your own version of that class.
@ryanheise Thanks for the support.
The following hack worked for me
I found a solution. I added on line 778 _proxy.start(); After that everything started to work. I don't claim this is the solution to the problem, but it worked for me.
The issue occurs when the iPhone screen locked normally while audio playing in the background. When the audio completed, the iOS after 30 seconds interrupts the app proxy and it stopped working as the result of the following error The PlayerException: (-1004) Could not connect to the server.
@Blacksith @AmmarZY can you please bring some code examples of your fix?
I am asking, cos line 778 is a bit busy one...
Also it's not clear, should _proxy.start();
be await
ed or not...
@esiqveland 's code looks on the right track to me. (just fyi I'll be in hospital on Monday, so probably can't give it attention until after then.)
Based on my earlier experiments I wasn't certain if it caught errors in all cases, so it needs further experimentation.
@pro100svitlo
Also it's not clear, should
_proxy.start();
beawait
ed or not...
No, you don't need to add it. Just replace this code starts at line 1783 as suggested by @esiqveland :
_server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
_server.listen((request) async {
if (request.method == 'GET') {
final uriPath = _requestKey(request.uri);
final handler = _handlerMap[uriPath]!;
handler(request);
}
},
onDone: () {
_running = false;
},
onError: (Object o, StackTrace st) {
_running = false;
});
I tested it already. I used a connectivity_plus package to check the connection, and if it is disconnected; then it will display error on network. Before I found @esiqveland solution, I always ended up having an error as mentioned by @agersant. Now, I don't have that problem anymore.
Well, if you still have error in
await sourceResponse.stream.pipe(request.response);
then just replace the code starts on line 2762:
// Pipe response
await sourceResponse.stream.pipe(request.response).then((value) async {
await request.response.close();
}, onError: (Object o, StackTrace st) {
dev.log("Unable to connect");
});
Whenever it gets disconnected on these conditions:
- at start of buffering/downloading the song from backend
- at the end of buffering to load the next song automatically
Then it won't get the same error again. I tested it 5 times and it really works.
@gOzaru okay, my case probably is different.
I describe it here.
It take me some time to debug example app and compare with mine. The issue comes cos I use headers with my source.
Here is piece of code from library, where dramattic change happened:
@override
Future<void> _setup(AudioPlayer player) async {
await super._setup(player);
if (uri.scheme == 'asset') {
_overrideUri = await _loadAsset(uri.pathSegments.join('/'));
} else if (uri.scheme != 'file' &&
!kIsWeb &&
(headers != null || player._userAgent != null)) {
await player._proxy.ensureRunning();
_overrideUri = player._proxy.addUriAudioSource(this);
}
}
we are interested in this particular line:
(headers != null || player._userAgent != null)) {
I use headers, so in my app code goes further, and then fails inside _proxyHandlerForUri, line 3039.
@pro100svitlo Your problem may be different but once the issue is fixed for LockCachingAudioSource
it should also automatically be fixed for your problem, too. All that's happening in both cases is that the proxy is invoked (it happens for caching, and it happens for headers). Because there's a bug with the proxy not being able to reconnect after a network error, it affects both caching and headers similarly.
It is not only about reconnection. It's also about notifying player about the error: right now error swallowed silently.
@pro100svitlo yes, but that too is something where the solution is going to be the same in both the caching and the headers cases.
On error, you would probably want to call _playbackEventSubject.addError(...)
.
I've just been looking at this today, and strangely I have never experienced the proxy crashing and needing to be restarted. I do indeed get the exception stack trace listed in the bug report, but that stack trace is not coming from the proxy server itself, instead it's coming from the StreamAudioResponse.stream
that is returned from LockCachingAudioSource
.
So inside _proxyHandlerForSource
you can do this:
StreamAudioResponse sourceResponse;
Stream<List<int>> stream;
try {
sourceResponse =
await source.request(rangeRequest?.start, rangeRequest?.endEx);
// Forward errors to playbackEven
stream = sourceResponse.stream.asBroadcastStream();
stream.listen((event) {}, onError: source._player?._playbackEventSubject.addError);
} catch (e, stackTrace) {
...
Then at the end of the method you can use the broadcast stream to pipe the response:
// Pipe response
await stream.pipe(request.response);
await request.response.close();
With this, everything seems to be working fine and the error can be listened to by the app via this playbackEvent
stream. After reconnecting the Internet, the proxy continues to work because it never crashed. That said, I still think checking if the proxy crashed is a good idea in case it does need to be restarted but I just didn't see that code path happening during my testing. I'd be interested to know how to actually make that happen.
Let me know if the above code works for you.
If anyone wants to test the code in my previous comment, I have pushed it to a branch called fix/caching
.
FYI, I intend to publish the above fix with or without testing within 24 hours, although of course I would appreciate if anyone can try the above fix before then.
I am using the fix/caching branch with LockCachingAudioSource. I have a bit of a unique setup in that I am using multiple just_audio instances with a bit of a custom implantation of audio_service. These errors are from Android 12 Nexus 7 emulator, but they also occur on a real device.
I have two issues:
- In my app, there is a way to terminate the players while the audioSource is being loaded. I am directly disposing the just_audio instance in this case. At times, while terminating an instance, I get an error like the following:
FileSystemException (FileSystemException: Cannot rename file to '/data/user/0/com.example.experience_beta/cache/just_audio_cache/remote/b14a3522dd64052b7fd9ea14665407eaefd25b40c6e0fb20c552939e2e981cac.mp3', path = '/data/user/0/com.example.experience_beta/cache/just_audio_cache/remote/b14a3522dd64052b7fd9ea14665407eaefd25b40c6e0fb20c552939e2e981cac.mp3.part' (OS Error: No such file or directory, errno = 2))
The exception does seem to be handled.
- My main issue is an unhandled exception. Here's an example of an error that just_audio with LockCachingAudioSource will throw if attempting to load a mp3 that doesn't exist on the remote server.
E/flutter (25316): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: Invalid argument(s) (onError): The error handler of Future.catchError must return a value of the future's type
E/flutter (25316): #0 _FutureListener.handleError (dart:async/future_impl.dart:193:7)
E/flutter (25316): #1 Future._propagateToListeners.handleError (dart:async/future_impl.dart:778:47)
E/flutter (25316): #2 Future._propagateToListeners (dart:async/future_impl.dart:799:13)
E/flutter (25316): #3 Future._completeError (dart:async/future_impl.dart:609:5)
E/flutter (25316): #4 _completeOnAsyncError (dart:async-patch/async_patch.dart:272:13)
E/flutter (25316): #5 LockCachingAudioSource._fetch (package:just_audio/just_audio.dart)
package:just_audio/just_audio.dart:1
E/flutter (25316): <asynchronous suspension>
E/flutter (25316):
I/flutter (25316): Proxy request failed: Exception: HTTP Status Error: 404
I/flutter (25316): #0 LockCachingAudioSource._fetch
package:just_audio/just_audio.dart:2738
I/flutter (25316): <asynchronous suspension>
When the above error occurs, it is always followed by the following:
E/ExoPlayerImplInternal(25316): at com.android.okhttp.internal.http.StreamAllocation.findHealthyConnection(StreamAllocation.java:128)
E/ExoPlayerImplInternal(25316): at com.android.okhttp.internal.http.StreamAllocation.newStream(StreamAllocation.java:97)
E/ExoPlayerImplInternal(25316): at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:289)
E/ExoPlayerImplInternal(25316): at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:232)
E/ExoPlayerImplInternal(25316): at com.android.okhttp.internal.huc.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:465)
E/ExoPlayerImplInternal(25316): at com.android.okhttp.internal.huc.HttpURLConnectionImpl.connect(HttpURLConnectionImpl.java:131)
E/ExoPlayerImplInternal(25316): at com.google.android.exoplayer2.upstream.DefaultHttpDataSource.makeConnection(DefaultHttpDataSource.java:629)
E/ExoPlayerImplInternal(25316): at com.google.android.exoplayer2.upstream.DefaultHttpDataSource.makeConnection(DefaultHttpDataSource.java:526)
E/ExoPlayerImplInternal(25316): at com.google.android.exoplayer2.upstream.DefaultHttpDataSource.open(DefaultHttpDataSource.java:352)
E/ExoPlayerImplInternal(25316): ... 7 more
E/ExoPlayerImplInternal(25316): Playback error
E/ExoPlayerImplInternal(25316): com.google.android.exoplayer2.ExoPlaybackException: Source error
E/ExoPlayerImplInternal(25316): at com.google.android.exoplayer2.ExoPlayerImplInternal.handleIoException(ExoPlayerImplInternal.java:624)
E/ExoPlayerImplInternal(25316): at com.google.android.exoplayer2.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:596)
E/ExoPlayerImplInternal(25316): at android.os.Handler.dispatchMessage(Handler.java:102)
E/ExoPlayerImplInternal(25316): at android.os.Looper.loop(Looper.java:223)
E/ExoPlayerImplInternal(25316): at android.os.HandlerThread.run(HandlerThread.java:67)
E/ExoPlayerImplInternal(25316): Caused by: com.google.android.exoplayer2.upstream.HttpDataSource$HttpDataSourceException: java.net.ConnectException: Failed to connect to /127.0.0.1:38815
E/ExoPlayerImplInternal(25316): at com.google.android.exoplayer2.upstream.DefaultHttpDataSource.open(DefaultHttpDataSource.java:358)
E/ExoPlayerImplInternal(25316): at com.google.android.exoplayer2.upstream.DefaultDataSource.open(DefaultDataSource.java:201)
E/ExoPlayerImplInternal(25316): at com.google.android.exoplayer2.upstream.StatsDataSource.open(StatsDataSource.java:84)
E/ExoPlayerImplInternal(25316): at com.google.android.exoplayer2.source.ProgressiveMediaPeriod$ExtractingLoadable.load(ProgressiveMediaPeriod.java:1014)
E/ExoPlayerImplInternal(25316): at com.google.android.exoplayer2.upstream.Loader$LoadTask.run(Loader.java:409)
E/ExoPlayerImplInternal(25316): at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
E/ExoPlayerImplInternal(25316): at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
E/ExoPlayerImplInternal(25316): at java.lang.Thread.run(Thread.java:923)
E/ExoPlayerImplInternal(25316): Caused by: java.net.ConnectException: Failed to connect to /127.0.0.1:38815
E/ExoPlayerImplInternal(25316): at com.android.okhttp.internal.io.RealConnection.connectSocket(RealConnection.java:147)
E/ExoPlayerImplInternal(25316): at com.android.okhttp.internal.io.RealConnection.connect(RealConnection.java:116)
E/ExoPlayerImplInternal(25316): at com.android.okhttp.internal.http.StreamAllocation.findConnection(StreamAllocation.java:186)
E/ExoPlayerImplInternal(25316): at com.android.okhttp.internal.http.StreamAllocation.findHealthyConnection(StreamAllocation.java:128)
E/ExoPlayerImplInternal(25316): at com.android.okhttp.internal.http.StreamAllocation.newStream(StreamAllocation.java:97)
E/ExoPlayerImplInternal(25316): at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:289)
The unhandled exception being thrown on ui_dart_state.cc causes the app to faulter until a hot reload.
I hope that this is of some help. I appreciate your work.
Thanks for testing, @Colton127 . What exactly happens when you say it "causes the app to faulter"? How can I reproduce it?
FYI, I intend to publish the above fix with or without testing within 24 hours, although of course I would appreciate if anyone can try the above fix before then.
I just tested the fix against the reproduction case at the top of this thread and it is only partially fixed:
✔ The unhandled exception output in step 5 is no longer present.
❌ Clicking the Prefetch
button still does not resume nor restart the download (steps 6-7).
I also confirmed with breakpoints that the following cleanup code does not run - which is most likely why a new connection is not attempted (as hinted by the nearby comment): https://github.com/ryanheise/just_audio/blob/29f201dff0a24e62acf07277f3226a504bb9e9d3/just_audio/lib/just_audio.dart#L2917
I should mention that the reproduction project at the top of this thread is slightly artificial because the prefetch button directly interacts with the audio source's request method rather than going through the actual player, so the behaviour won't be exactly the same. In particular, error handling won't be the same since the latest commit tries to broadcast the errors via the attached player.
It would be interesting if you could test this in an actual app, or with a minimal project that goes through the actual player.
Thanks for testing, @Colton127 . What exactly happens when you say it "causes the app to faulter"? How can I reproduce it?
Ryan,
The unhandled exception error can be thrown with code as simple as this:
final AudioPlayer _justPlayer = AudioPlayer();
final _audioSource = LockCachingAudioSource(Uri.parse('https://google.com/file.mp3'));
await _justPlayer.setAudioSource(_audioSource);
By the app faltering, I mean it becomes non-responsive. UI elements, say on a ListView builder, fail to load. However, this doesn't always occur. It usually occurs when I very rapidly play and stop numerous just_audio instances. This is not average usage, at all. I like to test these things because errors can be extremely unpredictable when your app is on a thousand different devices, all with different specifications and network speeds. I also cannot replicate the issue this morning, but I will keep a lookout and do my best to create a replicable environment when it occurs again.
For the record, I don't know if the issue is entirely the fault of just_audio. The exception being unhandled and thrown on ui_dart_state.cc causes the issue, sure, but it may be a deeper problem in flutter. The issue seems to be the UI state itself literally becoming non-responsive. I can still switch between PageViews, for example, but no content in my PageView (a Listview builder) loads. Basically, the UI becomes non-responsive when I attempt to load new elements. I think this is directly related to ui_dart_state.cc, and in fact, I honestly believe I have had an error on ui_dart_state.cc from something entirely different (and in a different project) result in the same issue. Fixing the issue should be as simple as handling the exception correctly, so it doesn't throw an exception on ui_dart_state.cc. Sorry for the wall of text...It's a tricky issue.
I am surprised if an unhandled exception would be the actual cause. Last time I looked at the flutter codebase, these exceptions are caught, and the stacktrace is printed out (as you see), or they are forwarded on to the error zone if you use runZonedGuarded
. Perhaps your issue is caused by some other code that coincides with this error.
Either way, I wouldn't mind handling that unhandled exception if I knew where it was coming from.
Here is an example that throws the unhandled exception error
import 'package:flutter/material.dart';
import 'package:just_audio/just_audio.dart';
void main() {
runApp(const Home());
}
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Container(
color: Colors.blue,
child: RawMaterialButton(
onPressed: () async {
try {
final AudioPlayer _justPlayer = AudioPlayer();
final _audioSource = LockCachingAudioSource(Uri.parse('https://google.com/file.mp3'));
await _justPlayer.setAudioSource(_audioSource);
await _justPlayer.dispose();
} catch (e) {
print('Error loading audio: $e');
}
},
child: Text('Load'),
),
),
)),
);
}
}
Output:
IE/flutter ( 2890): #5 LockCachingAudioSource._fetch (package:just_audio/just_audio.dart)
package:just_audio/just_audio.dart:1
E/flutter ( 2890): <asynchronous suspension>
E/flutter ( 2890):
I/flutter ( 2890): Proxy request failed: Exception: HTTP Status Error: 404
I/flutter ( 2890): #0 LockCachingAudioSource._fetch
package:just_audio/just_audio.dart:2738
I/flutter ( 2890): <asynchronous suspension>
E/flutter ( 2890): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: Invalid argument(s) (onError): The error handler of Future.catchError must return a value of the future's type
E/flutter ( 2890): #0 _FutureListener.handleError (dart:async/future_impl.dart:193:7)
E/flutter ( 2890): #1 Future._propagateToListeners.handleError (dart:async/future_impl.dart:778:47)
E/flutter ( 2890): #2 Future._propagateToListeners (dart:async/future_impl.dart:799:13)
E/flutter ( 2890): #3 Future._completeError (dart:async/future_impl.dart:609:5)
E/flutter ( 2890): #4 _completeOnAsyncError (dart:async-patch/async_patch.dart:272:13)
E/flutter ( 2890): #5 LockCachingAudioSource._fetch (package:just_audio/just_audio.dart)
package:just_audio/just_audio.dart:1
E/flutter ( 2890): <asynchronous suspension>
E/flutter ( 2890):
I/flutter ( 2890): Proxy request failed: Exception: HTTP Status Error: 404
I/flutter ( 2890): #0 LockCachingAudioSource._fetch
package:just_audio/just_audio.dart:2738
I/flutter ( 2890): <asynchronous suspension>
E/ExoPlayerImplInternal( 2890): Playback error
E/ExoPlayerImplInternal( 2890): com.google.android.exoplayer2.ExoPlaybackException: Source error
E/ExoPlayerImplInternal( 2890): at com.google.android.exoplayer2.ExoPlayerImplInternal.handleIoException(ExoPlayerImplInternal.java:624)
E/ExoPlayerImplInternal( 2890): at com.google.android.exoplayer2.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:596)
E/ExoPlayerImplInternal( 2890): at android.os.Handler.dispatchMessage(Handler.java:102)
E/ExoPlayerImplInternal( 2890): at android.os.Looper.loop(Looper.java:223)
E/ExoPlayerImplInternal( 2890): at android.os.HandlerThread.run(HandlerThread.java:67)
E/ExoPlayerImplInternal( 2890): Caused by: com.google.android.exoplayer2.upstream.HttpDataSource$InvalidResponseCodeException: Response code: 500
E/ExoPlayerImplInternal( 2890): at com.google.android.exoplayer2.upstream.DefaultHttpDataSource.open(DefaultHttpDataSource.java:389)
E/ExoPlayerImplInternal( 2890): at com.google.android.exoplayer2.upstream.DefaultDataSource.open(DefaultDataSource.java:201)
E/ExoPlayerImplInternal( 2890): at com.google.android.exoplayer2.upstream.StatsDataSource.open(StatsDataSource.java:84)
E/ExoPlayerImplInternal( 2890): at com.google.android.exoplayer2.source.ProgressiveMediaPeriod$ExtractingLoadable.load(ProgressiveMediaPeriod.java:1014)
E/ExoPlayerImplInternal( 2890): at com.google.android.exoplayer2.upstream.Loader$LoadTask.run(Loader.java:409)
E/ExoPlayerImplInternal( 2890): at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
E/ExoPlayerImplInternal( 2890): at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
E/ExoPlayerImplInternal( 2890): at java.lang.Thread.run(Thread.java:923)
E/AudioPlayer( 2890): TYPE_SOURCE: Response code: 500
I/ExoPlayerImpl( 2890): Release 8c0f270 [ExoPlayerLib/2.15.0] [generic_x86, Android SDK built for x86, unknown, 30] [goog.exo.core]
I/flutter ( 2890): Error loading audio: (0) Source error
Issue appears at package:just_audio/just_audio.dart:2738
I should mention that the reproduction project at the top of this thread is slightly artificial because the prefetch button directly interacts with the audio source's request method rather than going through the actual player [...]
I am in fact doing this in my app. (eg. https://github.com/agersant/polaris-android/blob/flutter/lib/core/prefetch.dart). The use case is that the app supports preloading music from different sources (current playlist, music explicitly flagged by the user, etc.) and I wanted to control manually what gets preloaded when.
Just to clarify for others, while this issue was created for LockCachingAudioSource, this issue occurs without use of "LockCachingAudioSource" on iOS devices for us with just setAudioSource() alone after app spends some time (few minutes or so) in the background while paused. Playback can't recover until the app is fully restarted.
setAudioSource() error (-1004) Could not connect to the server.
#0 AudioPlayer._load (package:just_audio/just_audio.dart:778:9)<asynchronous suspension>
#1 AudioPlayer.load (package:just_audio/just_audio.dart:708:14)<asynchronous suspension>
#2 AudioPlayer.setAudioSource (package:just_audio/just_audio.dart:683:18)<asynchronous suspension>
#3 FutureExtensions.onError.<anonymous closure> (dart:async/future.dart:874:15)<asynchronous suspension>
[VERBOSE-2:ui_dart_state.cc(209)] Unhandled Exception: PlatformException(-1004, Could not connect to the server., null, null)
Is there any reason we can't just re-instantiate AudioPlayer() when the error is caught? (see Edit, this is sort of a downstream mess)
just_audio: ^0.9.18
audio_session: ^0.1.6+1
audio_service: ^0.18.2
Edit: resurrecting the underlying proxy by creating a new instance isn't practical. Would going to HttpServer and getting connections info be a better way to ensure it's running? Straight from the horse's mouth rather than trusting _running? https://api.dart.dev/stable/2.16.2/dart-io/HttpConnectionsInfo/total.html
The other option would be to expose _proxy. As the python guy would say "we're all adults, here."
Edit again: I have only been using dart and flutter for 8 days.
@ndmgrphc You are right that this is a more general issue for anyone using the proxy. Please make sure you are testing the branch mentioned above with the developments intended to address this. Once you check out that branch, you could also try hacking it yourself as others above have done, to test out your theories. With my own testing, I have found the branch to work, but am trying to do more testing now to reproduce the issue @Colton127 just mentioned.
@ryanheise thanks. I simply exposed _proxy as proxyServer, caught the error in player.setAudioSource().onError, called player.proxyServer.start() and retried setAudioSource and all is good. In reading about HttpServer I'm not sure how one can determine whether the server is actually running.
This was a show stopper so I think for now I'll work in some kind of sane retry strategy and go with it for now.
@ndmgrphc please confirm whether you have tested the fix/caching
branch which already attempts to catch errors and restart the proxy.
@ryanheise sorry for the delay it takes a while to test because each time I have to wait for it to die. I tried the fix/caching branch and the error was still there but now traces to 799.
#just_audio: ^0.9.18
just_audio:
git:
url: [email protected]:ryanheise/just_audio.git
path: just_audio
ref: fix/caching
Sorry this is so ugly:
#0 AudioPlayer._load (package:just_audio/just_audio.dart:799:9)
flutter: <asynchronous suspension>
flutter: #1 AudioPlayer.load (package:just_audio/just_audio.dart:729:14)
flutter: <asynchronous suspension>
flutter: #2 AudioPlayer.setAudioSource (package:just_audio/just_audio.dart:704:18)
flutter: <asynchronous suspension>
flutter: #3 FutureExtensions.onError.<anonymous closure> (dart:async/future.dart:874:15)
flutter: <asynchronous suspension>
flutter:
[VERBOSE-2:ui_dart_state.cc(209)] Unhandled Exception: PlatformException(-1004, Could not connect to the server., null, null)
I need to use the exposed proxyServer. Again, works fine as long as audio is playing and app is active (foreground or background). But after a period of no audio activity it seems to die.
@ndmgrphc if your app is crashing on line 799 then you may not be using this branch in the way it was intended, because there should not be any need to reload the audio source if you want to let the plugin resolve the error transparently. Can you provide a stack trace to confirm?
@Colton127 The latest commit should handle the unhandled exceptions. I'm not convinced that this would fix any behavioural issues, only that it will suppress the unhandled exceptions messages in the logs.
@ryanheise I'm just calling setAudioSource() which works absolutely fine until a period of inactivity when the HttpServer appears to stop. I can't prove this because HttpServer has no exposed way of determine whether or not it's running. I'm assuming if it did have this, you'd not be setting _enabled elsewhere and would get getting the info straight from the horse's mouth (the horse here being HttpServer.
I switched to fix/caching and the problem remained.
As of this moment, the only way I can get background audio working reliably is to catch "-1004, Could not connect to the server." and explicitly restart your internal HttpServer which I am doing from the outside through my newly exposed AudioPlayer.proxy (and proxy.server). Once I restart it, I call setAudioSource again (same args, doing so recursively, carefully) and boom, works as expected. So the issue is indeed that HttpServer is dying (stopping) in the background unless I'm missing something.
If I understood this patch https://github.com/ryanheise/just_audio/issues/594#issuecomment-1007212756 correctly, it's at least setting _running = false for you so you can just run the request again on your own (letting your proxy's ensureRunning() do the start for you.
Is this what you meant? I still have to fire the retry myself, right (as in call setAudioSource() again)?
Again, very sorry as I have very little experience in Dart or any of these packages. In looking at HttpServer I don't understand how one would observe that the server has stopped. This does not seem to concern the Dart community so I must be looking at it wrong.
I also confirmed with breakpoints that the following cleanup code does not run - which is most likely why a new connection is not attempted (as hinted by the nearby comment):
https://github.com/ryanheise/just_audio/blob/29f201dff0a24e62acf07277f3226a504bb9e9d3/just_audio/lib/just_audio.dart#L2917
I can confirm. The correct hook should happen by listening to errors on the stream returned by LockCachingAudioSource.request().
@agersant you can try replacing the last line of the request method by this:
return byteRangeRequest.future.then((response) {
response.stream.listen((event) {}, onError: (Object e, StackTrace st) {
// So that we can restart later
_response = null;
// Cancel any pending request
for (final req in _requests) {
req.fail(e, st);
}
});
return response;
});
The latest commit on the fix/caching
branch applies the above code snippet. Please try it and let me know if there are still issues.
Note that with this code, there is still an unhandled exception which is very difficult to track down, but again i do not think unhandled exceptions inherently would cause any serious issues.
Thank you for the update. I merged fix/caching
into my just_audio/tree/just-audio-issue-594
branch and performed additional testing. This latest change did make significant improvements, but there are still some unwanted results. Each test below was performed multiple times and generated consistent results.
Test 1: call request()
while no connection is available
In this scenario, I disable Wi-Fi and LTE connections prior to making the request for audio. This does emit an unhandled exception within just_audio
. However, the expected result makes its way to the user application and the error while prefetching
log line from the test is printed. A subsequent request after enabling Wi-Fi or LTE succeeds.
Exception callstack:
E/flutter (10442): #0 _NativeSocket.startConnect (dart:io-patch/socket_patch.dart:681:35)
E/flutter (10442): #1 _RawSocket.startConnect (dart:io-patch/socket_patch.dart:1808:26)
E/flutter (10442): #2 RawSocket.startConnect (dart:io-patch/socket_patch.dart:27:23)
E/flutter (10442): #3 RawSecureSocket.startConnect (dart:io/secure_socket.dart:237:22)
E/flutter (10442): #4 SecureSocket.startConnect (dart:io/secure_socket.dart:60:28)
E/flutter (10442): #5 _ConnectionTarget.connect (dart:_http/http_impl.dart:2437:24)
E/flutter (10442): #6 _HttpClient._getConnection.connect (dart:_http/http_impl.dart:2808:12)
E/flutter (10442): #7 _HttpClient._getConnection (dart:_http/http_impl.dart:2813:12)
E/flutter (10442): #8 _HttpClient._openUrl (dart:_http/http_impl.dart:2697:12)
E/flutter (10442): #9 _HttpClient.getUrl (dart:_http/http_impl.dart:2574:48)
E/flutter (10442): #10 LockCachingAudioSource._fetch package:just_audio/just_audio.dart:2733
E/flutter (10442): <asynchronous suspension>
✔ This is an acceptable outcome as far as I'm concerned
Video: https://user-images.githubusercontent.com/817256/166585421-c16db6f2-c0c9-4f7d-8241-38552435ee2c.mp4
Test 2: Toggle Wi-Fi while the request is in flight
In this scenario, LTE connection is disabled and Wi-Fi is enabled. A request()
call is made to begin the download. While the download is in progress, Wi-Fi is manually disabled. There is no unhandled exception, the error log line from the test harness is printed. After re-enabling Wi-Fi and waiting a couple seconds, a new request may be started and succeed. Skipping the short wait before retrying leads to the same result as Test 1 (which makes sense).
✔ This is the ideal result
Note that this was the scenario in the initial report of this issue so we are definitely trending in the right direction.
Video: https://user-images.githubusercontent.com/817256/166586228-87be4559-e010-4ef6-89f8-faeb6adbc12f.mp4
Test 3: Toggle LTE while the request is in flight
In this scenario, LTE connection is enabled and Wi-Fi is disabled. A request()
call is made to begin the download. While the download is in progress, LTE is manually disabled. There is no unhandled exception, and the error log line from the test harness never prints. The download simply hangs and the audiosource is left in an unrecoverable state, even after re-enabling LTE and calling request()
again. This is similar to the behavior initially reported when this issue was created.
Video: https://user-images.githubusercontent.com/817256/166586462-0236bceb-d386-451e-8917-28098ecd6faa.mp4
❌ This is not the desired behavior
Additional notes:
- I also performed various tests starting from a state where both LTE and begin Wi-Fi are enabled, but I don't think those results add a lot to the simplified scenarios above.
- I expect the issue in Test 3 is caused by either another code path failing to cleanup the state of the audio source, or an underlying resource failing to timeout/emit an error at all.
Thanks, @agersant for that comprehensive report. I am not sure whether the Test 3 behaviour is due to code within this plugin or code within the network stack, but there isn't any code within this plugin that is specific to one type of network or the other, any and all errors are forwarded. If you find an extra error logged on the ExoPlayer side in the console, I can check the code to see if there is an error that I'm not forwarding from the ExoPlayer side.
But given that it seems to at least be a move in the right direction, I will publish the progress so far in a new release, and further improvements can still be made in the future.
The fixes so far have now been published on pub.dev.
@ryanheise Sorry to bring this news, but my issue is still there... I still face uncatched errors 😢
I'm facing this issue as well, in production. Just audio returns the (-1004) Could not connect to the server
after a while and it can't recover from it. After the first, it always returns the same. Seems the proxy is offline. The only way to recover from it is by restarting the entire App.
I will investigate it more today to find out why and where it is happening. I think it is related to switching Apps and when returning it is not working anymore.
Since each player has its own proxy, I think that creating a new player would be a way for the app to recover after this error, although that's only a workaround and it would be better to get to the bottom of why the proxy isn't being restarted. During the last round of bug fixes, we added some code to detect errors and set _running = false
which should cause the proxy to be restarted on the next request. One theory is that _running = false
is never actually happening in your scenario.