feat(player): custom playlist implementation for media_kit to replace unpredictable playlist of mpv

This commit is contained in:
Kingkor Roy Tirtho 2023-05-14 17:52:54 +06:00
parent 5a4e3baa51
commit eaf65b6db2
3 changed files with 227 additions and 55 deletions

View File

@ -74,20 +74,9 @@ class ProxyPlaylistNotifier extends StateNotifier<ProxyPlaylist>
try { try {
isPreSearching = true; isPreSearching = true;
final softReplace =
SpotubeAudioPlayer.mkSupportedPlatform && percent <= 98;
// TODO: Make repeat mode sensitive changes later // TODO: Make repeat mode sensitive changes later
final track = await ensureNthSourcePlayable( final track =
audioPlayer.currentIndex + 1, await ensureNthSourcePlayable(audioPlayer.currentIndex + 1);
/// [MediaKit] doesn't fully support replacing source, so we need
/// to check if the platform is supported or not and replace the
/// actual playlist with a playlist that contains the next track
/// at 98% >= progress
softReplace: softReplace,
exclusive: SpotubeAudioPlayer.mkSupportedPlatform,
);
if (track != null) { if (track != null) {
state = state.copyWith(tracks: mergeTracks([track], state.tracks)); state = state.copyWith(tracks: mergeTracks([track], state.tracks));
@ -190,17 +179,17 @@ class ProxyPlaylistNotifier extends StateNotifier<ProxyPlaylist>
// TODO: Safely Remove playing tracks // TODO: Safely Remove playing tracks
void removeTrack(String trackId) { Future<void> removeTrack(String trackId) async {
final track = final track =
state.tracks.firstWhereOrNull((element) => element.id == trackId); state.tracks.firstWhereOrNull((element) => element.id == trackId);
if (track == null) return; if (track == null) return;
state = state.copyWith(tracks: {...state.tracks..remove(track)}); state = state.copyWith(tracks: {...state.tracks..remove(track)});
final index = audioPlayer.sources.indexOf(makeAppropriateSource(track)); final index = audioPlayer.sources.indexOf(makeAppropriateSource(track));
if (index == -1) return; if (index == -1) return;
audioPlayer.removeTrack(index); await audioPlayer.removeTrack(index);
} }
void removeTracks(Iterable<String> tracksIds) { Future<void> removeTracks(Iterable<String> tracksIds) async {
final tracks = final tracks =
state.tracks.where((element) => tracksIds.contains(element.id)); state.tracks.where((element) => tracksIds.contains(element.id));
@ -211,7 +200,7 @@ class ProxyPlaylistNotifier extends StateNotifier<ProxyPlaylist>
for (final track in tracks) { for (final track in tracks) {
final index = audioPlayer.sources.indexOf(makeAppropriateSource(track)); final index = audioPlayer.sources.indexOf(makeAppropriateSource(track));
if (index == -1) continue; if (index == -1) continue;
audioPlayer.removeTrack(index); await audioPlayer.removeTrack(index);
} }
} }

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:catcher/catcher.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:media_kit/media_kit.dart' as mk; import 'package:media_kit/media_kit.dart' as mk;
import 'package:just_audio/just_audio.dart' as ja; import 'package:just_audio/just_audio.dart' as ja;
@ -17,7 +18,11 @@ class SpotubeAudioPlayer {
SpotubeAudioPlayer() SpotubeAudioPlayer()
: _mkPlayer = mkSupportedPlatform ? MkPlayerWithState() : null, : _mkPlayer = mkSupportedPlatform ? MkPlayerWithState() : null,
_justAudio = !mkSupportedPlatform ? ja.AudioPlayer() : null; _justAudio = !mkSupportedPlatform ? ja.AudioPlayer() : null {
_mkPlayer?.streams.error.listen((event) {
Catcher.reportCheckedError(event, StackTrace.current);
});
}
/// Whether the current platform supports the audioplayers plugin /// Whether the current platform supports the audioplayers plugin
static final bool mkSupportedPlatform = static final bool mkSupportedPlatform =
@ -140,9 +145,7 @@ class SpotubeAudioPlayer {
Stream<int> get currentIndexChangedStream { Stream<int> get currentIndexChangedStream {
if (mkSupportedPlatform) { if (mkSupportedPlatform) {
return _mkPlayer!.streams.playlist return _mkPlayer!.indexChangeStream;
.map((event) => event.index)
.asBroadcastStream();
} else { } else {
return _justAudio!.sequenceStateStream return _justAudio!.sequenceStateStream
.map((event) => event?.currentIndex ?? -1) .map((event) => event?.currentIndex ?? -1)
@ -179,7 +182,7 @@ class SpotubeAudioPlayer {
bool get hasSource { bool get hasSource {
if (mkSupportedPlatform) { if (mkSupportedPlatform) {
return _mkPlayer!.state.playlist.medias.isNotEmpty; return _mkPlayer!.playlist.medias.isNotEmpty;
} else { } else {
return _justAudio!.audioSource != null; return _justAudio!.audioSource != null;
} }
@ -378,7 +381,7 @@ class SpotubeAudioPlayer {
List<String> get sources { List<String> get sources {
if (mkSupportedPlatform) { if (mkSupportedPlatform) {
return _mkPlayer!.state.playlist.medias.map((e) => e.uri).toList(); return _mkPlayer!.playlist.medias.map((e) => e.uri).toList();
} else { } else {
return (_justAudio!.audioSource as ja.ConcatenatingAudioSource) return (_justAudio!.audioSource as ja.ConcatenatingAudioSource)
.children .children
@ -389,7 +392,7 @@ class SpotubeAudioPlayer {
int get currentIndex { int get currentIndex {
if (mkSupportedPlatform) { if (mkSupportedPlatform) {
return _mkPlayer!.state.playlist.index; return _mkPlayer!.playlist.index;
} else { } else {
return _justAudio!.sequenceState?.currentIndex ?? -1; return _justAudio!.sequenceState?.currentIndex ?? -1;
} }
@ -455,19 +458,19 @@ class SpotubeAudioPlayer {
final oldSourceIndex = sources.indexOf(oldSource); final oldSourceIndex = sources.indexOf(oldSource);
if (oldSourceIndex == -1) return; if (oldSourceIndex == -1) return;
if (mkSupportedPlatform) { // if (mkSupportedPlatform) {
final sourcesCp = sources.toList(); // final sourcesCp = sources.toList();
sourcesCp[oldSourceIndex] = newSource; // sourcesCp[oldSourceIndex] = newSource;
await _mkPlayer!.open( // await _mkPlayer!.open(
mk.Playlist( // mk.Playlist(
sourcesCp.map(mk.Media.new).toList(), // sourcesCp.map(mk.Media.new).toList(),
index: currentIndex, // index: currentIndex,
), // ),
play: false, // play: false,
); // );
if (exclusive) await jumpTo(oldSourceIndex); // if (exclusive) await jumpTo(oldSourceIndex);
} else { // } else {
await addTrack(newSource); await addTrack(newSource);
await removeTrack(oldSourceIndex); await removeTrack(oldSourceIndex);
@ -483,16 +486,12 @@ class SpotubeAudioPlayer {
await moveTrack(newSourceIndex, oldSourceIndex); await moveTrack(newSourceIndex, oldSourceIndex);
newSourceIndex = sources.indexOf(newSource); newSourceIndex = sources.indexOf(newSource);
} }
} // }
} }
Future<void> clearPlaylist() async { Future<void> clearPlaylist() async {
if (mkSupportedPlatform) { if (mkSupportedPlatform) {
await Future.wait( _mkPlayer!.stop();
_mkPlayer!.state.playlist.medias.mapIndexed(
(i, e) async => await _mkPlayer!.remove(i),
),
);
} else { } else {
await (_justAudio!.audioSource as ja.ConcatenatingAudioSource).clear(); await (_justAudio!.audioSource as ja.ConcatenatingAudioSource).clear();
} }

View File

@ -1,12 +1,16 @@
import 'dart:async'; import 'dart:async';
import 'package:collection/collection.dart';
import 'package:media_kit/media_kit.dart'; import 'package:media_kit/media_kit.dart';
// ignore: implementation_imports
import 'package:media_kit/src/models/playable.dart';
import 'package:spotube/services/audio_player/playback_state.dart'; import 'package:spotube/services/audio_player/playback_state.dart';
/// MediaKit [Player] by default doesn't have a state stream. /// MediaKit [Player] by default doesn't have a state stream.
/// This class adds a state stream to the [Player] class. /// This class adds a state stream to the [Player] class.
class MkPlayerWithState extends Player { class MkPlayerWithState extends Player {
final StreamController<AudioPlaybackState> _playerStateStream; final StreamController<AudioPlaybackState> _playerStateStream;
final StreamController<Playlist> _playlistStream;
final StreamController<bool> _shuffleStream; final StreamController<bool> _shuffleStream;
final StreamController<PlaylistMode> _loopModeStream; final StreamController<PlaylistMode> _loopModeStream;
@ -15,10 +19,14 @@ class MkPlayerWithState extends Player {
bool _shuffled; bool _shuffled;
PlaylistMode _loopMode; PlaylistMode _loopMode;
Playlist? _playlist;
List<Media>? _tempMedias;
MkPlayerWithState({super.configuration}) MkPlayerWithState({super.configuration})
: _playerStateStream = StreamController.broadcast(), : _playerStateStream = StreamController.broadcast(),
_shuffleStream = StreamController.broadcast(), _shuffleStream = StreamController.broadcast(),
_loopModeStream = StreamController.broadcast(), _loopModeStream = StreamController.broadcast(),
_playlistStream = StreamController.broadcast(),
_shuffled = false, _shuffled = false,
_loopMode = PlaylistMode.none { _loopMode = PlaylistMode.none {
_subscriptions = [ _subscriptions = [
@ -32,8 +40,15 @@ class MkPlayerWithState extends Player {
_playerStateStream.add(AudioPlaybackState.paused); _playerStateStream.add(AudioPlaybackState.paused);
} }
}), }),
streams.completed.listen((event) { streams.completed.listen((event) async {
_playerStateStream.add(AudioPlaybackState.completed); _playerStateStream.add(AudioPlaybackState.completed);
if (!event || _playlist == null) return;
if (loopMode == PlaylistMode.single) {
await super.open(_playlist!.medias[_playlist!.index], play: true);
} else {
await next();
}
}), }),
streams.playlist.listen((event) { streams.playlist.listen((event) {
if (event.medias.isEmpty) { if (event.medias.isEmpty) {
@ -45,16 +60,51 @@ class MkPlayerWithState extends Player {
bool get shuffled => _shuffled; bool get shuffled => _shuffled;
PlaylistMode get loopMode => _loopMode; PlaylistMode get loopMode => _loopMode;
Playlist get playlist => _playlist ?? const Playlist([], index: -1);
Stream<AudioPlaybackState> get playerStateStream => _playerStateStream.stream; Stream<AudioPlaybackState> get playerStateStream => _playerStateStream.stream;
Stream<bool> get shuffleStream => _shuffleStream.stream; Stream<bool> get shuffleStream => _shuffleStream.stream;
Stream<PlaylistMode> get loopModeStream => _loopModeStream.stream; Stream<PlaylistMode> get loopModeStream => _loopModeStream.stream;
Stream<Playlist> get playlistStream => _playlistStream.stream;
Stream<int> get indexChangeStream {
int index = playlist.index;
return playlistStream.map((event) => event.index).where((event) {
if (event != index) {
index = event;
return true;
}
return false;
});
}
set playlist(Playlist playlist) {
_playlist = playlist;
_playlistStream.add(playlist);
}
@override @override
Future<void> setShuffle(bool shuffle) async { Future<void> setShuffle(bool shuffle) async {
_shuffled = shuffle; _shuffled = shuffle;
await super.setShuffle(shuffle); await super.setShuffle(shuffle);
_shuffleStream.add(shuffle); _shuffleStream.add(shuffle);
if (shuffle) {
_tempMedias = _playlist!.medias;
final active = _playlist!.medias[_playlist!.index];
final newMedias = _playlist!.medias.toList()..shuffle();
playlist = _playlist!.copyWith(
medias: newMedias,
index: newMedias.indexOf(active),
);
} else {
if (_tempMedias == null) return;
playlist = _playlist!.copyWith(
medias: _tempMedias!,
index: _tempMedias?.indexOf(
_playlist!.medias[_playlist!.index],
),
);
_tempMedias = null;
}
} }
@override @override
@ -65,9 +115,14 @@ class MkPlayerWithState extends Player {
} }
Future<void> stop() async { Future<void> stop() async {
pause(); await pause();
_loopMode = PlaylistMode.none; _loopMode = PlaylistMode.none;
_shuffled = false; _shuffled = false;
_playlist = null;
_tempMedias = null;
_playerStateStream.add(AudioPlaybackState.stopped);
for (int i = 0; i < state.playlist.medias.length; i++) { for (int i = 0; i < state.playlist.medias.length; i++) {
await remove(i); await remove(i);
} }
@ -80,4 +135,133 @@ class MkPlayerWithState extends Player {
} }
return super.dispose(code: code); return super.dispose(code: code);
} }
@override
Future<void> open(
Playable playable, {
bool play = true,
}) async {
if (playable is Playlist) {
playlist = playable;
super.open(playable.medias[playable.index], play: play);
}
await super.open(playable, play: play);
}
@override
FutureOr<void> next() {
if (_playlist == null || _playlist!.index + 1 >= _playlist!.medias.length) {
return null;
}
if (loopMode == PlaylistMode.loop &&
_playlist!.index == _playlist!.medias.length - 1) {
playlist = _playlist!.copyWith(index: 0);
} else {
playlist = _playlist!.copyWith(index: _playlist!.index + 1);
}
return super.open(_playlist!.medias[_playlist!.index], play: true);
}
@override
FutureOr<void> previous() {
if (_playlist == null || _playlist!.index - 1 < 0) return null;
if (loopMode == PlaylistMode.loop && _playlist!.index == 0) {
playlist = _playlist!.copyWith(index: _playlist!.medias.length - 1);
} else {
playlist = _playlist!.copyWith(index: _playlist!.index - 1);
}
return super.open(_playlist!.medias[_playlist!.index], play: true);
}
@override
FutureOr<void> jump(int index) {
if (_playlist == null || index < 0 || index >= _playlist!.medias.length) {
return null;
}
playlist = _playlist!.copyWith(index: index);
return super.open(_playlist!.medias[_playlist!.index], play: true);
}
@override
FutureOr<void> move(int from, int to) {
if (_playlist == null ||
from >= _playlist!.medias.length ||
to >= _playlist!.medias.length) return null;
final active = _playlist!.medias[_playlist!.index];
final newPlaylist = _playlist!.copyWith(
medias: _playlist!.medias.mapIndexed((index, element) {
if (index == from) {
return _playlist!.medias[to];
} else if (index == to) {
return _playlist!.medias[from];
}
return element;
}).toList(),
);
playlist = _playlist!.copyWith(
index: newPlaylist.medias.indexOf(active),
medias: newPlaylist.medias,
);
}
@override
FutureOr<void> add(Media media) {
if (_playlist == null) return null;
playlist = _playlist!.copyWith(
medias: [..._playlist!.medias, media],
);
if (shuffled && _tempMedias != null) {
_tempMedias!.add(media);
}
}
@override
FutureOr<void> remove(int index) async {
if (_playlist == null || index >= _playlist!.medias.length) return null;
final item = _playlist!.medias.elementAtOrNull(index);
if (shuffled && _tempMedias != null && item != null) {
_tempMedias!.remove(item);
}
if (_playlist!.index == index) {
final hasNext = _playlist!.index + 1 < _playlist!.medias.length;
final hasPrevious = _playlist!.index - 1 >= 0;
if (hasNext) {
playlist = _playlist!.copyWith(
index: _playlist!.index + 1,
medias: _playlist!.medias..removeAt(index),
);
super.open(_playlist!.medias[_playlist!.index], play: true);
} else if (hasPrevious) {
playlist = _playlist!.copyWith(
index: _playlist!.index - 1,
medias: _playlist!.medias..removeAt(index),
);
super.open(_playlist!.medias[_playlist!.index], play: true);
} else {
playlist = _playlist!.copyWith(
medias: _playlist!.medias..removeAt(index),
index: -1,
);
await stop();
}
} else {
final active = _playlist!.medias[_playlist!.index];
final newMedias = _playlist!.medias..removeAt(index);
playlist = _playlist!.copyWith(
medias: newMedias,
index: newMedias.indexOf(active),
);
}
}
} }