Reputation: 3159
I'm using just_audio with audio_service in my Flutter app for audio playback. The issue I’m encountering is when a media item URL returns a 404 error, the player goes into the idle state instead of skipping to the next track automatically.
Here’s the error I’m seeing in the logs:
E/ExoPlayerImplInternal(16375): Playback error
E/ExoPlayerImplInternal(16375): com.google.android.exoplayer2.ExoPlaybackException: Source error
E/ExoPlayerImplInternal(16375): at com.google.android.exoplayer2.ExoPlayerImplInternal.handleIoException(ExoPlayerImplInternal.java:644)
E/ExoPlayerImplInternal(16375): at com.google.android.exoplayer2.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:616)
E/ExoPlayerImplInternal(16375): at android.os.Handler.dispatchMessage(Handler.java:102)
E/ExoPlayerImplInternal(16375): at android.os.Looper.loopOnce(Looper.java:226)
E/ExoPlayerImplInternal(16375): at android.os.Looper.loop(Looper.java:313)
E/ExoPlayerImplInternal(16375): at android.os.HandlerThread.run(HandlerThread.java:67)
E/ExoPlayerImplInternal(16375): Caused by: com.google.android.exoplayer2.upstream.HttpDataSource$InvalidResponseCodeException: Response code: 404
E/ExoPlayerImplInternal(16375): at com.google.android.exoplayer2.upstream.DefaultHttpDataSource.open(DefaultHttpDataSource.java:413)
E/ExoPlayerImplInternal(16375): at com.google.android.exoplayer2.upstream.DefaultDataSource.open(DefaultDataSource.java:263)
E/ExoPlayerImplInternal(16375): at com.google.android.exoplayer2.upstream.StatsDataSource.open(StatsDataSource.java:84)
E/ExoPlayerImplInternal(16375): at com.google.android.exoplayer2.source.ProgressiveMediaPeriod$ExtractingLoadable.load(ProgressiveMediaPeriod.java:1005)
E/ExoPlayerImplInternal(16375): at com.google.android.exoplayer2.upstream.Loader$LoadTask.run(Loader.java:412)
E/ExoPlayerImplInternal(16375): at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
E/ExoPlayerImplInternal(16375): at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
E/ExoPlayerImplInternal(16375): at java.lang.Thread.run(Thread.java:920)
E/AudioPlayer(16375): TYPE_SOURCE: Response code: 404
I/ExoPlayerImpl(16375): Release b5aecbc [ExoPlayerLib/2.18.7] [d2s, SM-N975F, samsung, 31] [goog.exo.core, goog.exo.exoplayer, goog.exo.decoder, goog.exo.datasource, goog.exo.extractor]
What I’m trying to achieve:
I want the player to automatically skip to the next track when a 404 error or any other playback error occurs. The player should not skip when the user performs regular actions like pausing or stopping the playback.
Here’s my current code:
import 'dart:async';
import 'dart:ui';
import 'package:audio_service/audio_service.dart';
import 'package:audio_session/audio_session.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:just_audio/just_audio.dart';
import 'package:rxdart/rxdart.dart';
class QueueState {
static const QueueState empty =
QueueState([], 0, [], AudioServiceRepeatMode.none);
final List<MediaItem> queue;
final int? queueIndex;
final List<int>? shuffleIndices;
final AudioServiceRepeatMode repeatMode;
const QueueState(
this.queue, this.queueIndex, this.shuffleIndices, this.repeatMode);
bool get hasPrevious =>
repeatMode != AudioServiceRepeatMode.none || (queueIndex ?? 0) > 0;
bool get hasNext =>
repeatMode != AudioServiceRepeatMode.none ||
(queueIndex ?? 0) + 1 < queue.length;
List<int> get indices =>
shuffleIndices ?? List.generate(queue.length, (i) => i);
}
Future<AudioPlayerHandler> initAudioService() async {
return await AudioService.init(
builder: () => AudioPlayerHandlerImpl(),
config: const AudioServiceConfig(
androidNotificationChannelId: 'org.thitsarparami.app.audio',
androidNotificationChannelName: 'သစ္စာပါရမီ',
notificationColor: Color(0xff1751af),
androidNotificationIcon: 'mipmap/ic_launcher',
androidNotificationOngoing: true,
//androidStopForegroundOnPause: true,
),
);
}
/// An [AudioHandler] for playing a list of podcast episodes.
///
/// This class exposes the interface and not the implementation.
abstract class AudioPlayerHandler implements AudioHandler {
Stream<QueueState> get queueState;
Future<void> moveQueueItem(int currentIndex, int newIndex);
ValueStream<double> get volume;
Future<void> setVolume(double volume);
ValueStream<double> get speed;
}
/// The implementation of [AudioPlayerHandler].
///
/// This handler is backed by a just_audio player. The player's effective
/// sequence is mapped onto the handler's queue, and the player's state is
/// mapped onto the handler's state.
class AudioPlayerHandlerImpl extends BaseAudioHandler
with SeekHandler
implements AudioPlayerHandler {
// ignore: close_sinks
final BehaviorSubject<List<MediaItem>> _recentSubject =
BehaviorSubject.seeded(<MediaItem>[]);
final _mediaLibrary = MediaLibrary();
final _player = AudioPlayer();
final _playlist = ConcatenatingAudioSource(children: []);
@override
final BehaviorSubject<double> volume = BehaviorSubject.seeded(1.0);
@override
final BehaviorSubject<double> speed = BehaviorSubject.seeded(1.0);
final _mediaItemExpando = Expando<MediaItem>();
/// A stream of the current effective sequence from just_audio.
Stream<List<IndexedAudioSource>> get _effectiveSequence => Rx.combineLatest3<
List<IndexedAudioSource>?,
List<int>?,
bool,
List<IndexedAudioSource>?>(_player.sequenceStream,
_player.shuffleIndicesStream, _player.shuffleModeEnabledStream,
(sequence, shuffleIndices, shuffleModeEnabled) {
if (sequence == null) return [];
if (!shuffleModeEnabled) return sequence;
if (shuffleIndices == null) return null;
if (shuffleIndices.length != sequence.length) return null;
return shuffleIndices.map((i) => sequence[i]).toList();
}).whereType<List<IndexedAudioSource>>();
/// Computes the effective queue index taking shuffle mode into account.
int? getQueueIndex(
int? currentIndex, bool shuffleModeEnabled, List<int>? shuffleIndices) {
final effectiveIndices = _player.effectiveIndices ?? [];
final shuffleIndicesInv = List.filled(effectiveIndices.length, 0);
for (var i = 0; i < effectiveIndices.length; i++) {
shuffleIndicesInv[effectiveIndices[i]] = i;
}
return (shuffleModeEnabled &&
((currentIndex ?? 0) < shuffleIndicesInv.length))
? shuffleIndicesInv[currentIndex ?? 0]
: currentIndex;
}
/// A stream reporting the combined state of the current queue and the current
/// media item within that queue.
@override
Stream<QueueState> get queueState =>
Rx.combineLatest3<List<MediaItem>, PlaybackState, List<int>, QueueState>(
queue,
playbackState,
_player.shuffleIndicesStream.whereType<List<int>>(),
(queue, playbackState, shuffleIndices) => QueueState(
queue,
playbackState.queueIndex,
playbackState.shuffleMode == AudioServiceShuffleMode.all
? shuffleIndices
: null,
playbackState.repeatMode,
)).where((state) =>
state.shuffleIndices == null ||
state.queue.length == state.shuffleIndices!.length);
@override
Future<void> setShuffleMode(AudioServiceShuffleMode shuffleMode) async {
final enabled = shuffleMode == AudioServiceShuffleMode.all;
if (enabled) {
await _player.shuffle();
}
playbackState.add(playbackState.value.copyWith(shuffleMode: shuffleMode));
await _player.setShuffleModeEnabled(enabled);
}
@override
Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) async {
playbackState.add(playbackState.value.copyWith(repeatMode: repeatMode));
await _player.setLoopMode(LoopMode.values[repeatMode.index]);
}
@override
Future<void> setSpeed(double speed) async {
this.speed.add(speed);
await _player.setSpeed(speed);
}
@override
Future<void> setVolume(double volume) async {
this.volume.add(volume);
await _player.setVolume(volume);
}
AudioPlayerHandlerImpl() {
_init();
}
Future<void> _init() async {
final session = await AudioSession.instance;
await session.configure(const AudioSessionConfiguration.speech());
// Broadcast speed changes. Debounce so that we don't flood the notification
// with updates.
speed.debounceTime(const Duration(milliseconds: 250)).listen((speed) {
playbackState.add(playbackState.value.copyWith(speed: speed));
});
// Load and broadcast the initial queue
// await updateQueue(_mediaLibrary.items[MediaLibrary.albumsRootId]);
// For Android 11, record the most recent item so it can be resumed.
mediaItem
.whereType<MediaItem>()
.listen((item) => _recentSubject.add([item]));
// Broadcast media item changes.
Rx.combineLatest4<int?, List<MediaItem>, bool, List<int>?, MediaItem?>(
_player.currentIndexStream,
queue,
_player.shuffleModeEnabledStream,
_player.shuffleIndicesStream,
(index, queue, shuffleModeEnabled, shuffleIndices) {
final queueIndex =
getQueueIndex(index, shuffleModeEnabled, shuffleIndices);
return (queueIndex != null && queueIndex < queue.length)
? queue[queueIndex]
: null;
}).whereType<MediaItem>().distinct().listen(mediaItem.add);
// Propagate all events from the audio player to AudioService clients.
_player.playbackEventStream.listen(_broadcastState);
_player.shuffleModeEnabledStream
.listen((enabled) => _broadcastState(_player.playbackEvent));
// In this example, the service stops when reaching the end.
_player.processingStateStream.listen((state) {
if (state == ProcessingState.completed) {
stop();
_player.seek(Duration.zero, index: 0);
}
});
// In this example, the service stops when reaching the end.
// Broadcast the current queue.
_effectiveSequence
.map((sequence) =>
sequence.map((source) => _mediaItemExpando[source]!).toList())
.pipe(queue);
// Load the playlist.
_playlist.addAll(queue.value.map(_itemToSource).toList());
await _player.setAudioSource(_playlist, preload: false);
}
@override
Future<List<MediaItem>> getChildren(String parentMediaId,
[Map<String, dynamic>? options]) async {
switch (parentMediaId) {
case AudioService.recentRootId:
// When the user resumes a media session, tell the system what the most
// recently played item was.
return _recentSubject.value;
default:
// Allow client to browse the media library.
return _mediaLibrary.items[parentMediaId]!;
}
}
@override
ValueStream<Map<String, dynamic>> subscribeToChildren(String parentMediaId) {
switch (parentMediaId) {
case AudioService.recentRootId:
final stream = _recentSubject.map((_) => <String, dynamic>{});
return _recentSubject.hasValue
? stream.shareValueSeeded(<String, dynamic>{})
: stream.shareValue();
default:
return Stream.value(_mediaLibrary.items[parentMediaId])
.map((_) => <String, dynamic>{})
.shareValue();
}
}
UriAudioSource _itemToSource(MediaItem mediaItem) {
final String radioUrl = dotenv.env['RADIO_URL']!;
final uri = mediaItem.extras == null
? Uri.parse(radioUrl)
: Uri.parse(mediaItem.extras?['url']);
final audioSource = AudioSource.uri(uri, tag: mediaItem);
_mediaItemExpando[audioSource] = mediaItem;
return audioSource;
}
List<AudioSource> _itemsToSources(List<MediaItem> mediaItems) =>
mediaItems.map(_itemToSource).toList();
@override
Future<void> addQueueItem(MediaItem mediaItem) async {
await _playlist.add(_itemToSource(mediaItem));
}
@override
Future<void> addQueueItems(List<MediaItem> mediaItems) async {
await _playlist.addAll(_itemsToSources(mediaItems));
}
@override
Future<void> insertQueueItem(int index, MediaItem mediaItem) async {
await _playlist.insert(index, _itemToSource(mediaItem));
}
@override
Future<void> updateQueue(List<MediaItem> queue) async {
await _playlist.clear();
await _playlist.addAll(_itemsToSources(queue));
}
@override
// ignore: avoid_renaming_method_parameters
Future<void> updateMediaItem(MediaItem newMediaItem) async {
// rewrite updateMediaItem function because it is need to notify the mediaItem about favourite or unfavourite.
final index = queue.value.indexWhere((item) => item.id == newMediaItem.id);
if (index != -1) {
queue.value[index] = newMediaItem;
_playlist.children[index] = _itemToSource(newMediaItem);
_player.sequenceState!.sequence[index] = _itemToSource(newMediaItem);
_mediaItemExpando[_player.sequence![index]] = newMediaItem;
mediaItem.add(newMediaItem);
}
}
@override
Future<void> removeQueueItem(MediaItem mediaItem) async {
final index = queue.value.indexOf(mediaItem);
await _playlist.removeAt(index);
}
@override
Future<void> removeQueueItemAt(int index) async {
await _playlist.removeAt(index);
}
@override
Future<void> moveQueueItem(int currentIndex, int newIndex) async {
await _playlist.move(currentIndex, newIndex);
}
@override
Future<void> skipToNext() => _player.seekToNext();
@override
Future<void> skipToPrevious() => _player.seekToPrevious();
@override
Future<void> skipToQueueItem(int index) async {
if (index < 0 || index >= _playlist.children.length) return;
// This jumps to the beginning of the queue item at [index].
_player.seek(Duration.zero,
index: _player.shuffleModeEnabled
? _player.shuffleIndices![index]
: index);
}
@override
Future<void> play() => _player.play();
@override
Future<void> pause() => _player.pause();
@override
Future<void> seek(Duration position) async {
await _player.seek(position);
}
@override
Future<void> stop() async {
await _player.stop();
await playbackState.firstWhere(
(state) => state.processingState == AudioProcessingState.idle);
}
@override
Future<void> fastForward() async {
_player.seek(Duration(seconds: _player.position.inSeconds + 15));
}
@override
Future<void> rewind() async {
_player.seek(Duration(seconds: _player.position.inSeconds - 15));
}
/// Broadcasts the current state to all clients.
void _broadcastState(PlaybackEvent event) {
final playing = _player.playing;
final queueIndex = getQueueIndex(
event.currentIndex, _player.shuffleModeEnabled, _player.shuffleIndices);
playbackState.add(playbackState.value.copyWith(
controls: [
MediaControl.skipToPrevious,
if (playing) MediaControl.pause else MediaControl.play,
MediaControl.stop,
MediaControl.skipToNext,
],
systemActions: const {
MediaAction.seek,
MediaAction.seekForward,
MediaAction.seekBackward,
},
androidCompactActionIndices: const [0, 1, 3],
processingState: const {
ProcessingState.idle: AudioProcessingState.idle,
ProcessingState.loading: AudioProcessingState.loading,
ProcessingState.buffering: AudioProcessingState.buffering,
ProcessingState.ready: AudioProcessingState.ready,
ProcessingState.completed: AudioProcessingState.completed,
}[_player.processingState]!,
playing: playing,
updatePosition: _player.position,
bufferedPosition: _player.bufferedPosition,
speed: _player.speed,
queueIndex: queueIndex,
));
}
}
// Provides access to a library of media items. In your app, this could come
// from a database or web service.
class MediaLibrary {
static const albumsRootId = 'albums';
static final String radioUrl = dotenv.env['RADIO_URL'] ?? 'default_radio_url';
final items = <String, List<MediaItem>>{
};
}
Upvotes: 0
Views: 47