Closed andy-t-wang closed 3 years ago
HI @andy-t-wang Can you please
Which API doesn't behave as documented, and how does it misbehave? Name here the specific methods or fields that are not behaving as documented, and explain clearly what is happening.
Can you please fill in the above section?
Minimal reproduction project
child: StreamBuilder<MediaItem?>( stream: AudioService.currentMediaItemStream, builder: (context, snapshot) { final mediaItem = AudioService.currentMediaItem;
The audioplayertask code is the same as the example. I'm assuming the stream gets set at the onStart.
Can you please provide a link to your fork of audio_service that reproduces this issue?
@ryanheise Ok added the api. Not sure what you mean the fork of audio_serivce I'm just using this one from the flutter pages. Library Link. I didnt make any changes to the library. I know the queue is getting successfully update because things can play so the service is working for that, however, only the currentmediaitemstream and currentmediaitem are always null. This wasn't a problem when I used 0.16.2+1 version. After upgrading its a problem for only android, the currentmediaItemstream returns the mediaitem on my ios device
On GitHub, click the button to fork this project, then you will have your own repo with a copy of the whole code. Then, modify the example in the example subdirectory with minimal changes necessary to reproduce the bug. I will then be able to git clone your repo and run it. Then provide me with user steps on what I need to click on to cause the bug, and what I should expect to see in logs, etc.
Any update? I have gone ahead and released 0.17.0 now. If there is a bug, it will need to be fixed in a bug-fix release, but that will also depend on your reproduction project.
Yeah I'm still figuring it out I tried the new package but I'm struggling to make a repo to reproduce it. Which is weird since it works fine on the old version.
Hello. I have a similar problem. Mine is that my ui state is not updating
@andy-t-wang as soon as you figure out how to reproduce it, that will be really helpful.
I've had what seems to be the same problem after upgrading to the null safe version. Both of these give null values:
AudioService.currentMediaItem
AudioService.currentMediaItemStream
I'm still trying to find the source of the problem. So far I've discovered that the example project with audio_service is working fine on the same Android device that my own project is not working on. This leads me to to think that it is something about how I initialize the playlist. I'll update again when I find the answer.
Interesting, yes this is something I've been curious about. I've seen it mentioned on StackOverflow but as yet I haven't seen it or been able to reproduce it myself.
I haven't been able to isolate the problem in my own code yet but it is interesting to note that the web version works fine while Android does not.
My next step is going to be to slowly add things into the example project until I find where it stops working.
This is my background audio service class:
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:audio_session/audio_session.dart';
import 'package:just_audio/just_audio.dart';
void audioPlayerTaskEntrypoint() async {
AudioServiceBackground.run(() => AudioPlayerTask());
}
class AudioPlayerTask extends BackgroundAudioTask {
AudioPlayer _player = AudioPlayer();
AudioProcessingState? _skipState;
late StreamSubscription<PlaybackEvent> _eventSubscription;
List<MediaItem> _queue = [];
int? get index => _player.currentIndex;
MediaItem? get mediaItem => index == null ? null : _queue[index!];
@override
Future<void> onStart(Map<String, dynamic>? params) async {
_loadMediaItemsIntoQueue(params);
await _setAudioSession();
// _broadcaseMediaItemChanges();
_propogateEventsFromAudioPlayerToAudioServiceClients();
_performSpecialProcessingForStateTransistions();
_loadQueue();
}
void _loadMediaItemsIntoQueue(Map<String, dynamic>? params) {
_queue.clear();
final List mediaItems = params?['data'];
for (var item in mediaItems) {
final mediaItem = MediaItem.fromJson(item);
_queue.add(mediaItem);
}
}
Future<void> _setAudioSession() async {
final session = await AudioSession.instance;
await session.configure(AudioSessionConfiguration.music());
}
// void _broadcaseMediaItemChanges() {
// _player.currentIndexStream.listen((index) {
// if (index != null) AudioServiceBackground.setMediaItem(_queue[index]);
// });
// }
void _propogateEventsFromAudioPlayerToAudioServiceClients() {
_eventSubscription = _player.playbackEventStream.listen((event) {
_broadcastState();
});
}
void _performSpecialProcessingForStateTransistions() {
_player.processingStateStream.listen((state) {
switch (state) {
case ProcessingState.completed:
_player.pause();
_player.seek(Duration.zero, index: _player.effectiveIndices?.first);
break;
case ProcessingState.ready:
_skipState = null;
break;
default:
break;
}
});
}
Future<void> _loadQueue() async {
AudioServiceBackground.setQueue(_queue);
try {
await _player.setAudioSource(ConcatenatingAudioSource(
useLazyPreparation: true,
children: _queue.map((item) {
final uri = Uri.parse(item.extras?['url']);
return AudioSource.uri(uri);
}).toList(),
));
_player.durationStream.listen((duration) {
_updateQueueWithCurrentDuration(duration);
});
//onPlay();
} on PlayerException catch (e) {
print("Error code: ${e.code}");
print("Error message: ${e.message}");
onStop();
} on PlayerInterruptedException catch (e) {
print("Connection aborted: ${e.message}");
onStop();
} catch (e) {
print('Error: $e');
onStop();
}
}
void _updateQueueWithCurrentDuration(Duration? duration) {
print('updating with new duration: $duration');
final songIndex = _player.currentIndex;
print('songIndex: $songIndex');
if (songIndex == null || mediaItem == null || duration == null) {
print('mediaItem: $mediaItem');
return;
}
print(
'current index: $songIndex, id: ${mediaItem!.id}, duration: $duration');
final modifiedMediaItem = mediaItem!.copyWith(duration: duration);
_queue[songIndex] = modifiedMediaItem;
AudioServiceBackground.setQueue(_queue);
AudioServiceBackground.setMediaItem(_queue[songIndex]);
}
@override
Future<void> onSkipToQueueItem(String mediaId) async {
final newIndex = _queue.indexWhere((item) => item.id == mediaId);
if (newIndex == -1 || index == null) return;
_skipState = newIndex > index!
? AudioProcessingState.skippingToNext
: AudioProcessingState.skippingToPrevious;
_player.seek(Duration.zero, index: newIndex);
}
@override
Future<void> onPlay() => _player.play();
@override
Future<void> onPause() => _player.pause();
@override
Future<void> onSeekTo(Duration position) => _player.seek(position);
@override
Future<void> onFastForward() => _seekRelative(fastForwardInterval);
@override
Future<void> onRewind() => _seekRelative(-rewindInterval);
@override
Future<void> onSetRepeatMode(AudioServiceRepeatMode repeatMode) async {
switch (repeatMode) {
case AudioServiceRepeatMode.none:
await _player.setLoopMode(LoopMode.off);
break;
case AudioServiceRepeatMode.one:
await _player.setLoopMode(LoopMode.one);
break;
case AudioServiceRepeatMode.all:
case AudioServiceRepeatMode.group:
await _player.setLoopMode(LoopMode.all);
break;
}
}
@override
Future<void> onStop() async {
await _player.dispose();
_eventSubscription.cancel();
await _broadcastState();
await super.onStop();
}
/// Jumps away from the current position by [offset].
Future<void> _seekRelative(Duration offset) async {
if (mediaItem?.duration == null) return;
var newPosition = _player.position + offset;
if (newPosition < Duration.zero) newPosition = Duration.zero;
if (newPosition > mediaItem!.duration!) newPosition = mediaItem!.duration!;
await _player.seek(newPosition);
}
/// Broadcasts the current state to all clients.
Future<void> _broadcastState() async {
await AudioServiceBackground.setState(
controls: [
MediaControl.skipToPrevious,
if (_player.playing) MediaControl.pause else MediaControl.play,
MediaControl.skipToNext,
],
androidCompactActions: [0, 1, 2],
processingState: _getProcessingState(),
playing: _player.playing,
position: _player.position,
bufferedPosition: _player.bufferedPosition,
speed: _player.speed,
);
}
/// Maps just_audio's processing state into into audio_service's playing
/// state. If we are in the middle of a skip, we use [_skipState] instead.
AudioProcessingState _getProcessingState() {
if (_skipState != null) return _skipState!;
switch (_player.processingState) {
case ProcessingState.idle:
return AudioProcessingState.stopped;
case ProcessingState.loading:
return AudioProcessingState.connecting;
case ProcessingState.buffering:
return AudioProcessingState.buffering;
case ProcessingState.ready:
return AudioProcessingState.ready;
case ProcessingState.completed:
return AudioProcessingState.completed;
default:
throw Exception("Invalid state: ${_player.processingState}");
}
}
}
And this is my state management class:
import 'package:audio_service/audio_service.dart';
import 'package:myapp/models/song.dart';
import 'package:myapp/services/audio_service/background_audio_service.dart';
import 'package:rxdart/rxdart.dart';
import 'package:url_launcher/url_launcher.dart';
import 'audio_buttons/repeat_button_notifier.dart';
import 'favorites_row/favorite_button_notifier.dart';
import 'lyrics/lyrics_notifier.dart';
import 'seek_bar/seek_bar_notifier.dart';
class PlayPageState {
final lyricsNotifier = LyricsNotifier();
final repeatButtonNotifier = RepeatButtonNotifier();
final favoriteButtonNotifier = FavoriteButtonNotifier();
List<Song> get playlist => _playlist;
List<Song> _playlist = [];
Stream<PlaybackState> get playbackStateStream =>
AudioService.playbackStateStream; //.map((state) => state.playing).distinct();
Stream<bool> get isFirstMediaItemStream => AudioService.currentMediaItemStream
.map((mediaItem) => mediaItem == AudioService.queue?.first)
.distinct();
Stream<bool> get isLastMediaItemStream => AudioService.currentMediaItemStream
.map((mediaItem) => mediaItem == AudioService.queue?.last)
.distinct();
Stream<AudioPlayerState> get mediaStateStream =>
Rx.combineLatest3<Duration, PlaybackState, MediaItem?, AudioPlayerState>(
AudioService.positionStream,
AudioService.playbackStateStream,
AudioService.currentMediaItemStream,
(position, playbackState, mediaItem) {
lyricsNotifier.updateLyrics(mediaItem?.id, position);
return AudioPlayerState(
current: position,
buffered: playbackState.bufferedPosition,
total: mediaItem?.duration,
);
});
Future<void> initAudioService(List<Song> playlist) async {
_playlist = playlist;
// load the lyrics
lyricsNotifier.updateLyrics(playlist[0].id, Duration.zero);
// load the favorite state
favoriteButtonNotifier.lookupSavedFavoriteStatus(playlist[0].id);
// set up the media service
final mediaList = [];
for (var song in playlist) {
final mediaItem = MediaItem(
id: song.id,
title: song.name,
album: '',
extras: {'url': song.url},
);
mediaList.add(mediaItem.toJson());
}
if (mediaList.isEmpty) return;
final params = {'data': mediaList};
AudioService.start(
backgroundTaskEntrypoint: audioPlayerTaskEntrypoint,
androidStopForegroundOnPause: true,
params: params,
);
_updateFavoriteStatus();
}
void _updateFavoriteStatus() {
AudioService.currentMediaItemStream.listen((mediaItem) {
if (mediaItem == null) return;
favoriteButtonNotifier.lookupSavedFavoriteStatus(mediaItem.id);
});
}
void onFavoriteButtonPressed() {
print('currentMediaItem: ${AudioService.currentMediaItem}');
final currentSongId = AudioService.currentMediaItem?.id;
if (currentSongId == null) {
return;
}
final song = playlist.firstWhere((song) => song.id == currentSongId);
favoriteButtonNotifier.toggleState(song);
}
void onRepeatButtonPressed() {
repeatButtonNotifier.changeState();
final repeatState = repeatButtonNotifier.value;
switch (repeatState) {
case RepeatState.off:
AudioService.setRepeatMode(AudioServiceRepeatMode.none);
break;
case RepeatState.repeatSong:
AudioService.setRepeatMode(AudioServiceRepeatMode.one);
break;
case RepeatState.repeatPlaylist:
AudioService.setRepeatMode(AudioServiceRepeatMode.all);
break;
}
}
void onPreviousSongButtonPressed() {
if (!AudioService.running) {
return;
}
AudioService.skipToPrevious();
}
Future<void> onPlayPauseButtonPressed() async {
if (!AudioService.running) {
return;
}
if (AudioService.playbackState.playing) {
AudioService.pause();
} else {
AudioService.play();
}
}
void onNextSongButtonPressed() {
if (!AudioService.running) {
return;
}
AudioService.skipToNext();
}
Future<void> disposeAudioService() async {
AudioService.stop();
}
void onAudioPlayerSeek(Duration position) {
AudioService.seekTo(position);
}
skipToSong(Song song) {
if (!AudioService.running) {
return;
}
// load the lyrics right away singe this may be faster than AudioService
//lyricsNotifier.updateLyrics(song.id, Duration.zero);
AudioService.skipToQueueItem(song.id);
}
void downloadCurrentSong() async {
final url = AudioService.currentMediaItem?.extras?['url'];
if (url == null) return;
if (await canLaunch(url)){
await launch(url);
}
}
}
I can confirm there is an issue with currentMediaItemStream
. I have just updated to 0.17.0
and now that broadcast stream is dead. I will debug it tomorrow to see what caused this.
I've found the issue. The culprit is this function:
private static String metadataToString(MediaMetadataCompat mediaMetadata, String key) {
CharSequence value = mediaMetadata.getText(key);
if (value != null && value.length() > 0)
return value.toString();
return null;
}
when we define album
as an empty String, it gets back as null
from the Android native code because value.length() == 0
.
However, because album
is marked as not nullable now, it throws an exception in the dart side in the following function:
factory MediaItem.fromJson(Map raw) => MediaItem(
id: raw['id'],
album: raw['album'],
title: raw['title'],
artist: raw['artist'],
genre: raw['genre'],
duration: raw['duration'] != null
? Duration(milliseconds: raw['duration'])
: null,
artUri: raw['artUri'] != null ? Uri.parse(raw['artUri']) : null,
playable: raw['playable'],
displayTitle: raw['displayTitle'],
displaySubtitle: raw['displaySubtitle'],
displayDescription: raw['displayDescription'],
rating: raw['rating'] != null ? Rating._fromRaw(raw['rating']) : null,
extras: raw['extras']?.cast<String, dynamic>(),
);
I will create a PR tomorrow to fix this.
@ryanheise before proceeding with the PR, can you give me more context on why metadataToString
function is casting empty strings to null
?
Ah, interesting. I'll look into it now, but this also suggests a workaround for everybody which is to set the album string to a non-empty whitespace string (e.g. " "
- with one space).
OK, so I think from memory that I noticed a bug in Android where it was unable to distinguish between an empty string and a null. But looking at the code, I honestly can't remember the finer details of that now. Maybe Android was returning empty strings for some metadata even though the app may have set them as nulls, or maybe it was something else.
Note that metadataToString
is used not only for title
but also nullable metadata like displayTitle
.
Overall, the ideal solution is one that preserves the null or empty string value from the time it was inputted to the time it is received through broadcasts. In the one-isolate
branch this is less of an issue because we now bypass the native layer and the UI listens directly to the same stream that the audio logic emits, in the same isolate. But in the master
branch it does go through the native layer. If you print out the values of these null and empty string metadata values before and after they go through this native layer, you might be able to confirm what is happening here.
For example:
@Override
public void onMetadataChanged(MediaMetadataCompat metadata) {
invokeMethod("onMediaChanged", mediaMetadata2raw(metadata));
}
If you log at this point, you will be able to see if Android has altered the null or empty string metadata before we deliver that event to the Flutter app.
Found the original issue for context: #363
Thanks @ryanheise. I created a PR to address this. I am already using a version with that fix and testing it on my real devices. I will let you know if I find any issue.
Hello I'm also facing an issue about the currentMediaStream on android. Everything works well without any navigation, but after using the back arrow of the android device it seems that the setMediaItem function doesn't send an event anymore. I handle the android's back arrow button with an OnWillPop that pop my pages and return false. Idk why this occurs, it worked perfectly before the null-safety version. Does anyone faces this issue too ?
I'm in the process of migrating to the one-isolate
branch. If that solves the issue then trying to fix the bug in version 0.17 is probably a mute point since I understand this will be the future version 0.18.
@hpapier if you read above you'll see both a workaround and a fix. Please try one of those as preferred and see how it goes.
@ryanheise Do you think that this issue comes from the null-safety values instead of another bug ? Because without any navigation, so without any use of the back arrow button on android, everything works fine, the mediaItems are correctly sent, but when the button back arrow is pressed and handled by a WillPopScope that return a false value, then it seems that all the streams connected to the UI that comes from the AudioService are disconnected or lost. So the StreamBuilders don't receive the events anymore, but the mediaItem is correctly set in the notification information etc... any ideas ? :)
@hpapier Try either the workaround or the fix above and see if it fixes your issue first.
@ryanheise Hey! As expected I tried with the fix #664 and it didn't work (because I always set the album field to a non empty string). The problem here is really the back arrow button that breaks the streams between the audio service isolate (I guess) and the UI, even the playbackStateStream doesn't work anymore after a use of the android's back button.
ps: I use BLoC archi which uses a lot of streams and no ones break because of the android's back button. Any idea of what could it be ?
@hpapier maybe you're disconnecting from audio service in your app when pressing the back button. It sounds like maybe an app logic/navigation issue rather than this bug (which is related to null values).
@ryanheise Not really an app logic/navigation issue, it worked before this null-safety version, everything work on IOS even with the navigation. (Not related to the null values but related to the null-safety version) :)
@hpapier it still appears to be a distinct bug though, as it is not solved by this fix. Would you mind opening a new issue with your own reproduction project?
Here's the commit that introduced the string size check
It looks to me like exception it was supposed to fix was fixed by the null check, whilst the size check is unnecessary. Empty string metadata should be completely legal
Could someone who was having an issue confirm it's resolved on the one-isolate
head?
The one-isolate branch is working for me on Android. I'm one of the ones who was having trouble earlier.
I will close now that #721 is merged. If anyone still has this issue on the one-isolate
branch, leave a message below with details and I will reopen.
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs, or use StackOverflow if you need help with audio_service.
Which API doesn't behave as documented, and how does it misbehave? currentMediaItemStream, will always be null despite a media item being played. No matter what audio Item in the queue is selected it will always be null. Minimal reproduction project
child: StreamBuilder<MediaItem?>( stream: AudioService.currentMediaItemStream, builder: (context, snapshot) { final mediaItem = AudioService.currentMediaItem;
The audioplayertask code is the same as the example. I'm assuming the stream gets set at the onStart.To Reproduce (i.e. user steps, not code) Steps to reproduce the behavior: User the null safety version
Error messages
I posted the logs printing. Not sure any of the errors have to do with this issue however Expected behavior Values being returned in the currentmediaItem stream, not null on android when something is playing
Flutter SDK version