mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-05-08 16:24:36 +00:00
refactor: remove SourcedTrack based audio player and utilize mediakit playback system
This commit is contained in:
parent
0d080b77b7
commit
82f2e12b79
@ -5,6 +5,7 @@ import 'package:path/path.dart';
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/extensions/album_simple.dart';
|
import 'package:spotube/extensions/album_simple.dart';
|
||||||
import 'package:spotube/extensions/artist_simple.dart';
|
import 'package:spotube/extensions/artist_simple.dart';
|
||||||
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
|
||||||
extension TrackExtensions on Track {
|
extension TrackExtensions on Track {
|
||||||
Track fromFile(
|
Track fromFile(
|
||||||
@ -90,3 +91,9 @@ extension TrackSimpleExtensions on TrackSimple {
|
|||||||
return track;
|
return track;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TracksToMediaExtension on Iterable<Track> {
|
||||||
|
List<SpotubeMedia> asMediaList() {
|
||||||
|
return map((track) => SpotubeMedia(track)).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,108 +0,0 @@
|
|||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/models/local_track.dart';
|
|
||||||
import 'package:spotube/models/logger.dart';
|
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
|
||||||
|
|
||||||
final logger = getLogger("NextFetcherMixin");
|
|
||||||
|
|
||||||
mixin NextFetcher on StateNotifier<ProxyPlaylist> {
|
|
||||||
Future<List<SourcedTrack>> fetchTracks(
|
|
||||||
Ref ref, {
|
|
||||||
int count = 3,
|
|
||||||
int offset = 0,
|
|
||||||
}) async {
|
|
||||||
/// get [count] [state.tracks] that are not [SourcedTrack] and [LocalTrack]
|
|
||||||
|
|
||||||
final bareTracks = state.tracks
|
|
||||||
.skip(offset)
|
|
||||||
.where((element) => element is! SourcedTrack && element is! LocalTrack)
|
|
||||||
.take(count);
|
|
||||||
|
|
||||||
/// fetch [bareTracks] one by one with 100ms delay
|
|
||||||
final fetchedTracks = await Future.wait(
|
|
||||||
bareTracks.mapIndexed((i, track) async {
|
|
||||||
final future = SourcedTrack.fetchFromTrack(
|
|
||||||
ref: ref,
|
|
||||||
track: track,
|
|
||||||
);
|
|
||||||
if (i == 0) {
|
|
||||||
return await future;
|
|
||||||
}
|
|
||||||
return await Future.delayed(
|
|
||||||
const Duration(milliseconds: 100),
|
|
||||||
() => future,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return fetchedTracks;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Merges List of [SourcedTrack]s with [Track]s and outputs a mixed List
|
|
||||||
Set<Track> mergeTracks(
|
|
||||||
Iterable<SourcedTrack> fetchTracks,
|
|
||||||
Iterable<Track> tracks,
|
|
||||||
) {
|
|
||||||
return tracks.map((track) {
|
|
||||||
final fetchedTrack = fetchTracks.firstWhereOrNull(
|
|
||||||
(fetchTrack) => fetchTrack.id == track.id,
|
|
||||||
);
|
|
||||||
if (fetchedTrack != null) {
|
|
||||||
return fetchedTrack;
|
|
||||||
}
|
|
||||||
return track;
|
|
||||||
}).toSet();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if [Track] is playable
|
|
||||||
bool isUnPlayable(String source) {
|
|
||||||
return source.startsWith('https://youtube.com/unplayable.m4a?id=');
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isPlayable(String source) => !isUnPlayable(source);
|
|
||||||
|
|
||||||
/// Returns [Track.id] from [isUnPlayable] source that is not playable
|
|
||||||
String getIdFromUnPlayable(String source) {
|
|
||||||
return source
|
|
||||||
.split('&')
|
|
||||||
.first
|
|
||||||
.replaceFirst('https://youtube.com/unplayable.m4a?id=', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns appropriate Media source for [Track]
|
|
||||||
///
|
|
||||||
/// * If [Track] is [SourcedTrack] then return [SourcedTrack.ytUri]
|
|
||||||
/// * If [Track] is [LocalTrack] then return [LocalTrack.path]
|
|
||||||
/// * If [Track] is [Track] then return [Track.id] with [isUnPlayable] source
|
|
||||||
String makeAppropriateSource(Track track) {
|
|
||||||
if (track is SourcedTrack) {
|
|
||||||
return track.url;
|
|
||||||
} else if (track is LocalTrack) {
|
|
||||||
return track.path;
|
|
||||||
} else {
|
|
||||||
return trackToUnplayableSource(track);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String trackToUnplayableSource(Track track) {
|
|
||||||
return "https://youtube.com/unplayable.m4a?id=${track.id}&title=${Uri.encodeComponent(track.name!)}";
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Track> mapSourcesToTracks(List<String> sources) {
|
|
||||||
return sources
|
|
||||||
.map((source) {
|
|
||||||
final track = state.tracks.firstWhereOrNull(
|
|
||||||
(track) =>
|
|
||||||
trackToUnplayableSource(track) == source ||
|
|
||||||
(track is SourcedTrack && track.url == source) ||
|
|
||||||
(track is LocalTrack && track.path == source),
|
|
||||||
);
|
|
||||||
return track;
|
|
||||||
})
|
|
||||||
.whereNotNull()
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3,87 +3,24 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:catcher_2/catcher_2.dart';
|
import 'package:catcher_2/catcher_2.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/skip_segments.dart';
|
import 'package:spotube/provider/proxy_playlist/skip_segments.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/sourced_track/exceptions.dart';
|
|
||||||
|
|
||||||
extension ProxyPlaylistListeners on ProxyPlaylistNotifier {
|
extension ProxyPlaylistListeners on ProxyPlaylistNotifier {
|
||||||
StreamSubscription<String> subscribeToSourceChanges() =>
|
StreamSubscription subscribeToPlaylist() {
|
||||||
audioPlayer.activeSourceChangedStream.listen((event) {
|
return audioPlayer.playlistStream.listen((playlist) {
|
||||||
try {
|
|
||||||
final newActiveTrack = mapSourcesToTracks([event]).firstOrNull;
|
|
||||||
|
|
||||||
if (newActiveTrack == null ||
|
|
||||||
newActiveTrack.id == state.activeTrack?.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationService.addTrack(newActiveTrack);
|
|
||||||
discord.updatePresence(newActiveTrack);
|
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
active: state.tracks
|
tracks: playlist.medias
|
||||||
.toList()
|
.map((media) => (media as SpotubeMedia).track)
|
||||||
.indexWhere((element) => element.id == newActiveTrack.id),
|
.toSet(),
|
||||||
|
active: playlist.index,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
notificationService.addTrack(state.activeTrack!);
|
||||||
|
discord.updatePresence(state.activeTrack!);
|
||||||
updatePalette();
|
updatePalette();
|
||||||
} catch (e, stackTrace) {
|
|
||||||
Catcher2.reportCheckedError(e, stackTrace);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
StreamSubscription subscribeToPercentCompletion() {
|
|
||||||
final isPreSearching = ObjectRef(false);
|
|
||||||
|
|
||||||
return audioPlayer.percentCompletedStream(2).listen((event) async {
|
|
||||||
if (isPreSearching.value ||
|
|
||||||
audioPlayer.currentSource == null ||
|
|
||||||
audioPlayer.nextSource == null ||
|
|
||||||
isPlayable(audioPlayer.nextSource!)) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
isPreSearching.value = true;
|
|
||||||
|
|
||||||
final track = await ensureSourcePlayable(audioPlayer.nextSource!);
|
|
||||||
|
|
||||||
if (track != null) {
|
|
||||||
state = state.copyWith(tracks: mergeTracks([track], state.tracks));
|
|
||||||
}
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
// Removing tracks that were not found to avoid queue interruption
|
|
||||||
if (e is TrackNotFoundError) {
|
|
||||||
final oldTrack =
|
|
||||||
mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull;
|
|
||||||
await removeTrack(oldTrack!.id!);
|
|
||||||
}
|
|
||||||
Catcher2.reportCheckedError(e, stackTrace);
|
|
||||||
} finally {
|
|
||||||
isPreSearching.value = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
StreamSubscription subscribeToShuffleChanges() {
|
|
||||||
return audioPlayer.shuffledStream.listen((event) {
|
|
||||||
try {
|
|
||||||
final newlyOrderedTracks = mapSourcesToTracks(audioPlayer.sources);
|
|
||||||
|
|
||||||
final newActiveIndex = newlyOrderedTracks.indexWhere(
|
|
||||||
(element) => element.id == state.activeTrack?.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (newActiveIndex == -1) return;
|
|
||||||
|
|
||||||
state = state.copyWith(
|
|
||||||
tracks: newlyOrderedTracks.toSet(),
|
|
||||||
active: newActiveIndex,
|
|
||||||
);
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
Catcher2.reportCheckedError(e, stackTrace);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/extensions/track.dart';
|
import 'package:spotube/extensions/track.dart';
|
||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
@ -14,12 +13,11 @@ class ProxyPlaylist {
|
|||||||
|
|
||||||
factory ProxyPlaylist.fromJson(
|
factory ProxyPlaylist.fromJson(
|
||||||
Map<String, dynamic> json,
|
Map<String, dynamic> json,
|
||||||
Ref ref,
|
|
||||||
) {
|
) {
|
||||||
return ProxyPlaylist(
|
return ProxyPlaylist(
|
||||||
List.castFrom<dynamic, Map<String, dynamic>>(
|
List.castFrom<dynamic, Map<String, dynamic>>(
|
||||||
json['tracks'] ?? <Map<String, dynamic>>[],
|
json['tracks'] ?? <Map<String, dynamic>>[],
|
||||||
).map((t) => _makeAppropriateTrack(t, ref)).toSet(),
|
).map((t) => _makeAppropriateTrack(t)).toSet(),
|
||||||
json['active'] as int?,
|
json['active'] as int?,
|
||||||
json['collections'] == null
|
json['collections'] == null
|
||||||
? {}
|
? {}
|
||||||
@ -58,10 +56,8 @@ class ProxyPlaylist {
|
|||||||
return tracks.every(containsTrack);
|
return tracks.every(containsTrack);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Track _makeAppropriateTrack(Map<String, dynamic> track, Ref ref) {
|
static Track _makeAppropriateTrack(Map<String, dynamic> track) {
|
||||||
if (track.containsKey("ytUri")) {
|
if (track.containsKey("path")) {
|
||||||
return SourcedTrack.fromJson(track, ref: ref);
|
|
||||||
} else if (track.containsKey("path")) {
|
|
||||||
return LocalTrack.fromJson(track);
|
return LocalTrack.fromJson(track);
|
||||||
} else {
|
} else {
|
||||||
return Track.fromJson(track);
|
return Track.fromJson(track);
|
||||||
|
|||||||
@ -1,17 +1,15 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:palette_generator/palette_generator.dart';
|
import 'package:palette_generator/palette_generator.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/extensions/track.dart';
|
||||||
|
|
||||||
import 'package:spotube/provider/blacklist_provider.dart';
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
import 'package:spotube/provider/palette_provider.dart';
|
import 'package:spotube/provider/palette_provider.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/next_fetcher_mixin.dart';
|
|
||||||
import 'package:spotube/provider/proxy_playlist/player_listeners.dart';
|
import 'package:spotube/provider/proxy_playlist/player_listeners.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
||||||
import 'package:spotube/provider/scrobbler_provider.dart';
|
import 'package:spotube/provider/scrobbler_provider.dart';
|
||||||
@ -21,12 +19,10 @@ import 'package:spotube/services/audio_player/audio_player.dart';
|
|||||||
import 'package:spotube/services/audio_services/audio_services.dart';
|
import 'package:spotube/services/audio_services/audio_services.dart';
|
||||||
import 'package:spotube/provider/discord_provider.dart';
|
import 'package:spotube/provider/discord_provider.dart';
|
||||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
||||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
|
||||||
|
|
||||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||||
|
|
||||||
class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist> {
|
||||||
with NextFetcher {
|
|
||||||
final Ref ref;
|
final Ref ref;
|
||||||
late final AudioServices notificationService;
|
late final AudioServices notificationService;
|
||||||
|
|
||||||
@ -54,49 +50,22 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
|||||||
|
|
||||||
_subscriptions = [
|
_subscriptions = [
|
||||||
// These are subscription methods from player_listeners.dart
|
// These are subscription methods from player_listeners.dart
|
||||||
subscribeToSourceChanges(),
|
subscribeToPlaylist(),
|
||||||
subscribeToPercentCompletion(),
|
|
||||||
subscribeToShuffleChanges(),
|
|
||||||
subscribeToSkipSponsor(),
|
subscribeToSkipSponsor(),
|
||||||
subscribeToScrobbleChanged(),
|
subscribeToScrobbleChanged(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SourcedTrack?> ensureSourcePlayable(String source) async {
|
|
||||||
if (isPlayable(source)) return null;
|
|
||||||
|
|
||||||
final track = mapSourcesToTracks([source]).firstOrNull;
|
|
||||||
|
|
||||||
if (track == null || track is LocalTrack) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final nthFetchedTrack = switch (track.runtimeType) {
|
|
||||||
SourcedTrack() => track as SourcedTrack,
|
|
||||||
_ => await SourcedTrack.fetchFromTrack(ref: ref, track: track),
|
|
||||||
};
|
|
||||||
|
|
||||||
await audioPlayer.replaceSource(
|
|
||||||
source,
|
|
||||||
nthFetchedTrack.url,
|
|
||||||
);
|
|
||||||
|
|
||||||
return nthFetchedTrack;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Basic methods for adding or removing tracks to playlist
|
// Basic methods for adding or removing tracks to playlist
|
||||||
|
|
||||||
Future<void> addTrack(Track track) async {
|
Future<void> addTrack(Track track) async {
|
||||||
if (blacklist.contains(track)) return;
|
if (blacklist.contains(track)) return;
|
||||||
state = state.copyWith(tracks: {...state.tracks, track});
|
await audioPlayer.addTrack(SpotubeMedia(track));
|
||||||
await audioPlayer.addTrack(makeAppropriateSource(track));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addTracks(Iterable<Track> tracks) async {
|
Future<void> addTracks(Iterable<Track> tracks) async {
|
||||||
tracks = blacklist.filter(tracks).toList() as List<Track>;
|
tracks = blacklist.filter(tracks).toList() as List<Track>;
|
||||||
state = state.copyWith(tracks: {...state.tracks, ...tracks});
|
|
||||||
for (final track in tracks) {
|
for (final track in tracks) {
|
||||||
await audioPlayer.addTrack(makeAppropriateSource(track));
|
await audioPlayer.addTrack(SpotubeMedia(track));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,25 +83,17 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeTrack(String trackId) async {
|
Future<void> removeTrack(String trackId) async {
|
||||||
final track =
|
final trackIndex =
|
||||||
state.tracks.firstWhereOrNull((element) => element.id == trackId);
|
state.tracks.toList().indexWhere((element) => element.id == trackId);
|
||||||
if (track == null) return;
|
if (trackIndex == -1) return;
|
||||||
state = state.copyWith(tracks: {...state.tracks..remove(track)});
|
await audioPlayer.removeTrack(trackIndex);
|
||||||
final index = audioPlayer.sources.indexOf(makeAppropriateSource(track));
|
|
||||||
if (index == -1) return;
|
|
||||||
await audioPlayer.removeTrack(index);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeTracks(Iterable<String> tracksIds) async {
|
Future<void> removeTracks(Iterable<String> tracksIds) async {
|
||||||
final tracks =
|
final tracks = state.tracks.map((t) => t.id!).toList();
|
||||||
state.tracks.where((element) => tracksIds.contains(element.id));
|
|
||||||
|
|
||||||
state = state.copyWith(tracks: {
|
|
||||||
...state.tracks..removeWhere((element) => tracksIds.contains(element.id))
|
|
||||||
});
|
|
||||||
|
|
||||||
for (final track in tracks) {
|
for (final track in tracks) {
|
||||||
final index = audioPlayer.sources.indexOf(makeAppropriateSource(track));
|
final index = tracks.indexOf(track);
|
||||||
if (index == -1) continue;
|
if (index == -1) continue;
|
||||||
await audioPlayer.removeTrack(index);
|
await audioPlayer.removeTrack(index);
|
||||||
}
|
}
|
||||||
@ -144,64 +105,16 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
|||||||
bool autoPlay = false,
|
bool autoPlay = false,
|
||||||
}) async {
|
}) async {
|
||||||
tracks = blacklist.filter(tracks).toList() as List<Track>;
|
tracks = blacklist.filter(tracks).toList() as List<Track>;
|
||||||
final indexTrack = tracks.elementAtOrNull(initialIndex) ?? tracks.first;
|
|
||||||
|
|
||||||
if (indexTrack is LocalTrack) {
|
|
||||||
state = state.copyWith(
|
|
||||||
tracks: tracks.toSet(),
|
|
||||||
active: initialIndex,
|
|
||||||
collections: {},
|
|
||||||
);
|
|
||||||
await notificationService.addTrack(indexTrack);
|
|
||||||
discord.updatePresence(indexTrack);
|
|
||||||
} else {
|
|
||||||
final addableTrack = await SourcedTrack.fetchFromTrack(
|
|
||||||
ref: ref,
|
|
||||||
track: tracks.elementAtOrNull(initialIndex) ?? tracks.first,
|
|
||||||
).catchError((e, stackTrace) {
|
|
||||||
return SourcedTrack.fetchFromTrack(
|
|
||||||
ref: ref,
|
|
||||||
track: tracks.elementAtOrNull(initialIndex + 1) ?? tracks.first,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
state = state.copyWith(
|
|
||||||
tracks: mergeTracks([addableTrack], tracks),
|
|
||||||
active: initialIndex,
|
|
||||||
collections: {},
|
|
||||||
);
|
|
||||||
await notificationService.addTrack(addableTrack);
|
|
||||||
discord.updatePresence(addableTrack);
|
|
||||||
}
|
|
||||||
|
|
||||||
await audioPlayer.openPlaylist(
|
await audioPlayer.openPlaylist(
|
||||||
state.tracks.map(makeAppropriateSource).toList(),
|
tracks.asMediaList(),
|
||||||
initialIndex: initialIndex,
|
initialIndex: initialIndex,
|
||||||
autoPlay: autoPlay,
|
autoPlay: autoPlay,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> jumpTo(int index) async {
|
Future<void> jumpTo(int index) async {
|
||||||
final oldTrack =
|
|
||||||
mapSourcesToTracks([audioPlayer.currentSource!]).firstOrNull;
|
|
||||||
|
|
||||||
state = state.copyWith(active: index);
|
|
||||||
await audioPlayer.pause();
|
|
||||||
final track = await ensureSourcePlayable(audioPlayer.sources[index]);
|
|
||||||
|
|
||||||
if (track != null) {
|
|
||||||
state = state.copyWith(
|
|
||||||
tracks: mergeTracks([track], state.tracks),
|
|
||||||
active: index,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await audioPlayer.jumpTo(index);
|
await audioPlayer.jumpTo(index);
|
||||||
|
|
||||||
if (oldTrack != null || track != null) {
|
|
||||||
await notificationService.addTrack(track ?? oldTrack!);
|
|
||||||
discord.updatePresence(track ?? oldTrack!);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> jumpToTrack(Track track) async {
|
Future<void> jumpToTrack(Track track) async {
|
||||||
@ -211,7 +124,6 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
|||||||
await jumpTo(index);
|
await jumpTo(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add safe guards for active/playing track that needs to be moved
|
|
||||||
Future<void> moveTrack(int oldIndex, int newIndex) async {
|
Future<void> moveTrack(int oldIndex, int newIndex) async {
|
||||||
if (oldIndex == newIndex ||
|
if (oldIndex == newIndex ||
|
||||||
newIndex < 0 ||
|
newIndex < 0 ||
|
||||||
@ -219,11 +131,6 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
|||||||
newIndex > state.tracks.length - 1 ||
|
newIndex > state.tracks.length - 1 ||
|
||||||
oldIndex > state.tracks.length - 1) return;
|
oldIndex > state.tracks.length - 1) return;
|
||||||
|
|
||||||
final tracks = state.tracks.toList();
|
|
||||||
final track = tracks.removeAt(oldIndex);
|
|
||||||
tracks.insert(newIndex, track);
|
|
||||||
state = state.copyWith(tracks: {...tracks});
|
|
||||||
|
|
||||||
await audioPlayer.moveTrack(oldIndex, newIndex);
|
await audioPlayer.moveTrack(oldIndex, newIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,104 +140,56 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
|||||||
}
|
}
|
||||||
|
|
||||||
tracks = blacklist.filter(tracks).toList() as List<Track>;
|
tracks = blacklist.filter(tracks).toList() as List<Track>;
|
||||||
final destIndex = state.active != null ? state.active! + 1 : 0;
|
|
||||||
final newTracks = state.tracks.toList()..insertAll(destIndex, tracks);
|
|
||||||
state = state.copyWith(tracks: newTracks.toSet());
|
|
||||||
|
|
||||||
tracks.forEachIndexed((index, track) async {
|
for (int i = 0; i < tracks.length; i++) {
|
||||||
audioPlayer.addTrackAt(
|
final track = tracks.elementAt(i);
|
||||||
makeAppropriateSource(track),
|
|
||||||
destIndex + index,
|
await audioPlayer.addTrackAt(
|
||||||
|
SpotubeMedia(track),
|
||||||
|
i + 1,
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> populateSibling() async {
|
Future<void> populateSibling() async {
|
||||||
if (state.activeTrack is SourcedTrack) {
|
// if (state.activeTrack is SourcedTrack) {
|
||||||
final activeTrackWithSiblingsForSure =
|
// final activeTrackWithSiblingsForSure =
|
||||||
await (state.activeTrack as SourcedTrack).copyWithSibling();
|
// await (state.activeTrack as SourcedTrack).copyWithSibling();
|
||||||
|
|
||||||
state = state.copyWith(
|
// state = state.copyWith(
|
||||||
tracks: mergeTracks([activeTrackWithSiblingsForSure], state.tracks),
|
// tracks: mergeTracks([activeTrackWithSiblingsForSure], state.tracks),
|
||||||
active: state.tracks.toList().indexWhere(
|
// active: state.tracks.toList().indexWhere(
|
||||||
(element) => element.id == activeTrackWithSiblingsForSure.id),
|
// (element) => element.id == activeTrackWithSiblingsForSure.id),
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> swapSibling(SourceInfo sibling) async {
|
Future<void> swapSibling(SourceInfo sibling) async {
|
||||||
if (state.activeTrack is SourcedTrack) {
|
// if (state.activeTrack is SourcedTrack) {
|
||||||
await populateSibling();
|
// await populateSibling();
|
||||||
final newTrack =
|
// final newTrack =
|
||||||
await (state.activeTrack as SourcedTrack).swapWithSibling(sibling);
|
// await (state.activeTrack as SourcedTrack).swapWithSibling(sibling);
|
||||||
if (newTrack == null) return;
|
// if (newTrack == null) return;
|
||||||
state = state.copyWith(
|
// state = state.copyWith(
|
||||||
tracks: mergeTracks([newTrack], state.tracks),
|
// tracks: mergeTracks([newTrack], state.tracks),
|
||||||
active: state.tracks
|
// active: state.tracks
|
||||||
.toList()
|
// .toList()
|
||||||
.indexWhere((element) => element.id == newTrack.id),
|
// .indexWhere((element) => element.id == newTrack.id),
|
||||||
);
|
// );
|
||||||
await audioPlayer.pause();
|
// await audioPlayer.pause();
|
||||||
await audioPlayer.replaceSource(
|
// await audioPlayer.replaceSource(
|
||||||
audioPlayer.currentSource!,
|
// audioPlayer.currentSource!,
|
||||||
makeAppropriateSource(newTrack),
|
// makeAppropriateSource(newTrack),
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> next() async {
|
Future<void> next() async {
|
||||||
if (audioPlayer.nextSource == null) return;
|
|
||||||
final oldTrack = mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull;
|
|
||||||
|
|
||||||
state = state.copyWith(
|
|
||||||
active: state.tracks
|
|
||||||
.toList()
|
|
||||||
.indexWhere((element) => element.id == oldTrack?.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
await audioPlayer.pause();
|
|
||||||
final track = await ensureSourcePlayable(audioPlayer.nextSource!);
|
|
||||||
|
|
||||||
if (track != null) {
|
|
||||||
state = state.copyWith(
|
|
||||||
tracks: mergeTracks([track], state.tracks),
|
|
||||||
active: state.tracks
|
|
||||||
.toList()
|
|
||||||
.indexWhere((element) => element.id == track.id),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await audioPlayer.skipToNext();
|
await audioPlayer.skipToNext();
|
||||||
|
|
||||||
if (oldTrack != null || track != null) {
|
|
||||||
await notificationService.addTrack(track ?? oldTrack!);
|
|
||||||
discord.updatePresence(track ?? oldTrack!);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> previous() async {
|
Future<void> previous() async {
|
||||||
if (audioPlayer.previousSource == null) return;
|
|
||||||
final oldTrack =
|
|
||||||
mapSourcesToTracks([audioPlayer.previousSource!]).firstOrNull;
|
|
||||||
state = state.copyWith(
|
|
||||||
active: state.tracks
|
|
||||||
.toList()
|
|
||||||
.indexWhere((element) => element.id == oldTrack?.id),
|
|
||||||
);
|
|
||||||
await audioPlayer.pause();
|
|
||||||
final track = await ensureSourcePlayable(audioPlayer.previousSource!);
|
|
||||||
if (track != null) {
|
|
||||||
state = state.copyWith(
|
|
||||||
tracks: mergeTracks([track], state.tracks),
|
|
||||||
active: state.tracks
|
|
||||||
.toList()
|
|
||||||
.indexWhere((element) => element.id == track.id),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await audioPlayer.skipToPrevious();
|
await audioPlayer.skipToPrevious();
|
||||||
if (oldTrack != null || track != null) {
|
|
||||||
await notificationService.addTrack(track ?? oldTrack!);
|
|
||||||
discord.updatePresence(track ?? oldTrack!);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> stop() async {
|
Future<void> stop() async {
|
||||||
@ -385,7 +244,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<ProxyPlaylist> fromJson(Map<String, dynamic> json) {
|
FutureOr<ProxyPlaylist> fromJson(Map<String, dynamic> json) {
|
||||||
return ProxyPlaylist.fromJson(json, ref);
|
return ProxyPlaylist.fromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import 'package:catcher_2/catcher_2.dart';
|
import 'package:catcher_2/catcher_2.dart';
|
||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/services/audio_player/mk_state_player.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
|
import 'package:spotube/services/audio_player/custom_player.dart';
|
||||||
// import 'package:just_audio/just_audio.dart' as ja;
|
// import 'package:just_audio/just_audio.dart' as ja;
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
@ -13,12 +14,29 @@ import 'package:spotube/services/sourced_track/sourced_track.dart';
|
|||||||
part 'audio_players_streams_mixin.dart';
|
part 'audio_players_streams_mixin.dart';
|
||||||
part 'audio_player_impl.dart';
|
part 'audio_player_impl.dart';
|
||||||
|
|
||||||
|
class SpotubeMedia extends mk.Media {
|
||||||
|
final Track track;
|
||||||
|
SpotubeMedia(
|
||||||
|
this.track, {
|
||||||
|
Map<String, String>? extras,
|
||||||
|
super.httpHeaders,
|
||||||
|
}) : super(
|
||||||
|
track is LocalTrack
|
||||||
|
? track.path
|
||||||
|
: "http://localhost:3000/stream/${track.id}",
|
||||||
|
extras: {
|
||||||
|
...?extras,
|
||||||
|
"trackId": track.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
abstract class AudioPlayerInterface {
|
abstract class AudioPlayerInterface {
|
||||||
final MkPlayerWithState _mkPlayer;
|
final CustomPlayer _mkPlayer;
|
||||||
// final ja.AudioPlayer? _justAudxio;
|
// final ja.AudioPlayer? _justAudxio;
|
||||||
|
|
||||||
AudioPlayerInterface()
|
AudioPlayerInterface()
|
||||||
: _mkPlayer = MkPlayerWithState(
|
: _mkPlayer = CustomPlayer(
|
||||||
configuration: const mk.PlayerConfiguration(
|
configuration: const mk.PlayerConfiguration(
|
||||||
title: "Spotube",
|
title: "Spotube",
|
||||||
),
|
),
|
||||||
@ -61,18 +79,18 @@ abstract class AudioPlayerInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AudioDevice> get selectedDevice async {
|
Future<mk.AudioDevice> get selectedDevice async {
|
||||||
return _mkPlayer.state.audioDevice;
|
return _mkPlayer.state.audioDevice;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<AudioDevice>> get devices async {
|
Future<List<mk.AudioDevice>> get devices async {
|
||||||
return _mkPlayer.state.audioDevices;
|
return _mkPlayer.state.audioDevices;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get hasSource {
|
bool get hasSource {
|
||||||
return _mkPlayer.playlist.medias.isNotEmpty;
|
return _mkPlayer.state.playlist.medias.isNotEmpty;
|
||||||
// if (mkSupportedPlatform) {
|
// if (mkSupportedPlatform) {
|
||||||
// return _mkPlayer.playlist.medias.isNotEmpty;
|
// return _mkPlayer.state.playlist.medias.isNotEmpty;
|
||||||
// } else {
|
// } else {
|
||||||
// return _justAudio!.audioSource != null;
|
// return _justAudio!.audioSource != null;
|
||||||
// }
|
// }
|
||||||
@ -125,7 +143,7 @@ abstract class AudioPlayerInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PlaybackLoopMode get loopMode {
|
PlaybackLoopMode get loopMode {
|
||||||
return PlaybackLoopMode.fromPlaylistMode(_mkPlayer.loopMode);
|
return PlaybackLoopMode.fromPlaylistMode(_mkPlayer.state.playlistMode);
|
||||||
// if (mkSupportedPlatform) {
|
// if (mkSupportedPlatform) {
|
||||||
// return PlaybackLoopMode.fromPlaylistMode(_mkPlayer.loopMode);
|
// return PlaybackLoopMode.fromPlaylistMode(_mkPlayer.loopMode);
|
||||||
// } else {
|
// } else {
|
||||||
|
|||||||
@ -83,7 +83,7 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
|
|||||||
// await _justAudio?.setSpeed(speed);
|
// await _justAudio?.setSpeed(speed);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setAudioDevice(AudioDevice device) async {
|
Future<void> setAudioDevice(mk.AudioDevice device) async {
|
||||||
await _mkPlayer.setAudioDevice(device);
|
await _mkPlayer.setAudioDevice(device);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,7 +95,7 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
|
|||||||
// Playlist related
|
// Playlist related
|
||||||
|
|
||||||
Future<void> openPlaylist(
|
Future<void> openPlaylist(
|
||||||
List<String> tracks, {
|
List<mk.Media> tracks, {
|
||||||
bool autoPlay = true,
|
bool autoPlay = true,
|
||||||
int initialIndex = 0,
|
int initialIndex = 0,
|
||||||
}) async {
|
}) async {
|
||||||
@ -103,10 +103,7 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
|
|||||||
assert(initialIndex <= tracks.length - 1);
|
assert(initialIndex <= tracks.length - 1);
|
||||||
// if (mkSupportedPlatform) {
|
// if (mkSupportedPlatform) {
|
||||||
await _mkPlayer.open(
|
await _mkPlayer.open(
|
||||||
mk.Playlist(
|
mk.Playlist(tracks, index: initialIndex),
|
||||||
tracks.map(mk.Media.new).toList(),
|
|
||||||
index: initialIndex,
|
|
||||||
),
|
|
||||||
play: autoPlay,
|
play: autoPlay,
|
||||||
);
|
);
|
||||||
// } else {
|
// } else {
|
||||||
@ -137,7 +134,7 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
|
|||||||
|
|
||||||
List<String> get sources {
|
List<String> get sources {
|
||||||
// if (mkSupportedPlatform) {
|
// if (mkSupportedPlatform) {
|
||||||
return _mkPlayer.playlist.medias.map((e) => e.uri).toList();
|
return _mkPlayer.state.playlist.medias.map((e) => e.uri).toList();
|
||||||
// } else {
|
// } else {
|
||||||
// return _justAudio!.sequenceState?.effectiveSequence
|
// return _justAudio!.sequenceState?.effectiveSequence
|
||||||
// .map((e) => (e as ja.UriAudioSource).uri.toString())
|
// .map((e) => (e as ja.UriAudioSource).uri.toString())
|
||||||
@ -148,9 +145,9 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
|
|||||||
|
|
||||||
String? get currentSource {
|
String? get currentSource {
|
||||||
// if (mkSupportedPlatform) {
|
// if (mkSupportedPlatform) {
|
||||||
if (_mkPlayer.playlist.index == -1) return null;
|
if (_mkPlayer.state.playlist.index == -1) return null;
|
||||||
return _mkPlayer.playlist.medias
|
return _mkPlayer.state.playlist.medias
|
||||||
.elementAtOrNull(_mkPlayer.playlist.index)
|
.elementAtOrNull(_mkPlayer.state.playlist.index)
|
||||||
?.uri;
|
?.uri;
|
||||||
// } else {
|
// } else {
|
||||||
// return (_justAudio?.sequenceState?.effectiveSequence
|
// return (_justAudio?.sequenceState?.effectiveSequence
|
||||||
@ -165,12 +162,13 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
|
|||||||
// if (mkSupportedPlatform) {
|
// if (mkSupportedPlatform) {
|
||||||
|
|
||||||
if (loopMode == PlaybackLoopMode.all &&
|
if (loopMode == PlaybackLoopMode.all &&
|
||||||
_mkPlayer.playlist.index == _mkPlayer.playlist.medias.length - 1) {
|
_mkPlayer.state.playlist.index ==
|
||||||
|
_mkPlayer.state.playlist.medias.length - 1) {
|
||||||
return sources.first;
|
return sources.first;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _mkPlayer.playlist.medias
|
return _mkPlayer.state.playlist.medias
|
||||||
.elementAtOrNull(_mkPlayer.playlist.index + 1)
|
.elementAtOrNull(_mkPlayer.state.playlist.index + 1)
|
||||||
?.uri;
|
?.uri;
|
||||||
// } else {
|
// } else {
|
||||||
// return (_justAudio?.sequenceState?.effectiveSequence
|
// return (_justAudio?.sequenceState?.effectiveSequence
|
||||||
@ -182,13 +180,14 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
String? get previousSource {
|
String? get previousSource {
|
||||||
if (loopMode == PlaybackLoopMode.all && _mkPlayer.playlist.index == 0) {
|
if (loopMode == PlaybackLoopMode.all &&
|
||||||
|
_mkPlayer.state.playlist.index == 0) {
|
||||||
return sources.last;
|
return sources.last;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (mkSupportedPlatform) {
|
// if (mkSupportedPlatform) {
|
||||||
return _mkPlayer.playlist.medias
|
return _mkPlayer.state.playlist.medias
|
||||||
.elementAtOrNull(_mkPlayer.playlist.index - 1)
|
.elementAtOrNull(_mkPlayer.state.playlist.index - 1)
|
||||||
?.uri;
|
?.uri;
|
||||||
// } else {
|
// } else {
|
||||||
// return (_justAudio?.sequenceState?.effectiveSequence
|
// return (_justAudio?.sequenceState?.effectiveSequence
|
||||||
@ -223,20 +222,18 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addTrack(String url) async {
|
Future<void> addTrack(mk.Media media) async {
|
||||||
final urlType = _resolveUrlType(url);
|
|
||||||
// if (mkSupportedPlatform && urlType is mk.Media) {
|
// if (mkSupportedPlatform && urlType is mk.Media) {
|
||||||
await _mkPlayer.add(urlType as mk.Media);
|
await _mkPlayer.add(media);
|
||||||
// } else {
|
// } else {
|
||||||
// await (_justAudio!.audioSource as ja.ConcatenatingAudioSource)
|
// await (_justAudio!.audioSource as ja.ConcatenatingAudioSource)
|
||||||
// .add(urlType as ja.AudioSource);
|
// .add(urlType as ja.AudioSource);
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addTrackAt(String url, int index) async {
|
Future<void> addTrackAt(mk.Media media, int index) async {
|
||||||
final urlType = _resolveUrlType(url);
|
|
||||||
// if (mkSupportedPlatform && urlType is mk.Media) {
|
// if (mkSupportedPlatform && urlType is mk.Media) {
|
||||||
await _mkPlayer.insert(index, urlType as mk.Media);
|
await _mkPlayer.insert(index, media);
|
||||||
// } else {
|
// } else {
|
||||||
// await (_justAudio!.audioSource as ja.ConcatenatingAudioSource)
|
// await (_justAudio!.audioSource as ja.ConcatenatingAudioSource)
|
||||||
// .insert(index, urlType as ja.AudioSource);
|
// .insert(index, urlType as ja.AudioSource);
|
||||||
@ -270,7 +267,7 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
|
|||||||
if (oldSourceIndex == -1) return;
|
if (oldSourceIndex == -1) return;
|
||||||
|
|
||||||
// if (mkSupportedPlatform) {
|
// if (mkSupportedPlatform) {
|
||||||
_mkPlayer.replace(oldSource, newSource);
|
// _mkPlayer.replace(oldSource, newSource);
|
||||||
// } else {
|
// } else {
|
||||||
// final playlist = _justAudio!.audioSource as ja.ConcatenatingAudioSource;
|
// final playlist = _justAudio!.audioSource as ja.ConcatenatingAudioSource;
|
||||||
|
|
||||||
|
|||||||
@ -73,7 +73,7 @@ mixin SpotubeAudioPlayersStreams on AudioPlayerInterface {
|
|||||||
|
|
||||||
Stream<PlaybackLoopMode> get loopModeStream {
|
Stream<PlaybackLoopMode> get loopModeStream {
|
||||||
// if (mkSupportedPlatform) {
|
// if (mkSupportedPlatform) {
|
||||||
return _mkPlayer.loopModeStream.map(PlaybackLoopMode.fromPlaylistMode);
|
return _mkPlayer.stream.playlistMode.map(PlaybackLoopMode.fromPlaylistMode);
|
||||||
// } else {
|
// } else {
|
||||||
// return _justAudio!.loopModeStream
|
// return _justAudio!.loopModeStream
|
||||||
// .map(PlaybackLoopMode.fromLoopMode)
|
// .map(PlaybackLoopMode.fromLoopMode)
|
||||||
@ -127,7 +127,7 @@ mixin SpotubeAudioPlayersStreams on AudioPlayerInterface {
|
|||||||
// if (mkSupportedPlatform) {
|
// if (mkSupportedPlatform) {
|
||||||
return _mkPlayer.indexChangeStream
|
return _mkPlayer.indexChangeStream
|
||||||
.map((event) {
|
.map((event) {
|
||||||
return _mkPlayer.playlist.medias.elementAtOrNull(event)?.uri;
|
return _mkPlayer.state.playlist.medias.elementAtOrNull(event)?.uri;
|
||||||
})
|
})
|
||||||
.where((event) => event != null)
|
.where((event) => event != null)
|
||||||
.cast<String>();
|
.cast<String>();
|
||||||
@ -141,11 +141,13 @@ mixin SpotubeAudioPlayersStreams on AudioPlayerInterface {
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<AudioDevice>> get devicesStream =>
|
Stream<List<mk.AudioDevice>> get devicesStream =>
|
||||||
_mkPlayer.stream.audioDevices.asBroadcastStream();
|
_mkPlayer.stream.audioDevices.asBroadcastStream();
|
||||||
|
|
||||||
Stream<AudioDevice> get selectedDeviceStream =>
|
Stream<mk.AudioDevice> get selectedDeviceStream =>
|
||||||
_mkPlayer.stream.audioDevice.asBroadcastStream();
|
_mkPlayer.stream.audioDevice.asBroadcastStream();
|
||||||
|
|
||||||
Stream<String> get errorStream => _mkPlayer.stream.error;
|
Stream<String> get errorStream => _mkPlayer.stream.error;
|
||||||
|
|
||||||
|
Stream<mk.Playlist> get playlistStream => _mkPlayer.stream.playlist;
|
||||||
}
|
}
|
||||||
|
|||||||
141
lib/services/audio_player/custom_player.dart
Normal file
141
lib/services/audio_player/custom_player.dart
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||||
|
import 'package:catcher_2/catcher_2.dart';
|
||||||
|
import 'package:media_kit/media_kit.dart';
|
||||||
|
import 'package:flutter_broadcasts/flutter_broadcasts.dart';
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
import 'package:audio_session/audio_session.dart';
|
||||||
|
// ignore: implementation_imports
|
||||||
|
import 'package:spotube/services/audio_player/playback_state.dart';
|
||||||
|
|
||||||
|
/// MediaKit [Player] by default doesn't have a state stream.
|
||||||
|
/// This class adds a state stream to the [Player] class.
|
||||||
|
class CustomPlayer extends Player {
|
||||||
|
final StreamController<AudioPlaybackState> _playerStateStream;
|
||||||
|
final StreamController<bool> _shuffleStream;
|
||||||
|
|
||||||
|
late final List<StreamSubscription> _subscriptions;
|
||||||
|
|
||||||
|
bool _shuffled;
|
||||||
|
int _androidAudioSessionId = 0;
|
||||||
|
String _packageName = "";
|
||||||
|
AndroidAudioManager? _androidAudioManager;
|
||||||
|
|
||||||
|
CustomPlayer({super.configuration})
|
||||||
|
: _playerStateStream = StreamController.broadcast(),
|
||||||
|
_shuffleStream = StreamController.broadcast(),
|
||||||
|
_shuffled = false {
|
||||||
|
_subscriptions = [
|
||||||
|
stream.buffering.listen((event) {
|
||||||
|
_playerStateStream.add(AudioPlaybackState.buffering);
|
||||||
|
}),
|
||||||
|
stream.playing.listen((playing) {
|
||||||
|
if (playing) {
|
||||||
|
_playerStateStream.add(AudioPlaybackState.playing);
|
||||||
|
} else {
|
||||||
|
_playerStateStream.add(AudioPlaybackState.paused);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
stream.completed.listen((isCompleted) async {
|
||||||
|
if (!isCompleted) return;
|
||||||
|
_playerStateStream.add(AudioPlaybackState.completed);
|
||||||
|
}),
|
||||||
|
stream.playlist.listen((event) {
|
||||||
|
if (event.medias.isEmpty) {
|
||||||
|
_playerStateStream.add(AudioPlaybackState.stopped);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
stream.error.listen((event) {
|
||||||
|
Catcher2.reportCheckedError('[MediaKitError] \n$event', null);
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
PackageInfo.fromPlatform().then((packageInfo) {
|
||||||
|
_packageName = packageInfo.packageName;
|
||||||
|
});
|
||||||
|
if (DesktopTools.platform.isAndroid) {
|
||||||
|
_androidAudioManager = AndroidAudioManager();
|
||||||
|
AudioSession.instance.then((s) async {
|
||||||
|
_androidAudioSessionId =
|
||||||
|
await _androidAudioManager!.generateAudioSessionId();
|
||||||
|
notifyAudioSessionUpdate(true);
|
||||||
|
|
||||||
|
await nativePlayer.setProperty(
|
||||||
|
"audiotrack-session-id",
|
||||||
|
_androidAudioSessionId.toString(),
|
||||||
|
);
|
||||||
|
await nativePlayer.setProperty("ao", "audiotrack,opensles,");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> notifyAudioSessionUpdate(bool active) async {
|
||||||
|
if (DesktopTools.platform.isAndroid) {
|
||||||
|
sendBroadcast(
|
||||||
|
BroadcastMessage(
|
||||||
|
name: active
|
||||||
|
? "android.media.action.OPEN_AUDIO_EFFECT_CONTROL_SESSION"
|
||||||
|
: "android.media.action.CLOSE_AUDIO_EFFECT_CONTROL_SESSION",
|
||||||
|
data: {
|
||||||
|
"android.media.extra.AUDIO_SESSION": _androidAudioSessionId,
|
||||||
|
"android.media.extra.PACKAGE_NAME": _packageName
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get shuffled => _shuffled;
|
||||||
|
|
||||||
|
Stream<AudioPlaybackState> get playerStateStream => _playerStateStream.stream;
|
||||||
|
Stream<bool> get shuffleStream => _shuffleStream.stream;
|
||||||
|
Stream<int> get indexChangeStream {
|
||||||
|
int oldIndex = state.playlist.index;
|
||||||
|
return stream.playlist.map((event) => event.index).where((newIndex) {
|
||||||
|
if (newIndex != oldIndex) {
|
||||||
|
oldIndex = newIndex;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setShuffle(bool shuffle) async {
|
||||||
|
_shuffled = shuffle;
|
||||||
|
await super.setShuffle(shuffle);
|
||||||
|
_shuffleStream.add(shuffle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> stop() async {
|
||||||
|
await super.stop();
|
||||||
|
|
||||||
|
_shuffled = false;
|
||||||
|
_playerStateStream.add(AudioPlaybackState.stopped);
|
||||||
|
_shuffleStream.add(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() async {
|
||||||
|
for (var element in _subscriptions) {
|
||||||
|
element.cancel();
|
||||||
|
}
|
||||||
|
await notifyAudioSessionUpdate(false);
|
||||||
|
return super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
NativePlayer get nativePlayer => platform as NativePlayer;
|
||||||
|
|
||||||
|
Future<void> insert(int index, Media media) async {
|
||||||
|
await add(media);
|
||||||
|
await move(state.playlist.medias.length, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setAudioNormalization(bool normalize) async {
|
||||||
|
if (normalize) {
|
||||||
|
await nativePlayer.setProperty('af', 'dynaudnorm=g=5:f=250:r=0.9:p=0.5');
|
||||||
|
} else {
|
||||||
|
await nativePlayer.setProperty('af', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,382 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
|
||||||
import 'package:catcher_2/catcher_2.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:media_kit/media_kit.dart';
|
|
||||||
import 'package:flutter_broadcasts/flutter_broadcasts.dart';
|
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
|
||||||
import 'package:audio_session/audio_session.dart';
|
|
||||||
// ignore: implementation_imports
|
|
||||||
import 'package:spotube/services/audio_player/playback_state.dart';
|
|
||||||
|
|
||||||
/// MediaKit [Player] by default doesn't have a state stream.
|
|
||||||
/// This class adds a state stream to the [Player] class.
|
|
||||||
class MkPlayerWithState extends Player {
|
|
||||||
final StreamController<AudioPlaybackState> _playerStateStream;
|
|
||||||
final StreamController<Playlist> _playlistStream;
|
|
||||||
final StreamController<bool> _shuffleStream;
|
|
||||||
final StreamController<PlaylistMode> _loopModeStream;
|
|
||||||
|
|
||||||
late final List<StreamSubscription> _subscriptions;
|
|
||||||
|
|
||||||
bool _shuffled;
|
|
||||||
PlaylistMode _loopMode;
|
|
||||||
|
|
||||||
Playlist? _playlist;
|
|
||||||
List<Media>? _tempMedias;
|
|
||||||
int _androidAudioSessionId = 0;
|
|
||||||
String _packageName = "";
|
|
||||||
AndroidAudioManager? _androidAudioManager;
|
|
||||||
|
|
||||||
MkPlayerWithState({super.configuration})
|
|
||||||
: _playerStateStream = StreamController.broadcast(),
|
|
||||||
_shuffleStream = StreamController.broadcast(),
|
|
||||||
_loopModeStream = StreamController.broadcast(),
|
|
||||||
_playlistStream = StreamController.broadcast(),
|
|
||||||
_shuffled = false,
|
|
||||||
_loopMode = PlaylistMode.none {
|
|
||||||
_subscriptions = [
|
|
||||||
stream.buffering.listen((event) {
|
|
||||||
_playerStateStream.add(AudioPlaybackState.buffering);
|
|
||||||
}),
|
|
||||||
stream.playing.listen((playing) {
|
|
||||||
if (playing) {
|
|
||||||
_playerStateStream.add(AudioPlaybackState.playing);
|
|
||||||
} else {
|
|
||||||
_playerStateStream.add(AudioPlaybackState.paused);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
stream.completed.listen((isCompleted) async {
|
|
||||||
try {
|
|
||||||
if (!isCompleted) return;
|
|
||||||
|
|
||||||
_playerStateStream.add(AudioPlaybackState.completed);
|
|
||||||
if (loopMode == PlaylistMode.single) {
|
|
||||||
await super.open(_playlist!.medias[_playlist!.index], play: true);
|
|
||||||
} else {
|
|
||||||
await next();
|
|
||||||
await Future.delayed(const Duration(milliseconds: 250), play);
|
|
||||||
}
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
Catcher2.reportCheckedError(e, stackTrace);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
stream.playlist.listen((event) {
|
|
||||||
if (event.medias.isEmpty) {
|
|
||||||
_playerStateStream.add(AudioPlaybackState.stopped);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
stream.error.listen((event) {
|
|
||||||
Catcher2.reportCheckedError('[MediaKitError] \n$event', null);
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
PackageInfo.fromPlatform().then((packageInfo) {
|
|
||||||
_packageName = packageInfo.packageName;
|
|
||||||
});
|
|
||||||
if (DesktopTools.platform.isAndroid) {
|
|
||||||
_androidAudioManager = AndroidAudioManager();
|
|
||||||
AudioSession.instance.then((s) async {
|
|
||||||
_androidAudioSessionId =
|
|
||||||
await _androidAudioManager!.generateAudioSessionId();
|
|
||||||
notifyAudioSessionUpdate(true);
|
|
||||||
|
|
||||||
await nativePlayer.setProperty(
|
|
||||||
"audiotrack-session-id",
|
|
||||||
_androidAudioSessionId.toString(),
|
|
||||||
);
|
|
||||||
await nativePlayer.setProperty("ao", "audiotrack,opensles,");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> notifyAudioSessionUpdate(bool active) async {
|
|
||||||
if (DesktopTools.platform.isAndroid) {
|
|
||||||
sendBroadcast(
|
|
||||||
BroadcastMessage(
|
|
||||||
name: active
|
|
||||||
? "android.media.action.OPEN_AUDIO_EFFECT_CONTROL_SESSION"
|
|
||||||
: "android.media.action.CLOSE_AUDIO_EFFECT_CONTROL_SESSION",
|
|
||||||
data: {
|
|
||||||
"android.media.extra.AUDIO_SESSION": _androidAudioSessionId,
|
|
||||||
"android.media.extra.PACKAGE_NAME": _packageName
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get shuffled => _shuffled;
|
|
||||||
PlaylistMode get loopMode => _loopMode;
|
|
||||||
Playlist get playlist => _playlist ?? const Playlist([], index: -1);
|
|
||||||
|
|
||||||
Stream<AudioPlaybackState> get playerStateStream => _playerStateStream.stream;
|
|
||||||
Stream<bool> get shuffleStream => _shuffleStream.stream;
|
|
||||||
Stream<PlaylistMode> get loopModeStream => _loopModeStream.stream;
|
|
||||||
Stream<Playlist> get playlistStream => _playlistStream.stream;
|
|
||||||
Stream<int> get indexChangeStream {
|
|
||||||
int oldIndex = playlist.index;
|
|
||||||
return playlistStream.map((event) => event.index).where((newIndex) {
|
|
||||||
if (newIndex != oldIndex) {
|
|
||||||
oldIndex = newIndex;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
set playlist(Playlist playlist) {
|
|
||||||
_playlist = playlist;
|
|
||||||
_playlistStream.add(playlist);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> setShuffle(bool shuffle) async {
|
|
||||||
_shuffled = shuffle;
|
|
||||||
if (shuffle) {
|
|
||||||
_tempMedias = _playlist!.medias;
|
|
||||||
final active = _playlist!.medias[_playlist!.index];
|
|
||||||
final newMedias = _playlist!.medias.toList()
|
|
||||||
..shuffle()
|
|
||||||
..remove(active)
|
|
||||||
..insert(0, active);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
await super.setShuffle(shuffle);
|
|
||||||
_shuffleStream.add(shuffle);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> setPlaylistMode(PlaylistMode playlistMode) async {
|
|
||||||
_loopMode = playlistMode;
|
|
||||||
await super.setPlaylistMode(playlistMode);
|
|
||||||
_loopModeStream.add(playlistMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> stop() async {
|
|
||||||
await super.stop();
|
|
||||||
await pause();
|
|
||||||
await seek(Duration.zero);
|
|
||||||
|
|
||||||
_loopMode = PlaylistMode.none;
|
|
||||||
_shuffled = false;
|
|
||||||
_playlist = null;
|
|
||||||
_tempMedias = null;
|
|
||||||
_playerStateStream.add(AudioPlaybackState.stopped);
|
|
||||||
_shuffleStream.add(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> dispose() async {
|
|
||||||
for (var element in _subscriptions) {
|
|
||||||
element.cancel();
|
|
||||||
}
|
|
||||||
await notifyAudioSessionUpdate(false);
|
|
||||||
return super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> open(
|
|
||||||
Playable playable, {
|
|
||||||
bool play = true,
|
|
||||||
}) async {
|
|
||||||
await stop();
|
|
||||||
if (playable is Playlist) {
|
|
||||||
playlist = playable;
|
|
||||||
super.open(playable.medias[playable.index], play: play);
|
|
||||||
}
|
|
||||||
await super.open(playable, play: play);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> next() async {
|
|
||||||
if (_playlist == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final isLast = _playlist!.index == _playlist!.medias.length - 1;
|
|
||||||
|
|
||||||
if (isLast) {
|
|
||||||
switch (loopMode) {
|
|
||||||
case PlaylistMode.loop:
|
|
||||||
playlist = _playlist!.copyWith(index: 0);
|
|
||||||
super.open(_playlist!.medias[_playlist!.index], play: true);
|
|
||||||
break;
|
|
||||||
case PlaylistMode.none:
|
|
||||||
// Fixes auto-repeating the last track
|
|
||||||
await super.stop();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
playlist = _playlist!.copyWith(index: _playlist!.index + 1);
|
|
||||||
|
|
||||||
return super.open(_playlist!.medias[_playlist!.index], play: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> previous() async {
|
|
||||||
if (_playlist == null || _playlist!.index - 1 < 0) return;
|
|
||||||
|
|
||||||
if (loopMode == PlaylistMode.loop && _playlist!.index == 0) {
|
|
||||||
playlist = _playlist!.copyWith(index: _playlist!.medias.length - 1);
|
|
||||||
return super.open(_playlist!.medias[_playlist!.index], play: true);
|
|
||||||
} else if (_playlist!.index != 0) {
|
|
||||||
playlist = _playlist!.copyWith(index: _playlist!.index - 1);
|
|
||||||
return super.open(_playlist!.medias[_playlist!.index], play: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> jump(int index) async {
|
|
||||||
if (_playlist == null || index < 0 || index >= _playlist!.medias.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
playlist = _playlist!.copyWith(index: index);
|
|
||||||
return super.open(_playlist!.medias[index], play: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> move(int from, int to) async {
|
|
||||||
if (_playlist == null ||
|
|
||||||
from >= _playlist!.medias.length ||
|
|
||||||
to >= _playlist!.medias.length) return;
|
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This replaces the old source with a new one
|
|
||||||
///
|
|
||||||
/// If the old source is playing, the new one will play
|
|
||||||
/// from the beginning
|
|
||||||
///
|
|
||||||
/// This doesn't work when [playlist] is null
|
|
||||||
void replace(String oldUrl, String newUrl) {
|
|
||||||
if (_playlist == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final isOldUrlPlaying = _playlist!.medias[_playlist!.index].uri == oldUrl;
|
|
||||||
|
|
||||||
// ends the loop where match is found
|
|
||||||
// tends to be a bit more efficient than forEach
|
|
||||||
_playlist!.medias.firstWhereIndexedOrNull((i, media) {
|
|
||||||
if (media.uri != oldUrl) return false;
|
|
||||||
if (isOldUrlPlaying) {
|
|
||||||
pause();
|
|
||||||
}
|
|
||||||
final copyMedias = [..._playlist!.medias];
|
|
||||||
copyMedias[i] = Media(newUrl, extras: media.extras);
|
|
||||||
playlist = _playlist!.copyWith(medias: copyMedias);
|
|
||||||
if (isOldUrlPlaying) {
|
|
||||||
super.open(
|
|
||||||
copyMedias[i],
|
|
||||||
play: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// replace in the _tempMedias if it's not null
|
|
||||||
if (shuffled && _tempMedias != null) {
|
|
||||||
final tempIndex = _tempMedias!.indexOf(media);
|
|
||||||
_tempMedias![tempIndex] = Media(newUrl, extras: media.extras);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> add(Media media) async {
|
|
||||||
if (_playlist == null) return;
|
|
||||||
|
|
||||||
playlist = _playlist!.copyWith(
|
|
||||||
medias: [..._playlist!.medias, media],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shuffled && _tempMedias != null) {
|
|
||||||
_tempMedias!.add(media);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FutureOr<void> insert(int index, Media media) {
|
|
||||||
if (_playlist == null ||
|
|
||||||
index < 0 ||
|
|
||||||
(_playlist!.medias.length > 1 &&
|
|
||||||
index > _playlist!.medias.length - 1)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final newMedias = _playlist!.medias.toList()..insert(index, media);
|
|
||||||
|
|
||||||
playlist = _playlist!.copyWith(
|
|
||||||
medias: newMedias,
|
|
||||||
index: newMedias.indexOf(_playlist!.medias[_playlist!.index]),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shuffled && _tempMedias != null) {
|
|
||||||
_tempMedias!.insert(index, media);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Doesn't work when active media is the one to be removed
|
|
||||||
@override
|
|
||||||
Future<void> remove(int index) async {
|
|
||||||
if (_playlist == null ||
|
|
||||||
index < 0 ||
|
|
||||||
index > _playlist!.medias.length - 1 ||
|
|
||||||
_playlist!.index == index) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final targetItem = _playlist!.medias.elementAtOrNull(index);
|
|
||||||
if (targetItem == null) return;
|
|
||||||
|
|
||||||
if (shuffled && _tempMedias != null) {
|
|
||||||
_tempMedias!.remove(targetItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
final newMedias = _playlist!.medias.toList()..removeAt(index);
|
|
||||||
|
|
||||||
playlist = _playlist!.copyWith(
|
|
||||||
medias: newMedias,
|
|
||||||
index: newMedias.indexOf(_playlist!.medias[_playlist!.index]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
NativePlayer get nativePlayer => platform as NativePlayer;
|
|
||||||
|
|
||||||
Future<void> setAudioNormalization(bool normalize) async {
|
|
||||||
if (normalize) {
|
|
||||||
await nativePlayer.setProperty('af', 'dynaudnorm=g=5:f=250:r=0.9:p=0.5');
|
|
||||||
} else {
|
|
||||||
await nativePlayer.setProperty('af', '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user