Stop streamed songs from wedging the player on weak connections - #209
Conversation
LockCachingAudioSource (#156, shipped in 2.2.8) routes streaming through just_audio's loopback proxy and a Dart HttpClient that sets neither a connection nor a read timeout. A stalled socket parks the proxy's byte-range request forever, so no HTTP response ever reaches the platform player, and `setAudioSource` neither completes nor throws. `_playAtIndex`'s catch never runs, and every subsequent track hangs the same way — only downloaded songs, which skip the network, still play. Go back to `setUrl` so the platform player does its own networking, and bound the load so any failure leaves a state the user can retry or skip out of: give up after `sourceLoadTimeout`, stop the player, and report AudioProcessingState.error. Play then restarts the current item rather than resuming a player that holds no source. Both player stream subscriptions now handle errors instead of leaking them into the zone.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (11)
🚧 Files skipped from review as they are similar to previous changes (10)
📝 WalkthroughWalkthrough
ChangesPlayback error recovery
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant MiniPlayer
participant KoelAudioHandler
participant AudioPlayer
User->>MiniPlayer: press play
MiniPlayer->>KoelAudioHandler: play current queue item
KoelAudioHandler->>AudioPlayer: load source with timeout
AudioPlayer-->>KoelAudioHandler: playback error or timeout
KoelAudioHandler->>AudioPlayer: stop failed source
KoelAudioHandler-->>MiniPlayer: emit error processing state
MiniPlayer-->>User: show warning icon
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
lib/audio_handler.dart (2)
103-109: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winConsider reusing
_abandonSourcefor player-emitted errors.The
onErrorhandler sets_playbackFailedand emits state, but it leaves the player running._abandonSourceperforms the same two steps and also calls_player.stop(). A player-emitted error leaves the platform player in an undefined state, so stopping it makes the retry path inplay()start from a clean state.♻️ Proposed change
_player.playbackEventStream.listen( (_) => _emitPlaybackState(), - onError: (_, __) { - _playbackFailed = true; - _emitPlaybackState(); - }, + onError: (_, __) => _abandonSource(), );Note:
_abandonSourcereturns aFuture, so confirm that the unawaited call does not trip your lint rules.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/audio_handler.dart` around lines 103 - 109, Update the playbackEventStream error handler to invoke _abandonSource so player-emitted errors also stop the player and mark playback as failed. Handle its returned Future in the project’s accepted unawaited manner, and avoid duplicating the existing failure-state updates.
369-372: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the unused error-count state.
_errorCountis incremented and reset in_playAtIndex, butMAX_ERROR_COUNTand_errorCountare never read for auto-advance logic or another branch. Keep either the count consumer or the declarations; do not keep dead state.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/audio_handler.dart` around lines 369 - 372, Remove the unused error-count state by deleting _errorCount and MAX_ERROR_COUNT, along with their increment/reset logic in _playAtIndex and the catch block. Preserve the existing _abandonSource behavior and other playback error handling.test/audio_handler_test.dart (1)
103-121: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider adding a test that a failed load does not advance the queue.
The PR description states that the change avoids automatic queue advancement after a failure. No test asserts that. A short assertion here would lock in that behavior.
💚 Proposed addition
test('does not advance the queue after a failed load', () async { final failing = registerSong(); final next = registerSong(); when(player.setUrl(failing.sourceUrl)) .thenThrow(Exception('no route to host')); await handler.replaceQueue([failing, next]); verifyNever(player.setUrl(next.sourceUrl)); expect(handler.currentQueueIndex, 0); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/audio_handler_test.dart` around lines 103 - 121, Add a test alongside the failed-load tests verifying that when the first song in a queue fails during player.setUrl, the handler does not load the next song and keeps currentQueueIndex at 0. Use distinct failing and subsequent songs, stub the failure for the first source, and assert player.setUrl was never called with the next source.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/audio_handler.dart`:
- Around line 356-360: Update the playback flow around
_setPlayerSource(mediaItem).timeout(sourceLoadTimeout) to track and validate the
current load generation after the await, preventing interrupted or stale loads
from continuing. When the generation is no longer current, await _player.stop()
before returning or taking the abandoned-source path; only call exitRadioMode,
seek, play, and _emitPlaybackState for the active generation.
In `@lib/ui/widgets/mini_player.dart`:
- Around line 251-260: Add a descriptive semanticLabel to the Icon identified by
MiniPlayer.playbackErrorIconKey in the hasFailed overlay, so screen readers
announce that playback failed while preserving the existing visual icon and
layout.
---
Nitpick comments:
In `@lib/audio_handler.dart`:
- Around line 103-109: Update the playbackEventStream error handler to invoke
_abandonSource so player-emitted errors also stop the player and mark playback
as failed. Handle its returned Future in the project’s accepted unawaited
manner, and avoid duplicating the existing failure-state updates.
- Around line 369-372: Remove the unused error-count state by deleting
_errorCount and MAX_ERROR_COUNT, along with their increment/reset logic in
_playAtIndex and the catch block. Preserve the existing _abandonSource behavior
and other playback error handling.
In `@test/audio_handler_test.dart`:
- Around line 103-121: Add a test alongside the failed-load tests verifying that
when the first song in a queue fails during player.setUrl, the handler does not
load the next song and keeps currentQueueIndex at 0. Use distinct failing and
subsequent songs, stub the failure for the first source, and assert
player.setUrl was never called with the next source.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: e268f449-3b54-4cf4-bf9b-2ee8e0c7a608
📒 Files selected for processing (11)
lib/audio_handler.dartlib/ui/widgets/mini_player.darttest/audio_handler_test.darttest/audio_handler_test.mocks.darttest/ui/screens/album_action_sheet_test.mocks.darttest/ui/screens/artist_action_sheet_test.mocks.darttest/ui/screens/downloaded_test.mocks.darttest/ui/screens/playable_action_sheet_test.mocks.darttest/ui/screens/podcast_action_sheet_test.mocks.darttest/ui/widgets/mini_player_test.darttest/ui/widgets/mini_player_test.mocks.dart
| await _setPlayerSource(mediaItem).timeout(sourceLoadTimeout); | ||
| exitRadioMode(); | ||
| _player.seek(Duration(seconds: position.toInt())); | ||
| await play(); | ||
| _emitPlaybackState(); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major
🧩 Analysis chain
🌐 Web query:
just_audio setUrl call not cancelled by stop, late completion replaces current audio source
💡 Result:
In just_audio, calling setUrl is an asynchronous operation that initiates a load process [1][2]. If a previous call to setUrl or load is still in progress when a new one is initiated, or if the player is stopped, the original pending operation may be interrupted [3][1]. When an interruption occurs, such as calling stop or initiating a new load while a previous setUrl is still loading, the original call will throw a PlayerInterruptedException [1]. This is the documented behavior in just_audio to indicate that the operation was aborted because the player state changed (e.g., the player was stopped or a new source was loaded) before the original source could complete its loading process [1]. If you are experiencing "late completion" where a previous source seems to "replace" the current one, it is often due to not properly awaiting the setUrl operation or not handling the resulting PlayerInterruptedException [4][1]. To resolve this: 1. Always await the setUrl or setAudioSource call to ensure the operation completes or throws an exception before proceeding to the next step [4][1]. 2. Wrap your loading calls in a try-catch block to specifically handle PlayerInterruptedException [1]. This allows your application to gracefully ignore the error when you intentionally trigger an interruption (such as a user cancelling a selection) [1]. 3. If you are managing rapid user interactions, ensure that you track the current active load request or use a state-management pattern that prevents outdated "in-flight" requests from overriding the intended current state once they eventually complete [1]. The library has undergone numerous fixes over time to ensure that stop and setUrl behave predictably across platforms, including ensuring that platform-side events are properly synchronized with the Flutter side to avoid race conditions [5][6][7]. If you are on an older version of just_audio, updating to the latest version is strongly recommended [6].
Citations:
- 1: connection aborted when stopped while loading ryanheise/just_audio#374
- 2: https://github.com/ryanheise/just_audio/blob/f234a9a7681ea712a6a9d64590a601af0a6b2a38/just_audio/lib/just_audio.dart
- 3: AbortError: The play() request was interrupted by a new load request. ryanheise/just_audio#1107
- 4: Exceptions when transitioning between playback states ryanheise/just_audio#28
- 5: Address event lag through platform channels ryanheise/just_audio#21
- 6: [IOS]
play()not completing whenstop()is called ryanheise/just_audio#1297 - 7: https://pub.dev/packages/just_audio/versions/0.10.4/changelog
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Files:"
git ls-files | rg '(^|/)(pubspec\.yaml|pubspec\.lock|audio_handler\.dart|.*audio.*test.*|.*audio.*test.*\.dart|.*test.*audio.*\.dart)$' || true
echo
echo "audio_handler outline:"
ast-grep outline lib/audio_handler.dart --view compact || true
echo
echo "Relevant audio_handler lines:"
sed -n '320,400p' lib/audio_handler.dart | cat -n
echo
echo "Search just_audio dependency:"
rg -n "just_audio|audio_player|playback_state|sourceLoadTimeout|_setPlayerSource|_abandonSource|setAudioSource|setUrl|stop\(" pubspec.yaml pubspec.lock lib test -SRepository: koel/player
Length of output: 11288
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "audio_handler relevant methods:"
sed -n '176,265p' lib/audio_handler.dart | cat -n
echo
echo "stalled source test:"
sed -n '96,138p' test/audio_handler_test.dart | cat -n
echo
echo "search for load generation/RequestTracker/PlayerInterruptedException handling:"
rg -n "PlayerInterruptedException|generation|request|load.*counter|stale|_setPlayerSource|setAudioSource|setUrl|stop\(\)" lib test pubspec.yaml -SRepository: koel/player
Length of output: 25882
🌐 Web query:
just_audio 0.9.32 setUrl stop PlayerInterruptedException pending future completes after stop
💡 Result:
In just_audio, a PlayerInterruptedException occurs when a pending operation—such as setUrl—is interrupted by a subsequent, conflicting operation on the same player instance [1][2][3]. If you call setUrl and then immediately call stop before the setUrl future has completed, the original setUrl future will complete with a PlayerInterruptedException [4][2][3]. This exception indicates that the loading process was aborted because another operation (in this case, stop) took precedence [2][3]. To handle this behavior correctly, you should use a try-catch block to specifically handle PlayerInterruptedException when awaiting operations like setUrl [3]: try { await player.setUrl('https://example.com/audio.mp3'); } on PlayerInterruptedException catch (e) { // Handle the interruption (e.g., the player was stopped or another // setUrl call occurred before this one finished loading). print("Loading was interrupted: ${e.message}"); } The best practice to avoid unnecessary interruptions is to always await your player methods (like setUrl) before initiating a new one [5][6]. If your application logic requires starting a new audio source while one is currently loading or playing, ensure you have managed your player state or instances appropriately, as just_audio is designed to handle one primary sequence of operations per instance [7][8][6]. If you need to play multiple sounds simultaneously, the recommended approach is to use multiple AudioPlayer instances rather than interrupting a single one [8].
Citations:
- 1: https://pub.dev/documentation/just_audio/latest/just_audio/PlayerInterruptedException-class.html
- 2: https://github.com/ryanheise/just_audio/blob/minor/just_audio/lib/just_audio.dart
- 3: https://pub.dev/documentation/just_audio/latest/
- 4: ryanheise/just_audio@e1c1587
- 5: Exceptions when transitioning between playback states ryanheise/just_audio#28
- 6: Bug: Logcat exception output ryanheise/just_audio#13
- 7: Player InterruptedException (Connection aborted) ryanheise/just_audio#1170
- 8: https://stackoverflow.com/questions/77839598/just-audio-in-flutter-need-many-instances-to-avoid-player-interruptedexception
🌐 Web query:
just_audio 0.9.32 setUrl pending stop setUrl no PlayerInterruptedException
💡 Result:
The PlayerInterruptedException is an intended behavior in just_audio designed to signal that an operation (like setUrl) was aborted because the player state changed—such as when the player is stopped, disposed, or another audio source is loaded while a previous load was still in progress [1][2]. If you encounter this exception, it generally means your application code is attempting to change the player's state (e.g., calling stop or loading a new URL) while a previous setUrl operation is still awaiting completion [1][3]. To handle or avoid this: 1. Catch the exception: You should wrap your setUrl calls in a try-catch block to specifically handle PlayerInterruptedException. This allows your app to gracefully ignore the error if it is expected behavior [1][2][3]. try { await player.setUrl("https://your-url.com"); } on PlayerInterruptedException { // Ignore this exception as it signifies an intentional interruption } catch (e) { // Handle other actual errors } 2. Ensure proper awaiting: Ensure you are correctly awaiting your setUrl and stop operations before triggering subsequent state changes. Failing to await an asynchronous stop call before initiating a new load is a common cause of this exception [1][4]. 3. State management: If your player is controlled by UI elements that can be triggered rapidly (like tabs or buttons), ensure that your logic is synchronized so that redundant or conflicting commands are not sent to the player simultaneously [4][3]. The version 0.9.32 of just_audio includes specific fixes related to hang issues when stopping while loading and handling pending timers, which may improve stability regarding these interruptions [5][6][7]. However, the core behavior of throwing PlayerInterruptedException remains the standard mechanism for managing interrupted loading sequences [1][2].
Citations:
- 1: connection aborted when stopped while loading ryanheise/just_audio#374
- 2: https://pub.dev/documentation/just_audio/latest/
- 3: https://stackoverflow.com/questions/74570132/how-to-avoid-playerinterruptedexception-connection-aborted-from-just-audio-ins
- 4: setAudioSources with preload=true throws errors ryanheise/just_audio#1485
- 5: https://github.com/ryanheise/just_audio/blob/minor/just_audio/CHANGELOG.md
- 6: https://pub.dev/packages/just_audio/changelog
- 7: https://pub.dev/packages/just_audio/versions/0.10.4/changelog
🌐 Web query:
just_audio 0.9.32 Future.timeout setUrl pending call stopped while still running
💡 Result:
When using just_audio, a Future such as setUrl can be interrupted if a new audio source is loaded, or if the player is stopped or disposed before the initial operation completes [1][2]. If you encounter a situation where an asynchronous operation like setUrl is cancelled or reports an interruption, it typically results in a PlayerInterruptedException [1]. Technical details regarding this behavior include: 1. Handling Interruptions: The library is designed to allow operations to be superseded. If you call setUrl and then immediately call stop or load a different URL, the previous pending operation will be aborted as it is no longer relevant to the current player state [1]. 2. Exception Handling: You should handle these cases explicitly using a try-catch block specifically for PlayerInterruptedException [1][2]. This distinguishes intentional interruptions (e.g., user navigated away, playback was stopped) from network errors or platform-level timeouts [1]. 3. Network Timeouts: If the "pending call stopped" behavior is associated with an error message like "The request timed out" (e.g., error -1001 on iOS), this is often a network-related issue rather than a code-level interruption [3][4]. This occurs when the underlying platform player (AVPlayer on iOS or ExoPlayer on Android) fails to fetch the media within the platform's default timeout period [3]. 4. Event Lag: There is a known, slight lag between when a method like setUrl is called in Dart and when the platform side updates the player state [5]. During this transition period, querying the playback state might return unexpected values (such as 'connecting' or 'idle') [5]. To resolve or debug this: - Ensure you are wrapping your load operations in try-catch blocks to catch PlayerInterruptedException specifically [1]. - If you suspect a network timeout, check your network environment, especially if dealing with large files or slow connections [3]. - If you are experiencing this when switching tracks, verify that your logic does not implicitly call stop or clear the player state before the next source has successfully initialized [4].
Citations:
- 1: https://pub.dev/documentation/just_audio/latest/
- 2: https://github.com/ryanheise/just_audio/blob/master/just_audio/README.md
- 3: should customize connect and read timeout when the audio file is large or network is lag sometime ryanheise/just_audio#1119
- 4: PlatformException(-1001, The request timed out., {index: 2}, null) ryanheise/just_audio#1313
- 5: Address event lag through platform channels ryanheise/just_audio#21
🌐 Web query:
"just_audio" "PlayerInterruptedException" documentation version 0.9.32
💡 Result:
In the just_audio package, PlayerInterruptedException is an exception class that indicates an operation on the audio player was interrupted by another simultaneous operation (such as loading a new audio source before the previous one finished) [1][2][3]. Documentation and usage patterns for this exception, consistent with version 0.9.32 and other versions, recommend handling it within a try-catch block when calling asynchronous methods like setUrl or load [4][5][6]. Example usage for catching this exception: try { await player.setUrl("https://example.com/audio.mp3"); } on PlayerInterruptedException catch (e) { // This call was interrupted since another audio source was loaded or the // player was stopped or disposed before this audio source could complete // loading. print("Connection aborted: ${e.message}"); } catch (e) { // Fallback for all other errors print('An error occured: $e'); } The changelog for version 0.9.32 focuses on fixes related to tag parameters in AudioSource and HLS nested URIs [7][8]; it does not introduce or fundamentally change the PlayerInterruptedException class itself, which remains a standard part of the library's error handling API [2][3].
Citations:
- 1: https://pub.dev/documentation/just_audio/latest/just_audio/
- 2: https://pub.dev/documentation/just_audio/latest/just_audio/PlayerInterruptedException-class.html
- 3: https://github.com/ryanheise/just_audio/blob/minor/just_audio/lib/just_audio.dart
- 4: https://pub.dev/documentation/just_audio/latest/index.html
- 5: https://github.com/ryanheise/just_audio/blob/minor/just_audio/README.md
- 6: https://pub.dev/packages/just_audio
- 7: https://github.com/ryanheise/just_audio/blob/minor/just_audio/CHANGELOG.md
- 8: https://pub.dev/packages/just_audio/changelog
🌐 Web query:
just_audio 0.9.32 changelog stop while loading PlayerInterruptedException
💡 Result:
The PlayerInterruptedException in just_audio is not a bug, but rather the documented behavior of the library when an ongoing operation (such as loading or playing an audio source) is interrupted by another operation (like a new load request, a stop command, or disposing of the player) before the original operation completes [1][2][3]. Regarding version 0.9.32 specifically, the changelog entries focus on fixes for tag parameters in asset/file audio sources and nested HLS URIs [4][5]. There is no changelog entry for this version indicating a change to how PlayerInterruptedException is handled. The best practice for managing this exception is to catch it explicitly in your code when performing operations that might be interrupted [1][3]. The official documentation provides the following pattern for handling this: try { await player.setUrl("https://example.com/audio.mp3"); } on PlayerInterruptedException catch (e) { // Handle the interruption (e.g., ignore it if it was expected) print("Connection aborted: ${e.message}"); } catch (e) { // Handle other types of errors print('An error occurred: $e'); } If you encounter this exception frequently, it is often due to asynchronous code triggering multiple load or play commands simultaneously [6]. Ensuring you await your player operations properly and managing state to avoid overlapping commands can often resolve or mitigate the issue [3].
Citations:
- 1: https://pub.dev/documentation/just_audio/latest/
- 2: AbortError: The play() request was interrupted by a new load request. ryanheise/just_audio#1107
- 3: connection aborted when stopped while loading ryanheise/just_audio#374
- 4: https://pub.dev/packages/just_audio/changelog
- 5: https://github.com/ryanheise/just_audio/blob/minor/just_audio/CHANGELOG.md
- 6: Player InterruptedException (Connection aborted) ryanheise/just_audio#1170
🌐 Web query:
"just_audio" "setUrl" "no longer running" "PlayerInterruptedException" 2021
💡 Result:
The PlayerInterruptedException is not a bug that was removed; rather, it is the intended behavior in the just_audio package when a loading operation (such as setUrl) is interrupted by another action [1][2]. Around 2021, the package was updated to formalize this exception handling [3][4]. Specifically, in version 0.10.1, the library was updated to explicitly throw PlayerInterruptedException instead of a generic PlatformException [3] when an operation is cancelled (for example, if a new audio source is loaded or the player is stopped before the previous one completes loading) [1][2][5]. To resolve issues where this exception disrupts your application flow, you should wrap your loading calls in a try-catch block and specifically catch the PlayerInterruptedException [1][6][2]: try { await player.setUrl("https://example.com/audio.mp3"); } on PlayerInterruptedException catch (e) { // This exception occurs when the operation is interrupted. // You can safely ignore it if your app logic handles the interruption. print("Connection aborted: ${e.message}"); } catch (e) { // Handle other unexpected errors print("An error occurred: $e"); } If you continue to see this error frequently, ensure that you are properly awaiting your asynchronous calls (e.g., await player.setUrl(...)) [1][6] and check if your UI logic is triggering multiple conflicting playback commands simultaneously (such as rapid tab switching or repeated play/stop calls) [6][7].
Citations:
- 1: connection aborted when stopped while loading ryanheise/just_audio#374
- 2: https://github.com/ryanheise/just_audio/blob/master/just_audio/README.md
- 3: ryanheise/just_audio@e1c1587
- 4: https://pub.dev/packages/just_audio/versions/0.10.4/changelog
- 5: https://pub.dev/documentation/just_audio/latest/just_audio/
- 6: https://stackoverflow.com/questions/74570132/how-to-avoid-playerinterruptedexception-connection-aborted-from-just-audio-ins
- 7: Player InterruptedException (Connection aborted) ryanheise/just_audio#1170
🌐 Web query:
"just_audio" "setUrl" "stopped" "PlayerInterruptedException" "pending" "never completed"
💡 Result:
The PlayerInterruptedException in the just_audio Flutter package is not a bug, but rather intended behavior indicating that an audio loading operation was interrupted before it could complete [1][2]. This occurs when a new load request (e.g., calling setUrl or setAudioSource) is initiated while a previous load request is still in progress, or if the player is stopped or disposed during the loading process [3][4][2]. To handle this exception, you should wrap your audio loading calls in a try-catch block and specifically catch PlayerInterruptedException [3][5]. Example implementation: try { await player.setUrl("https://example.com/audio.mp3"); } on PlayerInterruptedException catch (e) { // Handle the interruption (e.g., log it or simply ignore it) print("Connection aborted: ${e.message}"); } catch (e) { // Handle other types of errors print('An error occurred: $e'); } Recommendations to avoid or manage this: 1. Ensure proper awaiting: Always use the await keyword when calling methods like setUrl or load to ensure operations complete before moving to the next step [6][1]. 2. Avoid rapid load requests: If your app logic triggers multiple play requests in rapid succession (e.g., during UI transitions or tab switching), check the player state or implement logic to prevent redundant load requests [7][6][2]. 3. Graceful handling: Since this exception is often a natural consequence of user interaction (like changing a screen or clicking play on a new file before the previous one finished loading), catching it and suppressing it is the standard, documented approach [1][2]. If the player appears to be stuck in a pending or loading state despite your attempts to handle it, ensure your code is not creating a race condition where multiple load commands are fighting for the player's internal state [2][8].
Citations:
- 1: connection aborted when stopped while loading ryanheise/just_audio#374
- 2: AbortError: The play() request was interrupted by a new load request. ryanheise/just_audio#1107
- 3: https://github.com/ryanheise/just_audio/blob/master/just_audio/README.md
- 4: https://github.com/ryanheise/just_audio/blob/minor/just_audio/lib/just_audio.dart
- 5: https://pub.dev/documentation/just_audio/latest/
- 6: https://stackoverflow.com/questions/74570132/how-to-avoid-playerinterruptedexception-connection-aborted-from-just-audio-ins
- 7: Player InterruptedException (Connection aborted) ryanheise/just_audio#1170
- 8: Race Condition - Short multiple network requests (<1 second) can cause playing state not being update ryanheise/just_audio#1153
🌐 Web query:
"just_audio" "0.9.32" "setUrl" "completed" "PlayerInterruptedException" "setUrl" "completed successfully" "pending"
💡 Result:
The PlayerInterruptedException in the just_audio package occurs when a player operation (such as setUrl, load, or play) is interrupted by another simultaneous operation, such as the player being stopped, disposed, or a new audio source being loaded before the previous one finished loading [1][2][3]. In just_audio version 0.9.32 and later, this exception is specifically used to signal that a loading operation was aborted because the state of the player changed (e.g., a new source was requested) [4][2]. To handle this exception, you should wrap your audio loading and playback calls in a try-catch block and specifically catch PlayerInterruptedException [1][3]. This allows your application to gracefully handle cases where, for example, a user rapidly navigates between screens or triggers multiple play requests in quick succession [5][6]. Example of proper handling: try { await player.setUrl("https://your-audio-url.mp3"); } on PlayerInterruptedException catch (e) { // This call was interrupted; log it or ignore it if expected print("Connection aborted: ${e.message}"); } catch (e) { // Handle other types of errors print('An error occurred: $e'); } If you encounter this exception frequently, it is often a sign of asynchronous race conditions in your code where multiple calls to player methods are overlapping [6]. Ensure that your logic properly awaits previous operations or uses state checks (e.g., checking the player's processing state) before initiating new ones [5][6].
Citations:
- 1: https://pub.dev/documentation/just_audio/latest/
- 2: https://pub.dev/documentation/just_audio/latest/just_audio/PlayerInterruptedException-class.html
- 3: https://github.com/ryanheise/just_audio/blob/master/just_audio/README.md
- 4: ryanheise/just_audio@e1c1587
- 5: https://stackoverflow.com/questions/74570132/how-to-avoid-playerinterruptedexception-connection-aborted-from-just-audio-ins
- 6: Player InterruptedException (Connection aborted) ryanheise/just_audio#1170
🌐 Web query:
"just_audio" "0.9.32" "Futures" "timeout" "isCompleted" "PlayerInterruptedException"
💡 Result:
In just_audio, a PlayerInterruptedException occurs when a pending operation on the audio player is interrupted by another simultaneous operation, such as loading a new audio source, stopping the player, or disposing of it before the original operation could complete [1][2][3]. In version 0.9.32, this behavior is a standard part of the library's error handling design, where asynchronous operations like load are explicitly aborted if the state changes before they finish [4][3]. How to Handle PlayerInterruptedException: The recommended way to deal with this error is to catch the exception specifically using a try-catch block around your audio operations [2][5]. This is particularly necessary if you are triggering audio loads based on UI interactions (like switching tabs or rapid button clicks) where a previous operation might still be in progress [6]. Example implementation: try { await player.setUrl("https://your-audio-url.mp3"); await player.load; } on PlayerInterruptedException catch (e) { // This call was interrupted because another audio source was loaded, // the player was stopped, or disposed of. // You can safely ignore this or log it for debugging purposes. print("Operation interrupted: ${e.message}"); } catch (e) { // Handle other types of errors print("An unexpected error occurred: $e"); } Common Scenarios for Interruption: 1. UI Transitions: If you trigger an audio load inside a tab change or navigation event, the previous load might still be executing, causing the library to interrupt it to prioritize the new, current state [6]. 2. Rapid Sequential Calls: If you call load or setUrl multiple times in rapid succession, only the latest call is guaranteed to complete, while previous ones will throw PlayerInterruptedException [7][3]. If you are experiencing timeouts (e.g., "The request timed out") in addition to interruptions, note that these are separate from PlayerInterruptedException and typically relate to network conditions [8][9]. While Dart's Future.timeout can be used to limit how long you wait for a player operation, it does not prevent the underlying interruption exception [10]. If you are facing persistent issues with this exception in version 0.9.32, it is often a sign of race conditions in your app's state management. Ensure that your audio player logic is properly synchronized and that you are not initiating multiple conflicting load operations simultaneously [7][11].
Citations:
- 1: https://pub.dev/documentation/just_audio/latest/just_audio/
- 2: https://pub.dev/documentation/just_audio/latest/
- 3: https://github.com/ryanheise/just_audio/blob/f234a9a7681ea712a6a9d64590a601af0a6b2a38/just_audio/lib/just_audio.dart
- 4: ryanheise/just_audio@e1c1587
- 5: Unified error handling ryanheise/just_audio#541
- 6: https://stackoverflow.com/questions/74570132/how-to-avoid-playerinterruptedexception-connection-aborted-from-just-audio-ins
- 7: Player InterruptedException (Connection aborted) ryanheise/just_audio#1170
- 8: should customize connect and read timeout when the audio file is large or network is lag sometime ryanheise/just_audio#1119
- 9: PlatformException(-1001, The request timed out., {index: 2}, null) ryanheise/just_audio#1314
- 10: https://api.flutter.dev/flutter/dart-async/Future/timeout.html
- 11: Race Condition - Short multiple network requests (<1 second) can cause playing state not being update ryanheise/just_audio#1153
Handle or guard _setPlayerSource interruptions before using sourceLoadTimeout.
Future.timeout(...) only drops the await at Dart; it does not change the player state. In this flow, a stalled setUrl can leave _setPlayerSource awaiting while other state has moved on, and the surrounding catch turns interruption into the generic abandoned-source path. Apply a load generation guard when awaiting the future, then await _player.stop() before proceeding.
[low_effort_or_unknown]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/audio_handler.dart` around lines 356 - 360, Update the playback flow
around _setPlayerSource(mediaItem).timeout(sourceLoadTimeout) to track and
validate the current load generation after the await, preventing interrupted or
stale loads from continuing. When the generation is no longer current, await
_player.stop() before returning or taking the abandoned-source path; only call
exitRadioMode, seek, play, and _emitPlaybackState for the active generation.
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
lib/audio_handler.dart (2)
362-363: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAwait the restored-position seek before starting playback.
AudioPlayer.seek(Duration)is asynchronous; callawait _player.seek(...)so_playAtIndex()starts after the restored position has been applied.Suggested ordering
- _player.seek(Duration(seconds: position.toInt())); + await _player.seek(Duration(seconds: position.toInt()));🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/audio_handler.dart` around lines 362 - 363, Update the restored-position flow in _playAtIndex() to await _player.seek(...) before calling play(), ensuring playback starts only after the seek completes.Source: MCP tools
106-114: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winSubscribe to
errorStreamfor player failures.
playbackEventStreamcarriesPlaybackEventvalues for playback state; it is not the documented path forPlayerExceptionplayer errors.AudioPlayer.errorStreamis the recommended source for decoder/network failures, and those should set_playbackFailedand emit the error state. Add a test case that emits an error through the mockederrorStream.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/audio_handler.dart` around lines 106 - 114, Update _subscribeToPlayerPlaybackEvents to listen to AudioPlayer.errorStream for PlayerException failures instead of relying on playbackEventStream's onError callback; set _playbackFailed and call _emitPlaybackState when an error is received. Add a test using the mocked errorStream to verify the failure state is emitted.Source: MCP tools
🧹 Nitpick comments (1)
lib/audio_handler.dart (1)
108-112: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse descriptive callback and catch parameter names.
The stream callbacks use
_and__. The restoration catch usese. Rename them to names such asstreamError,stackTrace, andrestoreError.As per coding guidelines, do not use single-letter variable names except
i,j, andh; use names that encode intent.Also applies to: 175-177, 202-202
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/audio_handler.dart` around lines 108 - 112, Rename the stream error callback parameters in _emitPlaybackState-related handling from _ and __ to descriptive names such as streamError and stackTrace, and rename the restoration catch parameter from e to restoreError. Apply these naming changes consistently at the referenced callback and catch sites without changing behavior.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@ios/Runner/Info.plist`:
- Around line 29-32: Update the LSApplicationQueriesSchemes array in the plist
to use the scheme name https without the trailing colon, replacing the current
https: entry.
In `@lib/audio_handler.dart`:
- Around line 373-378: Update the playback flow around _playAtIndex so
_errorCount is reset immediately after the track becomes ready and before
awaiting play(). Ensure every play() attempt resets the counter, including
attempts that fail, while preserving the existing _errorCount++ handling for
skipped attempts and error recovery.
In `@lib/main.dart`:
- Line 98: Update the KoelAudioHandler construction in the builder to use the
documented production sourceLoadTimeout of 30 seconds instead of the current
5-second override, preserving the handler’s intended slow-connection behavior.
---
Outside diff comments:
In `@lib/audio_handler.dart`:
- Around line 362-363: Update the restored-position flow in _playAtIndex() to
await _player.seek(...) before calling play(), ensuring playback starts only
after the seek completes.
- Around line 106-114: Update _subscribeToPlayerPlaybackEvents to listen to
AudioPlayer.errorStream for PlayerException failures instead of relying on
playbackEventStream's onError callback; set _playbackFailed and call
_emitPlaybackState when an error is received. Add a test using the mocked
errorStream to verify the failure state is emitted.
---
Nitpick comments:
In `@lib/audio_handler.dart`:
- Around line 108-112: Rename the stream error callback parameters in
_emitPlaybackState-related handling from _ and __ to descriptive names such as
streamError and stackTrace, and rename the restoration catch parameter from e to
restoreError. Apply these naming changes consistently at the referenced callback
and catch sites without changing behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 4fa033d6-5d1f-4fc8-a5ce-0f5b8cda3ec6
⛔ Files ignored due to path filters (1)
ios/Podfile.lockis excluded by!**/*.lock
📒 Files selected for processing (7)
ios/Flutter/AppFrameworkInfo.plistios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcschemeios/Runner/AppDelegate.swiftios/Runner/Info.plistlib/audio_handler.dartlib/main.darttest/audio_handler_test.dart
💤 Files with no reviewable changes (1)
- ios/Flutter/AppFrameworkInfo.plist
🚧 Files skipped from review as they are similar to previous changes (1)
- test/audio_handler_test.dart
| <key>LSApplicationQueriesSchemes</key> | ||
| <array> | ||
| <string>https:</string> | ||
| </array> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
plist="ios/Runner/Info.plist"
python3 - "$plist" <<'PY'
import plistlib
import sys
from pathlib import Path
plist_path = Path(sys.argv[1])
with plist_path.open("rb") as stream:
values = plistlib.load(stream)
schemes = values.get("LSApplicationQueriesSchemes", [])
assert "https" in schemes, f"Missing exact https scheme: {schemes!r}"
assert "https:" not in schemes, f"Invalid colon-suffixed scheme: {schemes!r}"
print(schemes)
PY
rg -nP '\b(canOpenURL|canLaunchUrl|canLaunch)\b|LSApplicationQueriesSchemes' \
-g '*.swift' -g '*.m' -g '*.mm' -g '*.dart' . || trueRepository: koel/player
Length of output: 278
🌐 Web query:
Apple LSApplicationQueriesSchemes scheme values cannot include colons https: documentation
💡 Result:
Yes, entries in the LSApplicationQueriesSchemes array should be the URL scheme only, not the full URL or the scheme with a trailing colon [1][2]. The LSApplicationQueriesSchemes key is used specifically with the canOpenURL: method to check if an app capable of handling a specific URL scheme is installed on the device [3][1][4]. According to Apple's requirements, you must declare the scheme itself as a string within the array [3][1]. Adding characters such as colons (e.g., "scheme://") or providing a full URL (e.g., "scheme://path") is incorrect and can result in the query failing because the system is looking specifically for the scheme identifier [2]. For example, if you want to check for the scheme "instagram", your Info.plist should contain: LSApplicationQueriesSchemes instagram It is also important to note that since iOS 15, there is a hard limit of 50 entries permitted in the LSApplicationQueriesSchemes array [5]. Apps attempting to query for schemes not declared in this list—or using the incorrect format—will have their calls to canOpenURL: return false [1][5].
Citations:
- 1: https://mas.owasp.org/MASTG/knowledge/ios/MASVS-PLATFORM/MASTG-KNOW-0079/
- 2: https://stackoverflow.com/questions/32870393/canopenurl-this-app-is-not-allowed-to-query-for-scheme-instragram
- 3: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/LaunchServicesKeys.html
- 4: https://developer.apple.com/forums/thread/732488
- 5: https://developer.apple.com/forums/thread/691156
Use https, not https: in LSApplicationQueriesSchemes.
LSApplicationQueriesSchemes expects URL scheme names without the trailing colon. On iOS, a colon-suffixed value can make canOpenURL queries for HTTPS URLs fail because the query does not match the declared scheme.
Replace https: with https in ios/Runner/Info.plist.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ios/Runner/Info.plist` around lines 29 - 32, Update the
LSApplicationQueriesSchemes array in the plist to use the scheme name https
without the trailing colon, replacing the current https: entry.
Source: MCP tools
A 404, a corrupt file or a codec the device won't decode says nothing about the rest of the queue, so stopping dead on one is just an obstacle. A stalled load says the opposite: the connection is gone and the next song would stall identically, 30 seconds at a time, costing the user their place in the queue for nothing. Skip on the former, hold position on the latter, and cap consecutive failures via MAX_ERROR_COUNT — which existed but was never read. Lowered it to 3: a run that long is systematic, and skipping further only buries the cause. Also from review: reuse _abandonSource for player-emitted errors so they stop the player too, await the restored-position seek before starting playback, and label the error icon for screen readers.
863f7fd to
43381c3
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
|
Thanks — took four, rejecting three. Applied
Not applied
Left The |
Streamed playback gets stuck on "loading" indefinitely on weak or unstable connections, and stays stuck: restoring the connection doesn't help, and skipping to the next song hangs the same way. Only downloaded songs still play. Reported against 2.2.8/2.2.9.
Cause
#156 replaced
setUrlwithLockCachingAudioSource, which routes streaming through just_audio's loopback proxy and a DartHttpClientthat sets neither a connection nor a read timeout. When a socket stalls mid-response:_requests, so no HTTP response ever reaches the platform player;AudioPlayer._loadsits onawait processingStateStream.firstWhere((state) => state != loading), sosetAudioSourceneither completes nor throws;_playAtIndex'scatchtherefore never runs, and every subsequent track hangs identically.Downloaded songs take the
setFilePathbranch, never touch the network, and were unaffected — which is what made the failure look selective.Fix
Go back to
setUrlso the platform player does its own networking; AVPlayer and ExoPlayer have real timeouts, stall detection and range-request retry, none of which the proxy path provides.Bound the load regardless of source, so a failure leaves a state the user can act on:
sourceLoadTimeout(30s);_player.stop()— this also aborts just_audio's pending_load, which otherwise stays parked;AudioProcessingState.error, so the mini player shows a warning icon instead of spinning forever;play()restarts the current item rather than resuming a player that holds no source.Both player stream subscriptions now handle errors instead of leaking them into the zone.
On failure, skip or hold?
Depends on what failed, so the two are split:
TimeoutException. The next song stalls identically at 30s a time, and each skip costs the user their place in the queue. Hold position and report the error.MAX_ERROR_COUNTcaps consecutive failures either way. It existed but was never read; lowered from 10 to 3, since a run that long is systematic and skipping further only buries the cause. A song that plays clears the count.Tests
test/audio_handler_test.dart(new, needs the player injectable) covers streamed vs. downloaded source selection, a source that never becomes playable, a source that throws, skip-vs-hold per error type, the consecutive-failure cap and its reset, the end of the queue, play-as-retry, and a player-emitted error.test/ui/widgets/mini_player_test.dart(new) covers the error icon vs. spinner vs. neither.Summary by CodeRabbit
New Features
Bug Fixes
Tests