diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index 363bf59e2..32cdeff93 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -44,9 +44,6 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { /// new queue. int? nextInitialIndex; - /// The item that was previously played. Used for reporting playback status. - MediaItem? _previousItem; - /// Set to true when we're stopping the audio service. Used to avoid playback /// progress reporting. bool _isStopping = false; @@ -67,7 +64,21 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { // Propagate all events from the audio player to AudioService clients. _player.playbackEventStream.listen((event) async { - playbackState.add(_transformEvent(event)); + final previousState = playbackState.valueOrNull; + final updatedState = _transformEvent(event); + + playbackState.add(updatedState); + + // Handle track changes + final previousIndex = previousState?.queueIndex; + final currentIndex = updatedState.queueIndex; + if (previousIndex != currentIndex) { + final previousItem = + previousIndex != null ? _getQueueItem(previousIndex) : null; + final currentItem = _getQueueItem(currentIndex!); + + onTrackChanged(currentItem, updatedState, previousItem, previousState); + } if (playbackState.valueOrNull != null && playbackState.valueOrNull?.processingState != @@ -87,41 +98,6 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } }); - _player.currentIndexStream.listen((event) async { - if (event == null) return; - - final currentItem = _getQueueItem(event); - mediaItem.add(currentItem); - - if (!FinampSettingsHelper.finampSettings.isOffline) { - final jellyfinApiHelper = GetIt.instance(); - - if (_previousItem != null) { - final playbackData = generatePlaybackProgressInfo( - item: _previousItem, - includeNowPlayingQueue: true, - isStopEvent: true, - ); - - if (playbackData != null) { - await jellyfinApiHelper.stopPlaybackProgress(playbackData); - } - } - - final playbackData = generatePlaybackProgressInfo( - item: currentItem, - includeNowPlayingQueue: true, - ); - - if (playbackData != null) { - await jellyfinApiHelper.reportPlaybackStart(playbackData); - } - - // Set item for next index update - _previousItem = currentItem; - } - }); - // PlaybackEvent doesn't include shuffle/loops so we listen for changes here _player.shuffleModeEnabledStream.listen( (_) => playbackState.add(_transformEvent(_player.playbackEvent))); @@ -155,13 +131,9 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { _isStopping = true; - // Clear the previous item. - _previousItem = null; - // Tell Jellyfin we're no longer playing audio if we're online if (!FinampSettingsHelper.finampSettings.isOffline) { - final playbackInfo = - generatePlaybackProgressInfo(includeNowPlayingQueue: false); + final playbackInfo = generateCurrentPlaybackProgressInfo(); if (playbackInfo != null) { await _jellyfinApiHelper.stopPlaybackProgress(playbackInfo); } @@ -349,7 +321,8 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { break; default: return Future.error( - "Unsupported AudioServiceRepeatMode! Recieved ${repeatMode.toString()}, requires all, none, or one."); + "Unsupported AudioServiceRepeatMode! Received ${repeatMode.toString()}, requires all, none, or one.", + ); } } catch (e) { _audioServiceBackgroundTaskLogger.severe(e); @@ -368,34 +341,55 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } } - /// Generates PlaybackProgressInfo from current player info. Returns null if - /// _queue is empty. If an item is not supplied, the current queue index will - /// be used. - PlaybackProgressInfo? generatePlaybackProgressInfo({ - MediaItem? item, - required bool includeNowPlayingQueue, - bool isStopEvent = false, - }) { - if (item == null) { - final currentIndex = _player.currentIndex; - if (_queueAudioSource.length == 0 || currentIndex == 0) { - // This function relies on _queue having items, - // so we return null if it's empty or no index is played - // and no custom item was passed to avoid more errors. - return null; + onTrackChanged( + MediaItem currentItem, + PlaybackState currentState, + MediaItem? previousItem, + PlaybackState? previousState, + ) async { + mediaItem.add(currentItem); + + if (!FinampSettingsHelper.finampSettings.isOffline) { + final jellyfinApiHelper = GetIt.instance(); + + if (previousItem != null && previousState != null) { + final playbackData = generatePlaybackProgressInfoFromState( + previousItem, + previousState, + ); + + if (playbackData != null) { + await jellyfinApiHelper.stopPlaybackProgress(playbackData); + } + } + + final playbackData = generatePlaybackProgressInfoFromState( + currentItem, + currentState, + ); + + if (playbackData != null) { + await jellyfinApiHelper.reportPlaybackStart(playbackData); } - item = _getQueueItem(currentIndex!); } + } + /// Generates PlaybackProgressInfo for the supplied item and player info. + PlaybackProgressInfo? generatePlaybackProgressInfo( + MediaItem item, { + required bool isPaused, + required bool isMuted, + required Duration playerPosition, + required String repeatMode, + required bool includeNowPlayingQueue, + }) { try { return PlaybackProgressInfo( itemId: item.extras!["itemJson"]["Id"], - isPaused: !_player.playing, - isMuted: _player.volume == 0, - positionTicks: isStopEvent - ? (item.duration?.inMicroseconds ?? 0) * 10 - : _player.position.inMicroseconds * 10, - repeatMode: _jellyfinRepeatMode(_player.loopMode), + isPaused: isPaused, + isMuted: isMuted, + positionTicks: playerPosition.inMicroseconds * 10, + repeatMode: repeatMode, playMethod: item.extras!["shouldTranscode"] ?? false ? "Transcode" : "DirectPlay", @@ -418,6 +412,45 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } } + /// Generates PlaybackProgressInfo from current player info. + /// Returns null if _queue is empty. + /// If an item is not supplied, the current queue index will be used. + PlaybackProgressInfo? generateCurrentPlaybackProgressInfo() { + final currentIndex = _player.currentIndex; + if (_queueAudioSource.length == 0 || currentIndex == null) { + // This function relies on _queue having items, + // so we return null if it's empty or no index is played + // and no custom item was passed to avoid more errors. + return null; + } + final item = _getQueueItem(currentIndex); + + return generatePlaybackProgressInfo( + item, + isPaused: !_player.playing, + isMuted: _player.volume == 0, + playerPosition: _player.position, + repeatMode: _jellyfinRepeatModeFromLoopMode(_player.loopMode), + includeNowPlayingQueue: false, + ); + } + + /// Generates PlaybackProgressInfo for the supplied item and playback state. + PlaybackProgressInfo? generatePlaybackProgressInfoFromState( + MediaItem item, + PlaybackState state, + ) { + return generatePlaybackProgressInfo( + item, + isPaused: !state.playing, + // TODO: get volume from state? + isMuted: false, + playerPosition: state.position, + repeatMode: _jellyfinRepeatModeFromRepeatMode(state.repeatMode), + includeNowPlayingQueue: true, + ); + } + void setNextInitialIndex(int index) { nextInitialIndex = index; } @@ -496,8 +529,7 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { try { JellyfinApiHelper jellyfinApiHelper = GetIt.instance(); - final playbackInfo = - generatePlaybackProgressInfo(includeNowPlayingQueue: false); + final playbackInfo = generateCurrentPlaybackProgressInfo(); if (playbackInfo != null) { await jellyfinApiHelper.updatePlaybackProgress(playbackInfo); } @@ -536,7 +568,7 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } } } else { - // We have to deserialise this because Dart is stupid and can't handle + // We have to deserialize this because Dart is stupid and can't handle // sending classes through isolates. final downloadedSong = DownloadedSong.fromJson(mediaItem.extras!["downloadedSongJson"]); @@ -610,24 +642,36 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } } -String _jellyfinRepeatMode(LoopMode loopMode) { +AudioServiceRepeatMode _audioServiceRepeatMode(LoopMode loopMode) { switch (loopMode) { - case LoopMode.all: - return "RepeatAll"; - case LoopMode.one: - return "RepeatOne"; case LoopMode.off: - return "RepeatNone"; + return AudioServiceRepeatMode.none; + case LoopMode.one: + return AudioServiceRepeatMode.one; + case LoopMode.all: + return AudioServiceRepeatMode.all; } } -AudioServiceRepeatMode _audioServiceRepeatMode(LoopMode loopMode) { +String _jellyfinRepeatModeFromLoopMode(LoopMode loopMode) { switch (loopMode) { case LoopMode.off: - return AudioServiceRepeatMode.none; + return "RepeatNone"; case LoopMode.one: - return AudioServiceRepeatMode.one; + return "RepeatOne"; case LoopMode.all: - return AudioServiceRepeatMode.all; + return "RepeatAll"; + } +} + +String _jellyfinRepeatModeFromRepeatMode(AudioServiceRepeatMode repeatMode) { + switch (repeatMode) { + case AudioServiceRepeatMode.none: + return "RepeatNone"; + case AudioServiceRepeatMode.one: + return "RepeatOne"; + case AudioServiceRepeatMode.all: + case AudioServiceRepeatMode.group: + return "RepeatAll"; } } \ No newline at end of file