mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-05-08 16:24:36 +00:00
feat: implement local (loopback) server to resolve stream source and leverage the media_kit playback API
This commit is contained in:
parent
82f2e12b79
commit
11964e588d
@ -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,
|
||||
]
|
||||
: <SourceInfo>[],
|
||||
[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);
|
||||
|
||||
@ -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<Spotube> {
|
||||
ref.watch(paletteProvider.select((s) => s?.dominantColor?.color));
|
||||
final router = ref.watch(routerProvider);
|
||||
|
||||
ref.listen(playbackServerProvider, (_, __) {});
|
||||
ref.listen(connectServerProvider, (_, __) {});
|
||||
ref.listen(connectClientsProvider, (_, __) {});
|
||||
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<ProxyPlaylist> {
|
||||
|
||||
await audioPlayer.addTrackAt(
|
||||
SpotubeMedia(track),
|
||||
i + 1,
|
||||
(state.active ?? 0) + i + 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> 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<void> 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<void> next() async {
|
||||
await audioPlayer.skipToNext();
|
||||
}
|
||||
|
||||
@ -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<List<SkipSegment>> getAndCacheSkipSegments(String id) async {
|
||||
|
||||
final segmentProvider = FutureProvider<SourcedSegments?>(
|
||||
(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) {
|
||||
|
||||
47
lib/provider/server/active_sourced_track.dart
Normal file
47
lib/provider/server/active_sourced_track.dart
Normal file
@ -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<SourcedTrack?> {
|
||||
@override
|
||||
build() {
|
||||
return null;
|
||||
}
|
||||
|
||||
void update(SourcedTrack? sourcedTrack) {
|
||||
state = sourcedTrack;
|
||||
}
|
||||
|
||||
Future<void> populateSibling() async {
|
||||
if (state == null) return;
|
||||
state = await state!.copyWithSibling();
|
||||
}
|
||||
|
||||
Future<void> 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, SourcedTrack?>(
|
||||
() => ActiveSourcedTrackNotifier(),
|
||||
);
|
||||
100
lib/provider/server/server.dart
Normal file
100
lib/provider/server/server.dart
Normal file
@ -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/<trackId>', 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/<trackId>')
|
||||
Future<Response> 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<Uint8List>?;
|
||||
|
||||
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<PlaybackServer>((ref) {
|
||||
return PlaybackServer(ref);
|
||||
});
|
||||
@ -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<String, String>? 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 {
|
||||
|
||||
@ -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<void> 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<void> 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<void> pause() async {
|
||||
await _mkPlayer.pause();
|
||||
// await _justAudio?.pause();
|
||||
}
|
||||
|
||||
Future<void> resume() async {
|
||||
await _mkPlayer.play();
|
||||
// await _justAudio?.play();
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
await _mkPlayer.stop();
|
||||
// await _justAudio?.stop();
|
||||
// await _justAudio?.setShuffleModeEnabled(false);
|
||||
// await _justAudio?.setLoopMode(ja.LoopMode.off);
|
||||
}
|
||||
|
||||
Future<void> seek(Duration position) async {
|
||||
await _mkPlayer.seek(position);
|
||||
// await _justAudio?.seek(position);
|
||||
}
|
||||
|
||||
/// Volume is between 0 and 1
|
||||
Future<void> setVolume(double volume) async {
|
||||
assert(volume >= 0 && volume <= 1);
|
||||
await _mkPlayer.setVolume(volume * 100);
|
||||
// await _justAudio?.setVolume(volume);
|
||||
}
|
||||
|
||||
Future<void> setSpeed(double speed) async {
|
||||
await _mkPlayer.setRate(speed);
|
||||
// await _justAudio?.setSpeed(speed);
|
||||
}
|
||||
|
||||
Future<void> setAudioDevice(mk.AudioDevice device) async {
|
||||
@ -89,7 +36,6 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
|
||||
|
||||
Future<void> 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<SourcedTrack> resolveTracksForSource(List<SourcedTrack> tracks) {
|
||||
return tracks.where((e) => sources.contains(e.url)).toList();
|
||||
}
|
||||
|
||||
bool tracksExistsInPlaylist(List<SourcedTrack> tracks) {
|
||||
return resolveTracksForSource(tracks).length == tracks.length;
|
||||
}
|
||||
|
||||
List<String> 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>[];
|
||||
// }
|
||||
}
|
||||
|
||||
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<void> skipToNext() async {
|
||||
// if (mkSupportedPlatform) {
|
||||
await _mkPlayer.next();
|
||||
// } else {
|
||||
// await _justAudio!.seekToNext();
|
||||
// }
|
||||
}
|
||||
|
||||
Future<void> skipToPrevious() async {
|
||||
// if (mkSupportedPlatform) {
|
||||
await _mkPlayer.previous();
|
||||
// } else {
|
||||
// await _justAudio!.seekToPrevious();
|
||||
// }
|
||||
}
|
||||
|
||||
Future<void> jumpTo(int index) async {
|
||||
// if (mkSupportedPlatform) {
|
||||
await _mkPlayer.jump(index);
|
||||
// } else {
|
||||
// await _justAudio!.seek(Duration.zero, index: index);
|
||||
// }
|
||||
}
|
||||
|
||||
Future<void> 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<void> 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<void> removeTrack(int index) async {
|
||||
// if (mkSupportedPlatform) {
|
||||
await _mkPlayer.remove(index);
|
||||
// } else {
|
||||
// await (_justAudio!.audioSource as ja.ConcatenatingAudioSource)
|
||||
// .removeAt(index);
|
||||
// }
|
||||
}
|
||||
|
||||
Future<void> moveTrack(int from, int to) async {
|
||||
// if (mkSupportedPlatform) {
|
||||
await _mkPlayer.move(from, to);
|
||||
// } else {
|
||||
// await (_justAudio!.audioSource as ja.ConcatenatingAudioSource)
|
||||
// .move(from, to);
|
||||
// }
|
||||
}
|
||||
|
||||
Future<void> 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<void> clearPlaylist() async {
|
||||
// if (mkSupportedPlatform) {
|
||||
_mkPlayer.stop();
|
||||
// } else {
|
||||
// await (_justAudio!.audioSource as ja.ConcatenatingAudioSource).clear();
|
||||
// }
|
||||
}
|
||||
|
||||
Future<void> setShuffle(bool shuffle) async {
|
||||
// if (mkSupportedPlatform) {
|
||||
await _mkPlayer.setShuffle(shuffle);
|
||||
// } else {
|
||||
// await _justAudio!.setShuffleModeEnabled(shuffle);
|
||||
// }
|
||||
}
|
||||
|
||||
Future<void> setLoopMode(PlaybackLoopMode loop) async {
|
||||
// if (mkSupportedPlatform) {
|
||||
await _mkPlayer.setPlaylistMode(loop.toPlaylistMode());
|
||||
// } else {
|
||||
// await _justAudio!.setLoopMode(loop.toLoopMode());
|
||||
// }
|
||||
}
|
||||
|
||||
Future<void> setAudioNormalization(bool normalize) async {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user