This commit is contained in:
S.B 2025-05-04 03:42:15 +00:00 committed by GitHub
commit 71bcc2228e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 187 additions and 245 deletions

View File

@ -1,5 +1,4 @@
import 'dart:io'; import 'dart:io';
import 'package:media_kit/media_kit.dart' hide Track; import 'package:media_kit/media_kit.dart' hide Track;
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -7,9 +6,7 @@ import 'package:spotify/spotify.dart' hide Playlist;
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
import 'package:spotube/services/audio_player/custom_player.dart'; import 'package:spotube/services/audio_player/custom_player.dart';
import 'dart:async'; import 'dart:async';
import 'package:media_kit/media_kit.dart' as mk; import 'package:media_kit/media_kit.dart' as mk;
import 'package:spotube/services/audio_player/playback_state.dart'; import 'package:spotube/services/audio_player/playback_state.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
@ -17,40 +14,40 @@ import 'package:spotube/utils/platform.dart';
part 'audio_players_streams_mixin.dart'; part 'audio_players_streams_mixin.dart';
part 'audio_player_impl.dart'; part 'audio_player_impl.dart';
// Constants class for shared constants like port and addresses
class Constants {
static const defaultServerPort = 8080;
static const defaultLocalHost = "localhost";
}
// Helper to get network address based on the platform
String getNetworkAddress() {
return kIsWindows ? Constants.defaultLocalHost : InternetAddress.anyIPv4.address;
}
// Helper to get URI for a given track
String getUriForTrack(Track track, int serverPort) {
return track is LocalTrack
? track.path
: "http://${getNetworkAddress()}:$serverPort/stream/${track.id}";
}
// SpotubeMedia class handling media creation logic
class SpotubeMedia extends mk.Media { class SpotubeMedia extends mk.Media {
final Track track; final Track track;
static int serverPort = Constants.defaultServerPort;
static int serverPort = 0; SpotubeMedia(this.track, {Map<String, dynamic>? extras, super.httpHeaders})
: super(
SpotubeMedia( getUriForTrack(track, serverPort),
this.track, {
Map<String, dynamic>? extras,
super.httpHeaders,
}) : super(
track is LocalTrack
? track.path
: "http://${kIsWindows ? "localhost" : InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}",
extras: { extras: {
...?extras, ...?extras,
"track": switch (track) { "track": track.toJson(),
LocalTrack() => track.toJson(),
SourcedTrack() => track.toJson(),
_ => track.toJson(),
},
}, },
); );
@override @override
String get uri { String get uri => getUriForTrack(track, serverPort);
return switch (track) {
/// [super.uri] must be used instead of [track.path] to prevent wrong
/// path format exceptions in Windows causing [extras] to be null
LocalTrack() => super.uri,
_ =>
"http://${kIsWindows ? "localhost" : InternetAddress.anyIPv4.address}:"
"$serverPort/stream/${track.id}",
};
}
factory SpotubeMedia.fromMedia(mk.Media media) { factory SpotubeMedia.fromMedia(mk.Media media) {
final track = media.uri.startsWith("http") final track = media.uri.startsWith("http")
@ -62,102 +59,100 @@ class SpotubeMedia extends mk.Media {
httpHeaders: media.httpHeaders, httpHeaders: media.httpHeaders,
); );
} }
// @override
// operator ==(Object other) {
// if (other is! SpotubeMedia) return false;
// final isLocal = track is LocalTrack && other.track is LocalTrack;
// return isLocal
// ? (other.track as LocalTrack).path == (track as LocalTrack).path
// : other.track.id == track.id;
// }
// @override
// int get hashCode => track is LocalTrack
// ? (track as LocalTrack).path.hashCode
// : track.id.hashCode;
} }
abstract class AudioPlayerInterface { // Factory class to create SpotubeMedia instances
final CustomPlayer _mkPlayer; class SpotubeMediaFactory {
static SpotubeMedia create(Track track, {Map<String, dynamic>? extras, Map<String, String>? headers}) {
return SpotubeMedia(track, extras: extras, httpHeaders: headers);
}
}
AudioPlayerInterface() // Playback state management class
: _mkPlayer = CustomPlayer( class PlaybackStateManager {
configuration: const mk.PlayerConfiguration( final CustomPlayer player;
title: "Spotube",
logLevel: kDebugMode ? mk.MPVLogLevel.info : mk.MPVLogLevel.error, PlaybackStateManager(this.player);
),
) { bool get isPlaying => player.state.playing;
_mkPlayer.stream.error.listen((event) { bool get isPaused => !player.state.playing;
bool get isStopped => player.state.playlist.medias.isEmpty;
Duration get duration => player.state.duration;
Duration get position => player.state.position;
Duration get bufferedPosition => player.state.buffer;
bool get isShuffled => player.shuffled;
double get volume => player.state.volume / 100;
Future<List<mk.AudioDevice>> get devices async => player.state.audioDevices;
Future<mk.AudioDevice> get selectedDevice async => player.state.audioDevice;
PlaylistMode get loopMode => player.state.playlistMode;
}
// Main AudioPlayerInterface class with DI and error handling
abstract class AudioPlayerInterface {
final CustomPlayer player;
final PlaybackStateManager stateManager;
AudioPlayerInterface(this.player)
: stateManager = PlaybackStateManager(player) {
player.stream.error.listen((event) {
AppLogger.reportError(event, StackTrace.current); AppLogger.reportError(event, StackTrace.current);
// Retry or fallback mechanism can be added here
}); });
} }
/// Whether the current platform supports the audioplayers plugin // High-level control methods for playback
static const bool _mkSupportedPlatform = true; Future<void> play() async {
try {
bool get mkSupportedPlatform => _mkSupportedPlatform; await player.play();
} catch (e) {
Duration get duration { AppLogger.reportError(e, StackTrace.current);
return _mkPlayer.state.duration; }
} }
Playlist get playlist { Future<void> pause() async {
return _mkPlayer.state.playlist; try {
await player.pause();
} catch (e) {
AppLogger.reportError(e, StackTrace.current);
}
} }
Duration get position { Future<void> stop() async {
return _mkPlayer.state.position; try {
await player.stop();
} catch (e) {
AppLogger.reportError(e, StackTrace.current);
}
} }
Duration get bufferedPosition { Future<void> seek(Duration position) async {
return _mkPlayer.state.buffer; try {
await player.seek(position);
} catch (e) {
AppLogger.reportError(e, StackTrace.current);
}
} }
Future<mk.AudioDevice> get selectedDevice async { // Access state information through the state manager
return _mkPlayer.state.audioDevice; bool get isPlaying => stateManager.isPlaying;
} bool get isPaused => stateManager.isPaused;
bool get isStopped => stateManager.isStopped;
Future<List<mk.AudioDevice>> get devices async { Duration get duration => stateManager.duration;
return _mkPlayer.state.audioDevices; Duration get position => stateManager.position;
} Duration get bufferedPosition => stateManager.bufferedPosition;
bool get isShuffled => stateManager.isShuffled;
bool get hasSource { double get volume => stateManager.volume;
return _mkPlayer.state.playlist.medias.isNotEmpty; Future<List<mk.AudioDevice>> get devices => stateManager.devices;
} Future<mk.AudioDevice> get selectedDevice => stateManager.selectedDevice;
PlaylistMode get loopMode => stateManager.loopMode;
// states }
bool get isPlaying {
return _mkPlayer.state.playing; // Example implementation for a specific platform/player
} class MyAudioPlayer extends AudioPlayerInterface {
MyAudioPlayer(CustomPlayer player) : super(player);
bool get isPaused {
return !_mkPlayer.state.playing; // Additional functionality can be added here if necessary
}
bool get isStopped {
return !hasSource;
}
Future<bool> get isCompleted async {
return _mkPlayer.state.completed;
}
bool get isShuffled {
return _mkPlayer.shuffled;
}
PlaylistMode get loopMode {
return _mkPlayer.state.playlistMode;
}
/// Returns the current volume of the player, between 0 and 1
double get volume {
return _mkPlayer.state.volume / 100;
}
bool get isBuffering {
return _mkPlayer.state.buffering;
}
} }

View File

@ -1,45 +1,28 @@
part of 'audio_player.dart';
final audioPlayer = SpotubeAudioPlayer(); final audioPlayer = SpotubeAudioPlayer();
class SpotubeAudioPlayer extends AudioPlayerInterface class SpotubeAudioPlayer extends AudioPlayerInterface with SpotubeAudioPlayersStreams {
with SpotubeAudioPlayersStreams { // Playback control methods
Future<void> pause() async { Future<void> pause() async => await player.pause();
await _mkPlayer.pause();
}
Future<void> resume() async { Future<void> resume() async => await player.play();
await _mkPlayer.play();
}
Future<void> stop() async { Future<void> stop() async => await player.stop();
await _mkPlayer.stop();
}
Future<void> seek(Duration position) async { Future<void> seek(Duration position) async => await player.seek(position);
await _mkPlayer.seek(position);
}
/// Volume is between 0 and 1 /// Set volume between 0 and 1
Future<void> setVolume(double volume) async { Future<void> setVolume(double volume) async {
assert(volume >= 0 && volume <= 1); assert(volume >= 0 && volume <= 1);
await _mkPlayer.setVolume(volume * 100); await player.setVolume(volume * 100);
} }
Future<void> setSpeed(double speed) async { Future<void> setSpeed(double speed) async => await player.setRate(speed);
await _mkPlayer.setRate(speed);
}
Future<void> setAudioDevice(mk.AudioDevice device) async { Future<void> setAudioDevice(mk.AudioDevice device) async => await player.setAudioDevice(device);
await _mkPlayer.setAudioDevice(device);
}
Future<void> dispose() async { Future<void> dispose() async => await player.dispose();
await _mkPlayer.dispose();
}
// Playlist related
// Playlist control methods
Future<void> openPlaylist( Future<void> openPlaylist(
List<mk.Media> tracks, { List<mk.Media> tracks, {
bool autoPlay = true, bool autoPlay = true,
@ -47,88 +30,59 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
}) async { }) async {
assert(tracks.isNotEmpty); assert(tracks.isNotEmpty);
assert(initialIndex <= tracks.length - 1); assert(initialIndex <= tracks.length - 1);
await _mkPlayer.open(
await player.open(
mk.Playlist(tracks, index: initialIndex), mk.Playlist(tracks, index: initialIndex),
play: autoPlay, play: autoPlay,
); );
} }
List<String> get sources { // Helper methods for playlist sources
return _mkPlayer.state.playlist.medias.map((e) => e.uri).toList(); List<String> get sources => player.state.playlist.medias.map((e) => e.uri).toList();
}
String? get currentSource { String? get currentSource {
if (_mkPlayer.state.playlist.index == -1) return null; final index = player.state.playlist.index;
return _mkPlayer.state.playlist.medias if (index == -1) return null;
.elementAtOrNull(_mkPlayer.state.playlist.index) return player.state.playlist.medias.elementAtOrNull(index)?.uri;
?.uri;
} }
String? get nextSource { String? get nextSource {
if (loopMode == PlaylistMode.loop && final isLastTrack = player.state.playlist.index == player.state.playlist.medias.length - 1;
_mkPlayer.state.playlist.index == if (loopMode == PlaylistMode.loop && isLastTrack) return sources.first;
_mkPlayer.state.playlist.medias.length - 1) {
return sources.first;
}
return _mkPlayer.state.playlist.medias return player.state.playlist.medias.elementAtOrNull(player.state.playlist.index + 1)?.uri;
.elementAtOrNull(_mkPlayer.state.playlist.index + 1)
?.uri;
} }
String? get previousSource { String? get previousSource {
if (loopMode == PlaylistMode.loop && _mkPlayer.state.playlist.index == 0) { if (loopMode == PlaylistMode.loop && player.state.playlist.index == 0) return sources.last;
return sources.last;
}
return _mkPlayer.state.playlist.medias return player.state.playlist.medias.elementAtOrNull(player.state.playlist.index - 1)?.uri;
.elementAtOrNull(_mkPlayer.state.playlist.index - 1)
?.uri;
} }
int get currentIndex => _mkPlayer.state.playlist.index; int get currentIndex => player.state.playlist.index;
Future<void> skipToNext() async { // Playlist navigation methods
await _mkPlayer.next(); Future<void> skipToNext() async => await player.next();
}
Future<void> skipToPrevious() async { Future<void> skipToPrevious() async => await player.previous();
await _mkPlayer.previous();
}
Future<void> jumpTo(int index) async { Future<void> jumpTo(int index) async => await player.jump(index);
await _mkPlayer.jump(index);
}
Future<void> addTrack(mk.Media media) async { // Playlist management methods
await _mkPlayer.add(media); Future<void> addTrack(mk.Media media) async => await player.add(media);
}
Future<void> addTrackAt(mk.Media media, int index) async { Future<void> addTrackAt(mk.Media media, int index) async => await player.insert(index, media);
await _mkPlayer.insert(index, media);
}
Future<void> removeTrack(int index) async { Future<void> removeTrack(int index) async => await player.remove(index);
await _mkPlayer.remove(index);
}
Future<void> moveTrack(int from, int to) async { Future<void> moveTrack(int from, int to) async => await player.move(from, to);
await _mkPlayer.move(from, to);
}
Future<void> clearPlaylist() async { Future<void> clearPlaylist() async => await player.stop();
_mkPlayer.stop();
}
Future<void> setShuffle(bool shuffle) async { // Shuffle and loop mode control
await _mkPlayer.setShuffle(shuffle); Future<void> setShuffle(bool shuffle) async => await player.setShuffle(shuffle);
}
Future<void> setLoopMode(PlaylistMode loop) async { Future<void> setLoopMode(PlaylistMode loop) async => await player.setPlaylistMode(loop);
await _mkPlayer.setPlaylistMode(loop);
}
Future<void> setAudioNormalization(bool normalize) async { Future<void> setAudioNormalization(bool normalize) async => await player.setAudioNormalization(normalize);
await _mkPlayer.setAudioNormalization(normalize);
}
} }

View File

@ -4,43 +4,52 @@ import 'package:media_kit/media_kit.dart';
import 'package:flutter_broadcasts/flutter_broadcasts.dart'; import 'package:flutter_broadcasts/flutter_broadcasts.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:audio_session/audio_session.dart'; import 'package:audio_session/audio_session.dart';
// ignore: implementation_imports
import 'package:spotube/services/audio_player/playback_state.dart'; import 'package:spotube/services/audio_player/playback_state.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.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 { class CustomPlayer extends Player {
final StreamController<AudioPlaybackState> _playerStateStream; final StreamController<AudioPlaybackState> _playerStateStream = StreamController.broadcast();
final StreamController<bool> _shuffleStream; final StreamController<bool> _shuffleStream = StreamController.broadcast();
late final List<StreamSubscription> _subscriptions; late final List<StreamSubscription> _subscriptions;
bool _shuffled = false;
bool _shuffled;
int _androidAudioSessionId = 0; int _androidAudioSessionId = 0;
String _packageName = ""; String _packageName = "";
AndroidAudioManager? _androidAudioManager; AndroidAudioManager? _androidAudioManager;
CustomPlayer({super.configuration}) CustomPlayer({super.configuration}) {
: _playerStateStream = StreamController.broadcast(),
_shuffleStream = StreamController.broadcast(),
_shuffled = false {
nativePlayer.setProperty("network-timeout", "120"); nativePlayer.setProperty("network-timeout", "120");
_initPlatformSpecificSetup();
_listenToPlayerEvents();
}
Future<void> _initPlatformSpecificSetup() async {
final packageInfo = await PackageInfo.fromPlatform();
_packageName = packageInfo.packageName;
if (kIsAndroid) {
_androidAudioManager = AndroidAudioManager();
_androidAudioSessionId = await _androidAudioManager!.generateAudioSessionId();
notifyAudioSessionUpdate(true);
await _setAndroidAudioSession();
}
}
Future<void> _setAndroidAudioSession() async {
await nativePlayer.setProperty("audiotrack-session-id", _androidAudioSessionId.toString());
await nativePlayer.setProperty("ao", "audiotrack,opensles,");
}
void _listenToPlayerEvents() {
_subscriptions = [ _subscriptions = [
stream.buffering.listen((event) { stream.buffering.listen((_) => _playerStateStream.add(AudioPlaybackState.buffering)),
_playerStateStream.add(AudioPlaybackState.buffering);
}),
stream.playing.listen((playing) { stream.playing.listen((playing) {
if (playing) { _playerStateStream.add(playing ? AudioPlaybackState.playing : AudioPlaybackState.paused);
_playerStateStream.add(AudioPlaybackState.playing);
} else {
_playerStateStream.add(AudioPlaybackState.paused);
}
}), }),
stream.completed.listen((isCompleted) async { stream.completed.listen((isCompleted) {
if (!isCompleted) return; if (isCompleted) {
_playerStateStream.add(AudioPlaybackState.completed); _playerStateStream.add(AudioPlaybackState.completed);
}
}), }),
stream.playlist.listen((event) { stream.playlist.listen((event) {
if (event.medias.isEmpty) { if (event.medias.isEmpty) {
@ -51,23 +60,6 @@ class CustomPlayer extends Player {
AppLogger.reportError('[MediaKitError] \n$event', StackTrace.current); AppLogger.reportError('[MediaKitError] \n$event', StackTrace.current);
}), }),
]; ];
PackageInfo.fromPlatform().then((packageInfo) {
_packageName = packageInfo.packageName;
});
if (kIsAndroid) {
_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 { Future<void> notifyAudioSessionUpdate(bool active) async {
@ -79,7 +71,7 @@ class CustomPlayer extends Player {
: "android.media.action.CLOSE_AUDIO_EFFECT_CONTROL_SESSION", : "android.media.action.CLOSE_AUDIO_EFFECT_CONTROL_SESSION",
data: { data: {
"android.media.extra.AUDIO_SESSION": _androidAudioSessionId, "android.media.extra.AUDIO_SESSION": _androidAudioSessionId,
"android.media.extra.PACKAGE_NAME": _packageName "android.media.extra.PACKAGE_NAME": _packageName,
}, },
), ),
); );
@ -90,6 +82,7 @@ class CustomPlayer extends Player {
Stream<AudioPlaybackState> get playerStateStream => _playerStateStream.stream; Stream<AudioPlaybackState> get playerStateStream => _playerStateStream.stream;
Stream<bool> get shuffleStream => _shuffleStream.stream; Stream<bool> get shuffleStream => _shuffleStream.stream;
Stream<int> get indexChangeStream { Stream<int> get indexChangeStream {
int oldIndex = state.playlist.index; int oldIndex = state.playlist.index;
return stream.playlist.map((event) => event.index).where((newIndex) { return stream.playlist.map((event) => event.index).where((newIndex) {
@ -106,6 +99,8 @@ class CustomPlayer extends Player {
_shuffled = shuffle; _shuffled = shuffle;
await super.setShuffle(shuffle); await super.setShuffle(shuffle);
_shuffleStream.add(shuffle); _shuffleStream.add(shuffle);
// Ensure delay before rearranging playlist
await Future.delayed(const Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
if (shuffle) { if (shuffle) {
await move(state.playlist.index, 0); await move(state.playlist.index, 0);
@ -115,7 +110,6 @@ class CustomPlayer extends Player {
@override @override
Future<void> stop() async { Future<void> stop() async {
await super.stop(); await super.stop();
_shuffled = false; _shuffled = false;
_playerStateStream.add(AudioPlaybackState.stopped); _playerStateStream.add(AudioPlaybackState.stopped);
_shuffleStream.add(false); _shuffleStream.add(false);
@ -123,10 +117,10 @@ class CustomPlayer extends Player {
@override @override
Future<void> dispose() async { Future<void> dispose() async {
for (var element in _subscriptions) { await Future.wait(_subscriptions.map((sub) => sub.cancel()));
element.cancel();
}
await notifyAudioSessionUpdate(false); await notifyAudioSessionUpdate(false);
await _playerStateStream.close();
await _shuffleStream.close();
return super.dispose(); return super.dispose();
} }
@ -138,10 +132,9 @@ class CustomPlayer extends Player {
} }
Future<void> setAudioNormalization(bool normalize) async { Future<void> setAudioNormalization(bool normalize) async {
if (normalize) { await nativePlayer.setProperty(
await nativePlayer.setProperty('af', 'dynaudnorm=g=5:f=250:r=0.9:p=0.5'); 'af',
} else { normalize ? 'dynaudnorm=g=5:f=250:r=0.9:p=0.5' : ''
await nativePlayer.setProperty('af', ''); );
}
} }
} }