mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
feat(player): custom playlist implementation for media_kit to replace unpredictable playlist of mpv
This commit is contained in:
parent
5a4e3baa51
commit
eaf65b6db2
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user