From 11964e588d07943664973cfd247817dc689d7e45 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 9 Apr 2024 01:02:30 +0600 Subject: [PATCH] feat: implement local (loopback) server to resolve stream source and leverage the media_kit playback API --- .../player/sibling_tracks_sheet.dart | 44 ++-- lib/main.dart | 2 + .../proxy_playlist/player_listeners.dart | 2 +- .../proxy_playlist/proxy_playlist.dart | 5 +- .../proxy_playlist_provider.dart | 36 +--- .../proxy_playlist/skip_segments.dart | 10 +- lib/provider/server/active_sourced_track.dart | 47 +++++ lib/provider/server/server.dart | 100 +++++++++ lib/services/audio_player/audio_player.dart | 15 +- .../audio_player/audio_player_impl.dart | 192 +----------------- 10 files changed, 188 insertions(+), 265 deletions(-) create mode 100644 lib/provider/server/active_sourced_track.dart create mode 100644 lib/provider/server/server.dart diff --git a/lib/components/player/sibling_tracks_sheet.dart b/lib/components/player/sibling_tracks_sheet.dart index 99ab223f..eef34be6 100644 --- a/lib/components/player/sibling_tracks_sheet.dart +++ b/lib/components/player/sibling_tracks_sheet.dart @@ -4,7 +4,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotify/spotify.dart' hide Offset; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/spotube_icons.dart'; @@ -16,6 +15,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/hooks/utils/use_debounce.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/services/sourced_track/models/source_info.dart'; @@ -53,21 +53,22 @@ class SiblingTracksSheet extends HookConsumerWidget { Widget build(BuildContext context, ref) { final theme = Theme.of(context); final playlist = ref.watch(ProxyPlaylistNotifier.provider); - final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); final preferences = ref.watch(userPreferencesProvider); final isSearching = useState(false); final searchMode = useState(preferences.searchMode); + final activeTrackNotifier = ref.watch(activeSourcedTrackProvider.notifier); + final activeTrack = + ref.watch(activeSourcedTrackProvider) ?? playlist.activeTrack; final title = ServiceUtils.getTitle( - playlist.activeTrack?.name ?? "", - artists: - playlist.activeTrack?.artists?.map((e) => e.name!).toList() ?? [], + activeTrack?.name ?? "", + artists: activeTrack?.artists?.map((e) => e.name!).toList() ?? [], onlyCleanArtist: true, ).trim(); final defaultSearchTerm = - "$title - ${playlist.activeTrack?.artists?.asString() ?? ""}"; + "$title - ${activeTrack?.artists?.asString() ?? ""}"; final searchController = useTextEditingController( text: defaultSearchTerm, ); @@ -91,8 +92,7 @@ class SiblingTracksSheet extends HookConsumerWidget { return siblingType.info; })); - final activeSourceInfo = - (playlist.activeTrack! as SourcedTrack).sourceInfo; + final activeSourceInfo = (activeTrack! as SourcedTrack).sourceInfo; return results ..removeWhere((element) => element.id == activeSourceInfo.id) @@ -112,8 +112,7 @@ class SiblingTracksSheet extends HookConsumerWidget { return siblingType.info; }), ); - final activeSourceInfo = - (playlist.activeTrack! as SourcedTrack).sourceInfo; + final activeSourceInfo = (activeTrack! as SourcedTrack).sourceInfo; return searchResults ..removeWhere((element) => element.id == activeSourceInfo.id) ..insert( @@ -124,18 +123,18 @@ class SiblingTracksSheet extends HookConsumerWidget { }, [ searchTerm, searchMode.value, - playlist.activeTrack, + activeTrack, preferences.audioSource, ]); final siblings = useMemoized( () => playlist.isFetching == false ? [ - (playlist.activeTrack as SourcedTrack).sourceInfo, - ...(playlist.activeTrack as SourcedTrack).siblings, + (activeTrack as SourcedTrack).sourceInfo, + ...activeTrack.siblings, ] : [], - [playlist.isFetching, playlist.activeTrack], + [playlist.isFetching, activeTrack], ); final borderRadius = floating @@ -146,12 +145,11 @@ class SiblingTracksSheet extends HookConsumerWidget { ); useEffect(() { - if (playlist.activeTrack is SourcedTrack && - (playlist.activeTrack as SourcedTrack).siblings.isEmpty) { - playlistNotifier.populateSibling(); + if (activeTrack is SourcedTrack && activeTrack.siblings.isEmpty) { + activeTrackNotifier.populateSibling(); } return null; - }, [playlist.activeTrack]); + }, [activeTrack]); final itemBuilder = useCallback( (SourceInfo sourceInfo) { @@ -178,20 +176,18 @@ class SiblingTracksSheet extends HookConsumerWidget { ), enabled: playlist.isFetching != true, selected: playlist.isFetching != true && - sourceInfo.id == - (playlist.activeTrack as SourcedTrack).sourceInfo.id, + sourceInfo.id == (activeTrack as SourcedTrack).sourceInfo.id, selectedTileColor: theme.popupMenuTheme.color, onTap: () { if (playlist.isFetching == false && - sourceInfo.id != - (playlist.activeTrack as SourcedTrack).sourceInfo.id) { - playlistNotifier.swapSibling(sourceInfo); + sourceInfo.id != (activeTrack as SourcedTrack).sourceInfo.id) { + activeTrackNotifier.swapSibling(sourceInfo); Navigator.of(context).pop(); } }, ); }, - [playlist.isFetching, playlist.activeTrack, siblings], + [playlist.isFetching, activeTrack, siblings], ); final mediaQuery = MediaQuery.of(context); diff --git a/lib/main.dart b/lib/main.dart index 8de524c7..d6df20ea 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,6 +26,7 @@ import 'package:spotube/models/source_match.dart'; import 'package:spotube/provider/connect/clients.dart'; import 'package:spotube/provider/connect/server.dart'; import 'package:spotube/provider/palette_provider.dart'; +import 'package:spotube/provider/server/server.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/cli/cli.dart'; @@ -182,6 +183,7 @@ class SpotubeState extends ConsumerState { ref.watch(paletteProvider.select((s) => s?.dominantColor?.color)); final router = ref.watch(routerProvider); + ref.listen(playbackServerProvider, (_, __) {}); ref.listen(connectServerProvider, (_, __) {}); ref.listen(connectClientsProvider, (_, __) {}); diff --git a/lib/provider/proxy_playlist/player_listeners.dart b/lib/provider/proxy_playlist/player_listeners.dart index efe2ed5f..ed648bf7 100644 --- a/lib/provider/proxy_playlist/player_listeners.dart +++ b/lib/provider/proxy_playlist/player_listeners.dart @@ -13,7 +13,7 @@ extension ProxyPlaylistListeners on ProxyPlaylistNotifier { return audioPlayer.playlistStream.listen((playlist) { state = state.copyWith( tracks: playlist.medias - .map((media) => (media as SpotubeMedia).track) + .map((media) => SpotubeMedia.fromMedia(media).track) .toSet(), active: playlist.index, ); diff --git a/lib/provider/proxy_playlist/proxy_playlist.dart b/lib/provider/proxy_playlist/proxy_playlist.dart index c02f473e..f70301ff 100644 --- a/lib/provider/proxy_playlist/proxy_playlist.dart +++ b/lib/provider/proxy_playlist/proxy_playlist.dart @@ -38,10 +38,7 @@ class ProxyPlaylist { Track? get activeTrack => active == null || active == -1 ? null : tracks.elementAtOrNull(active!); - bool get isFetching => - activeTrack != null && - activeTrack is! SourcedTrack && - activeTrack is! LocalTrack; + bool get isFetching => activeTrack == null && tracks.isNotEmpty; bool containsCollection(String collection) { return collections.contains(collection); diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index ec53578d..7d6a33c6 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -18,7 +18,6 @@ import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_services/audio_services.dart'; import 'package:spotube/provider/discord_provider.dart'; -import 'package:spotube/services/sourced_track/models/source_info.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; @@ -146,44 +145,11 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier { await audioPlayer.addTrackAt( SpotubeMedia(track), - i + 1, + (state.active ?? 0) + i + 1, ); } } - Future populateSibling() async { - // if (state.activeTrack is SourcedTrack) { - // final activeTrackWithSiblingsForSure = - // await (state.activeTrack as SourcedTrack).copyWithSibling(); - - // state = state.copyWith( - // tracks: mergeTracks([activeTrackWithSiblingsForSure], state.tracks), - // active: state.tracks.toList().indexWhere( - // (element) => element.id == activeTrackWithSiblingsForSure.id), - // ); - // } - } - - Future swapSibling(SourceInfo sibling) async { - // if (state.activeTrack is SourcedTrack) { - // await populateSibling(); - // final newTrack = - // await (state.activeTrack as SourcedTrack).swapWithSibling(sibling); - // if (newTrack == null) return; - // state = state.copyWith( - // tracks: mergeTracks([newTrack], state.tracks), - // active: state.tracks - // .toList() - // .indexWhere((element) => element.id == newTrack.id), - // ); - // await audioPlayer.pause(); - // await audioPlayer.replaceSource( - // audioPlayer.currentSource!, - // makeAppropriateSource(newTrack), - // ); - // } - } - Future next() async { await audioPlayer.skipToNext(); } diff --git a/lib/provider/proxy_playlist/skip_segments.dart b/lib/provider/proxy_playlist/skip_segments.dart index 94a63324..2d90eea6 100644 --- a/lib/provider/proxy_playlist/skip_segments.dart +++ b/lib/provider/proxy_playlist/skip_segments.dart @@ -3,12 +3,10 @@ import 'dart:convert'; import 'package:catcher_2/catcher_2.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:http/http.dart'; -import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/skip_segment.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; -import 'package:spotube/services/sourced_track/sourced_track.dart'; class SourcedSegments { final String source; @@ -75,13 +73,9 @@ Future> getAndCacheSkipSegments(String id) async { final segmentProvider = FutureProvider( (ref) async { - final track = ref.watch( - ProxyPlaylistNotifier.provider.select((s) => s.activeTrack), - ); + final track = ref.watch(activeSourcedTrackProvider); if (track == null) return null; - if (track is LocalTrack || track is! SourcedTrack) return null; - final skipNonMusic = ref.watch( userPreferencesProvider.select( (s) { diff --git a/lib/provider/server/active_sourced_track.dart b/lib/provider/server/active_sourced_track.dart new file mode 100644 index 00000000..6ecd67b4 --- /dev/null +++ b/lib/provider/server/active_sourced_track.dart @@ -0,0 +1,47 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/sourced_track/models/source_info.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; + +class ActiveSourcedTrackNotifier extends Notifier { + @override + build() { + return null; + } + + void update(SourcedTrack? sourcedTrack) { + state = sourcedTrack; + } + + Future populateSibling() async { + if (state == null) return; + state = await state!.copyWithSibling(); + } + + Future swapSibling(SourceInfo sibling) async { + if (state == null) return; + await populateSibling(); + final newTrack = await state!.swapWithSibling(sibling); + if (newTrack == null) return; + + state = newTrack; + await audioPlayer.pause(); + + final playbackNotifier = ref.read(ProxyPlaylistNotifier.notifier); + final oldActiveIndex = audioPlayer.currentIndex; + + await playbackNotifier.addTracksAtFirst([newTrack]); + await Future.delayed(const Duration(milliseconds: 50)); + await playbackNotifier.jumpToTrack(newTrack); + + await audioPlayer.removeTrack(oldActiveIndex); + + await audioPlayer.resume(); + } +} + +final activeSourcedTrackProvider = + NotifierProvider( + () => ActiveSourcedTrackNotifier(), +); diff --git a/lib/provider/server/server.dart b/lib/provider/server/server.dart new file mode 100644 index 00000000..41d1388d --- /dev/null +++ b/lib/provider/server/server.dart @@ -0,0 +1,100 @@ +import 'dart:io'; +import 'dart:math'; + +import 'package:catcher_2/catcher_2.dart'; +import 'package:dio/dio.dart' hide Response; +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logger/logger.dart'; +import 'package:shelf/shelf.dart'; +import 'package:shelf/shelf_io.dart'; +import 'package:shelf_router/shelf_router.dart'; +import 'package:spotube/models/logger.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/server/active_sourced_track.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; + +class PlaybackServer { + final Ref ref; + UserPreferences get userPreferences => ref.read(userPreferencesProvider); + ProxyPlaylist get playlist => ref.read(ProxyPlaylistNotifier.provider); + final Logger logger; + final Dio dio; + + final Router router; + + static final port = Random().nextInt(17000) + 1500; + + PlaybackServer(this.ref) + : logger = getLogger('PlaybackServer'), + dio = Dio(), + router = Router() { + router.get('/stream/', getStreamTrackId); + + const pipeline = Pipeline(); + + if (kDebugMode) { + pipeline.addMiddleware(logRequests()); + dio.interceptors.add(LogInterceptor()); + } + + serve(pipeline.addHandler(router.call), InternetAddress.loopbackIPv4, port) + .then((server) { + logger + .t('Playback server at http://${server.address.host}:${server.port}'); + + ref.onDispose(() { + dio.close(force: true); + server.close(); + }); + }); + } + + /// @get('/stream/') + Future getStreamTrackId(Request request, String trackId) async { + try { + final track = + playlist.tracks.firstWhere((element) => element.id == trackId); + final sourcedTrack = + await SourcedTrack.fetchFromTrack(track: track, ref: ref); + + ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack); + + final res = await dio.get( + sourcedTrack.url, + options: Options( + headers: { + ...request.headers, + "User-Agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", + "host": Uri.parse(sourcedTrack.url).host, + "Cache-Control": "max-age=0", + "Connection": "keep-alive", + }, + responseType: ResponseType.stream, + ), + ); + + final audioStream = res.data?.stream as Stream?; + + return Response( + 200, + body: audioStream, + context: { + "shelf.io.buffer_output": false, + }, + headers: res.headers.map, + ); + } catch (e, stack) { + Catcher2.reportCheckedError(e, stack); + return Response.internalServerError(); + } + } +} + +final playbackServerProvider = Provider((ref) { + return PlaybackServer(ref); +}); diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index 6382dddd..617efb1b 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -1,6 +1,10 @@ +import 'dart:io'; + import 'package:catcher_2/catcher_2.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/extensions/track.dart'; import 'package:spotube/models/local_track.dart'; +import 'package:spotube/provider/server/server.dart'; import 'package:spotube/services/audio_player/custom_player.dart'; // import 'package:just_audio/just_audio.dart' as ja; import 'dart:async'; @@ -9,13 +13,13 @@ import 'package:media_kit/media_kit.dart' as mk; import 'package:spotube/services/audio_player/loop_mode.dart'; import 'package:spotube/services/audio_player/playback_state.dart'; -import 'package:spotube/services/sourced_track/sourced_track.dart'; part 'audio_players_streams_mixin.dart'; part 'audio_player_impl.dart'; class SpotubeMedia extends mk.Media { final Track track; + SpotubeMedia( this.track, { Map? extras, @@ -23,12 +27,17 @@ class SpotubeMedia extends mk.Media { }) : super( track is LocalTrack ? track.path - : "http://localhost:3000/stream/${track.id}", + : "http://${InternetAddress.loopbackIPv4.address}:${PlaybackServer.port}/stream/${track.id}", extras: { ...?extras, - "trackId": track.id, + "track": track.toJson(), }, ); + + factory SpotubeMedia.fromMedia(mk.Media media) { + final track = Track.fromJson(media.extras?["track"]); + return SpotubeMedia(track); + } } abstract class AudioPlayerInterface { diff --git a/lib/services/audio_player/audio_player_impl.dart b/lib/services/audio_player/audio_player_impl.dart index cfbe4368..58868aed 100644 --- a/lib/services/audio_player/audio_player_impl.dart +++ b/lib/services/audio_player/audio_player_impl.dart @@ -4,83 +4,30 @@ final audioPlayer = SpotubeAudioPlayer(); class SpotubeAudioPlayer extends AudioPlayerInterface with SpotubeAudioPlayersStreams { - Object _resolveUrlType(String url) { - // if (mkSupportedPlatform) { - return mk.Media(url); - // } else { - // if (url.startsWith("https")) { - // return ja.AudioSource.uri(Uri.parse(url)); - // } else { - // return ja.AudioSource.file(url); - // } - // } - } - - Future preload(String url) async { - throw UnimplementedError(); - // final urlType = _resolveUrlType(url); - // if (mkSupportedPlatform && urlType is ap.Source) { - // // audioplayers doesn't have the capability to preload - // return; - // } else { - // return; - // } - } - - Future play(String url) async { - final urlType = _resolveUrlType(url); - // if (mkSupportedPlatform && urlType is mk.Media) { - await _mkPlayer.open(urlType as mk.Media, play: true); - // } else { - // if (_justAudio?.audioSource is ja.ProgressiveAudioSource && - // (_justAudio?.audioSource as ja.ProgressiveAudioSource) - // .uri - // .toString() == - // url) { - // await _justAudio?.play(); - // } else { - // await _justAudio?.stop(); - // await _justAudio?.setAudioSource( - // urlType as ja.AudioSource, - // preload: true, - // ); - // await _justAudio?.play(); - // } - // } - } - Future pause() async { await _mkPlayer.pause(); - // await _justAudio?.pause(); } Future resume() async { await _mkPlayer.play(); - // await _justAudio?.play(); } Future stop() async { await _mkPlayer.stop(); - // await _justAudio?.stop(); - // await _justAudio?.setShuffleModeEnabled(false); - // await _justAudio?.setLoopMode(ja.LoopMode.off); } Future seek(Duration position) async { await _mkPlayer.seek(position); - // await _justAudio?.seek(position); } /// Volume is between 0 and 1 Future setVolume(double volume) async { assert(volume >= 0 && volume <= 1); await _mkPlayer.setVolume(volume * 100); - // await _justAudio?.setVolume(volume); } Future setSpeed(double speed) async { await _mkPlayer.setRate(speed); - // await _justAudio?.setSpeed(speed); } Future setAudioDevice(mk.AudioDevice device) async { @@ -89,7 +36,6 @@ class SpotubeAudioPlayer extends AudioPlayerInterface Future dispose() async { await _mkPlayer.dispose(); - // await _justAudio?.dispose(); } // Playlist related @@ -101,66 +47,24 @@ class SpotubeAudioPlayer extends AudioPlayerInterface }) async { assert(tracks.isNotEmpty); assert(initialIndex <= tracks.length - 1); - // if (mkSupportedPlatform) { await _mkPlayer.open( mk.Playlist(tracks, index: initialIndex), play: autoPlay, ); - // } else { - // await _justAudio!.setAudioSource( - // ja.ConcatenatingAudioSource( - // useLazyPreparation: true, - // children: - // tracks.map((e) => ja.AudioSource.uri(Uri.parse(e))).toList(), - // ), - // preload: true, - // initialIndex: initialIndex, - // ); - // if (autoPlay) { - // await _justAudio!.play(); - // } - // } - } - - // TODO: Make sure audio player soruces are also - // TODO: changed when preferences sources are changed - List resolveTracksForSource(List tracks) { - return tracks.where((e) => sources.contains(e.url)).toList(); - } - - bool tracksExistsInPlaylist(List tracks) { - return resolveTracksForSource(tracks).length == tracks.length; } List get sources { - // if (mkSupportedPlatform) { return _mkPlayer.state.playlist.medias.map((e) => e.uri).toList(); - // } else { - // return _justAudio!.sequenceState?.effectiveSequence - // .map((e) => (e as ja.UriAudioSource).uri.toString()) - // .toList() ?? - // []; - // } } String? get currentSource { - // if (mkSupportedPlatform) { if (_mkPlayer.state.playlist.index == -1) return null; return _mkPlayer.state.playlist.medias .elementAtOrNull(_mkPlayer.state.playlist.index) ?.uri; - // } else { - // return (_justAudio?.sequenceState?.effectiveSequence - // .elementAtOrNull(_justAudio!.sequenceState!.currentIndex) - // as ja.UriAudioSource?) - // ?.uri - // .toString(); - // } } String? get nextSource { - // if (mkSupportedPlatform) { - if (loopMode == PlaybackLoopMode.all && _mkPlayer.state.playlist.index == _mkPlayer.state.playlist.medias.length - 1) { @@ -170,13 +74,6 @@ class SpotubeAudioPlayer extends AudioPlayerInterface return _mkPlayer.state.playlist.medias .elementAtOrNull(_mkPlayer.state.playlist.index + 1) ?.uri; - // } else { - // return (_justAudio?.sequenceState?.effectiveSequence - // .elementAtOrNull(_justAudio!.sequenceState!.currentIndex + 1) - // as ja.UriAudioSource?) - // ?.uri - // .toString(); - // } } String? get previousSource { @@ -185,136 +82,51 @@ class SpotubeAudioPlayer extends AudioPlayerInterface return sources.last; } - // if (mkSupportedPlatform) { return _mkPlayer.state.playlist.medias .elementAtOrNull(_mkPlayer.state.playlist.index - 1) ?.uri; - // } else { - // return (_justAudio?.sequenceState?.effectiveSequence - // .elementAtOrNull(_justAudio!.sequenceState!.currentIndex - 1) - // as ja.UriAudioSource?) - // ?.uri - // .toString(); - // } } + int get currentIndex => _mkPlayer.state.playlist.index; + Future skipToNext() async { - // if (mkSupportedPlatform) { await _mkPlayer.next(); - // } else { - // await _justAudio!.seekToNext(); - // } } Future skipToPrevious() async { - // if (mkSupportedPlatform) { await _mkPlayer.previous(); - // } else { - // await _justAudio!.seekToPrevious(); - // } } Future jumpTo(int index) async { - // if (mkSupportedPlatform) { await _mkPlayer.jump(index); - // } else { - // await _justAudio!.seek(Duration.zero, index: index); - // } } Future addTrack(mk.Media media) async { - // if (mkSupportedPlatform && urlType is mk.Media) { await _mkPlayer.add(media); - // } else { - // await (_justAudio!.audioSource as ja.ConcatenatingAudioSource) - // .add(urlType as ja.AudioSource); - // } } Future addTrackAt(mk.Media media, int index) async { - // if (mkSupportedPlatform && urlType is mk.Media) { await _mkPlayer.insert(index, media); - // } else { - // await (_justAudio!.audioSource as ja.ConcatenatingAudioSource) - // .insert(index, urlType as ja.AudioSource); - // } } Future removeTrack(int index) async { - // if (mkSupportedPlatform) { await _mkPlayer.remove(index); - // } else { - // await (_justAudio!.audioSource as ja.ConcatenatingAudioSource) - // .removeAt(index); - // } } Future moveTrack(int from, int to) async { - // if (mkSupportedPlatform) { await _mkPlayer.move(from, to); - // } else { - // await (_justAudio!.audioSource as ja.ConcatenatingAudioSource) - // .move(from, to); - // } - } - - Future replaceSource( - String oldSource, - String newSource, { - bool exclusive = false, - }) async { - final oldSourceIndex = sources.indexOf(oldSource); - if (oldSourceIndex == -1) return; - - // if (mkSupportedPlatform) { - // _mkPlayer.replace(oldSource, newSource); - // } else { - // final playlist = _justAudio!.audioSource as ja.ConcatenatingAudioSource; - - // print('oldSource: $oldSource'); - // print('newSource: $newSource'); - // final oldSourceIndexInPlaylist = - // _justAudio?.sequenceState?.effectiveSequence.indexWhere( - // (e) => (e as ja.UriAudioSource).uri.toString() == oldSource, - // ); - - // print('oldSourceIndexInPlaylist: $oldSourceIndexInPlaylist'); - - // // ignores non existing source - // if (oldSourceIndexInPlaylist == null || oldSourceIndexInPlaylist == -1) { - // return; - // } - - // await playlist.removeAt(oldSourceIndexInPlaylist); - // await playlist.insert( - // oldSourceIndexInPlaylist, - // ja.AudioSource.uri(Uri.parse(newSource)), - // ); - // } } Future clearPlaylist() async { - // if (mkSupportedPlatform) { _mkPlayer.stop(); - // } else { - // await (_justAudio!.audioSource as ja.ConcatenatingAudioSource).clear(); - // } } Future setShuffle(bool shuffle) async { - // if (mkSupportedPlatform) { await _mkPlayer.setShuffle(shuffle); - // } else { - // await _justAudio!.setShuffleModeEnabled(shuffle); - // } } Future setLoopMode(PlaybackLoopMode loop) async { - // if (mkSupportedPlatform) { await _mkPlayer.setPlaylistMode(loop.toPlaylistMode()); - // } else { - // await _justAudio!.setLoopMode(loop.toLoopMode()); - // } } Future setAudioNormalization(bool normalize) async {