No automatic track change on track complete fix (https://github.com/KRTirtho/spotube/issues/46)

This commit is contained in:
Kingkor Roy Tirtho 2022-05-03 14:20:10 +06:00
parent c0e2a21765
commit 83a45dd9b6
3 changed files with 35 additions and 73 deletions

View File

@ -36,6 +36,9 @@ class Player extends HookConsumerWidget {
useFuture(future, initialData: null); useFuture(future, initialData: null);
useEffect(() { useEffect(() {
// registering all the stream subscription listeners of player
playback.register();
/// warm up the audio player before playing actual audio /// warm up the audio player before playing actual audio
/// It's for resolving unresolved issue related to just_audio's /// It's for resolving unresolved issue related to just_audio's
/// [disposeAllPlayers] method which is throwing /// [disposeAllPlayers] method which is throwing
@ -51,7 +54,9 @@ class Player extends HookConsumerWidget {
} else { } else {
player.setAsset("assets/warmer.mp3"); player.setAsset("assets/warmer.mp3");
} }
return null; return () {
playback.dispose();
};
}, []); }, []);
useEffect(() { useEffect(() {

View File

@ -22,17 +22,6 @@ class PlayerControls extends HookConsumerWidget {
final AudioPlayer player = playback.player; final AudioPlayer player = playback.player;
final _shuffled = useState(false); final _shuffled = useState(false);
final _duration = useState<Duration?>(playback.duration);
useEffect(() {
listener(Duration? duration) {
_duration.value = duration;
}
playback.addDurationChangeListener(listener);
return () => playback.removeDurationChangeListener(listener);
}, []);
final onNext = useNextTrack(playback); final onNext = useNextTrack(playback);
@ -40,7 +29,7 @@ class PlayerControls extends HookConsumerWidget {
final _playOrPause = useTogglePlayPause(playback); final _playOrPause = useTogglePlayPause(playback);
final duration = _duration.value ?? Duration.zero; final duration = playback.duration ?? Duration.zero;
return Container( return Container(
constraints: const BoxConstraints(maxWidth: 600), constraints: const BoxConstraints(maxWidth: 600),

View File

@ -57,21 +57,16 @@ class Playback extends ChangeNotifier {
// states // states
bool _isPlaying = false; bool _isPlaying = false;
Duration? _duration; Duration? duration;
// using custom listeners for duration as it changes super quickly
// which will cause re-renders in components that don't even need it
// thus only allowing to listen to change in duration through only
// a listener function
List<Function(Duration?)> _durationListeners = [];
// listeners // listeners
StreamSubscription<bool>? _playingStreamListener; StreamSubscription<bool>? _playingStreamListener;
StreamSubscription<Duration?>? _durationStreamListener; StreamSubscription<Duration?>? _durationStreamListener;
StreamSubscription<ProcessingState>? _processingStateStreamListener;
StreamSubscription<AudioInterruptionEvent>? _audioInterruptionEventListener; StreamSubscription<AudioInterruptionEvent>? _audioInterruptionEventListener;
StreamSubscription<Duration>? _positionStreamListener; StreamSubscription<Duration>? _positionStreamListener;
Duration _prevPosition = Duration.zero;
AudioPlayer player; AudioPlayer player;
YoutubeExplode youtube; YoutubeExplode youtube;
AudioSession? _audioSession; AudioSession? _audioSession;
@ -82,7 +77,9 @@ class Playback extends ChangeNotifier {
CurrentPlaylist? currentPlaylist, CurrentPlaylist? currentPlaylist,
Track? currentTrack, Track? currentTrack,
}) : _currentPlaylist = currentPlaylist, }) : _currentPlaylist = currentPlaylist,
_currentTrack = currentTrack { _currentTrack = currentTrack;
void register() {
_playingStreamListener = player.playingStream.listen( _playingStreamListener = player.playingStream.listen(
(playing) { (playing) {
_isPlaying = playing; _isPlaying = playing;
@ -90,50 +87,48 @@ class Playback extends ChangeNotifier {
}, },
); );
_durationStreamListener = player.durationStream.listen((duration) async { _durationStreamListener = player.durationStream.listen((event) async {
if (duration != null) { if (event != null) {
// Actually things doesn't work all the time as they were // Actually things doesn't work all the time as they were
// described. So instead of listening to a `_ready` // described. So instead of listening to a `_ready`
// stream, it has to listen to duration stream since duration // stream, it has to listen to duration stream since duration
// is always added to the Stream sink after all icyMetadata has // is always added to the Stream sink after all icyMetadata has
// been loaded thus indicating buffering started // been loaded thus indicating buffering started
if (duration != Duration.zero && duration != _duration) { if (event != Duration.zero && event != duration) {
// this line is for prev/next or already playing playlist // this line is for prev/next or already playing playlist
if (player.playing) await player.pause(); if (player.playing) await player.pause();
await player.play(); await player.play();
} }
duration = event;
_duration = duration; notifyListeners();
// for avoiding unnecessary re-renders in other components that
// doesn't need duration
_callAllDurationListeners(duration);
} }
}); });
_processingStateStreamListener = _positionStreamListener =
player.processingStateStream.listen((event) async { player.createPositionStream().listen((position) async {
_logger.v("[Processing State Change] $event"); // detecting multiple same call
try { if (_prevPosition.inSeconds == position.inSeconds) return;
if (event != ProcessingState.completed) return; _prevPosition = position;
/// Because of ProcessingState.complete never gets set bug using a
/// custom solution to know when the audio stops playing
///
/// Details: https://github.com/KRTirtho/spotube/issues/46
if (duration != Duration.zero &&
duration?.isNegative == false &&
position.inSeconds == duration?.inSeconds) {
if (_currentTrack?.id != null) { if (_currentTrack?.id != null) {
await player.pause();
movePlaylistPositionBy(1); movePlaylistPositionBy(1);
} else { } else {
await audioSession?.setActive(false); await audioSession?.setActive(false);
_isPlaying = false; _isPlaying = false;
_duration = null; duration = null;
_callAllDurationListeners(null);
notifyListeners(); notifyListeners();
} }
} catch (e, stack) {
_logger.e("PrecessingStateStreamListener", e, stack);
} }
}); });
_positionStreamListener = (player.positionStream.isBroadcast
? player.positionStream
: player.positionStream.asBroadcastStream())
.listen((position) async {});
AudioSession.instance.then((session) async { AudioSession.instance.then((session) async {
_audioSession = session; _audioSession = session;
await session.configure(const AudioSessionConfiguration.music()); await session.configure(const AudioSessionConfiguration.music());
@ -148,28 +143,6 @@ class Playback extends ChangeNotifier {
bool get isPlaying => _isPlaying; bool get isPlaying => _isPlaying;
AudioSession? get audioSession => _audioSession; AudioSession? get audioSession => _audioSession;
/// this duration field is almost static & changes occasionally
///
/// If you want realtime duration with state-update/re-render
/// use custom state & the [addDurationChangeListener] function to do so
Duration? get duration => _duration;
_callAllDurationListeners(Duration? arg) {
for (var listener in _durationListeners) {
listener(arg);
}
}
void addDurationChangeListener(void Function(Duration? duration) listener) {
_durationListeners.add(listener);
}
void removeDurationChangeListener(
void Function(Duration? duration) listener) {
_durationListeners =
_durationListeners.where((p) => p != listener).toList();
}
set setCurrentTrack(Track track) { set setCurrentTrack(Track track) {
_logger.v("[Setting Current Track] ${track.name} - ${track.id}"); _logger.v("[Setting Current Track] ${track.name} - ${track.id}");
_currentTrack = track; _currentTrack = track;
@ -185,8 +158,7 @@ class Playback extends ChangeNotifier {
void reset() { void reset() {
_logger.v("Playback Reset"); _logger.v("Playback Reset");
_isPlaying = false; _isPlaying = false;
_duration = null; duration = null;
_callAllDurationListeners(null);
_currentPlaylist = null; _currentPlaylist = null;
_currentTrack = null; _currentTrack = null;
_audioSession?.setActive(false); _audioSession?.setActive(false);
@ -211,7 +183,6 @@ class Playback extends ChangeNotifier {
@override @override
dispose() { dispose() {
_processingStateStreamListener?.cancel();
_durationStreamListener?.cancel(); _durationStreamListener?.cancel();
_playingStreamListener?.cancel(); _playingStreamListener?.cancel();
_audioInterruptionEventListener?.cancel(); _audioInterruptionEventListener?.cancel();
@ -234,8 +205,7 @@ class Playback extends ChangeNotifier {
? _currentPlaylist!.tracks.elementAt(safeIndex) ? _currentPlaylist!.tracks.elementAt(safeIndex)
: null; : null;
if (track != null) { if (track != null) {
_duration = null; duration = null;
_callAllDurationListeners(null);
_currentTrack = track; _currentTrack = track;
notifyListeners(); notifyListeners();
// starts to play the newly entered next/prev track // starts to play the newly entered next/prev track
@ -268,8 +238,6 @@ class Playback extends ChangeNotifier {
) )
.then((value) async { .then((value) async {
_currentTrack = track; _currentTrack = track;
_duration = value;
_callAllDurationListeners(value);
notifyListeners(); notifyListeners();
}); });
} }