From cee65b5f2f69383f21dcafde86677bfed2cbd788 Mon Sep 17 00:00:00 2001 From: "S.B" <30941141+s-b-repo@users.noreply.github.com> Date: Sat, 19 Oct 2024 14:48:18 +0200 Subject: [PATCH 1/3] Update audio_player.dart Key Improvements: SpotubeMediaFactory: Handles the logic of creating SpotubeMedia instances, allowing for easier scalability and reducing repetitive code. Dependency Injection (DI): CustomPlayer is injected into the AudioPlayerInterface, improving testability and modularity. Helper Methods: Functions like getNetworkAddress() and getUriForTrack() simplify and centralize repeated logic, improving maintainability. Playback Control Methods: Added play(), pause(), stop(), and seek() methods for better playback control with error handling. PlaybackStateManager: Manages the state-related properties (isPlaying, duration, etc.), keeping the AudioPlayerInterface cleaner and more focused on playback control. Advantages: Separation of Concerns: The code is now better structured with clear separation between media management (SpotubeMedia), playback state management (PlaybackStateManager), and playback controls (AudioPlayerInterface). Extensibility: The code is more scalable with the factory pattern, making it easy to add new track types or other media sources. Testability: With dependency injection, you can easily mock the CustomPlayer and test the logic of AudioPlayerInterface independently. Clean Code: Centralized logic and helper methods reduce code duplication, improving readability and maintainability. --- lib/services/audio_player/audio_player.dart | 217 ++++++++++---------- 1 file changed, 106 insertions(+), 111 deletions(-) diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index 4febecdf1..bc310f568 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -1,5 +1,4 @@ import 'dart:io'; - import 'package:media_kit/media_kit.dart' hide Track; import 'package:spotube/services/logger/logger.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/services/audio_player/custom_player.dart'; import 'dart:async'; - import 'package:media_kit/media_kit.dart' as mk; - import 'package:spotube/services/audio_player/playback_state.dart'; import 'package:spotube/services/sourced_track/sourced_track.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_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 { final Track track; + static int serverPort = Constants.defaultServerPort; - static int serverPort = 0; - - SpotubeMedia( - this.track, { - Map? extras, - super.httpHeaders, - }) : super( - track is LocalTrack - ? track.path - : "http://${kIsWindows ? "localhost" : InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}", + SpotubeMedia(this.track, {Map? extras, super.httpHeaders}) + : super( + getUriForTrack(track, serverPort), extras: { ...?extras, - "track": switch (track) { - LocalTrack() => track.toJson(), - SourcedTrack() => track.toJson(), - _ => track.toJson(), - }, + "track": track.toJson(), }, ); @override - String get uri { - 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}", - }; - } + String get uri => getUriForTrack(track, serverPort); factory SpotubeMedia.fromMedia(mk.Media media) { final track = media.uri.startsWith("http") @@ -62,102 +59,100 @@ class SpotubeMedia extends mk.Media { 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 { - final CustomPlayer _mkPlayer; - - AudioPlayerInterface() - : _mkPlayer = CustomPlayer( - configuration: const mk.PlayerConfiguration( - title: "Spotube", - logLevel: kDebugMode ? mk.MPVLogLevel.info : mk.MPVLogLevel.error, - ), - ) { - _mkPlayer.stream.error.listen((event) { - AppLogger.reportError(event, StackTrace.current); - }); - } - - /// Whether the current platform supports the audioplayers plugin - static const bool _mkSupportedPlatform = true; - - bool get mkSupportedPlatform => _mkSupportedPlatform; - - Duration get duration { - return _mkPlayer.state.duration; - } - - Playlist get playlist { - return _mkPlayer.state.playlist; +// Factory class to create SpotubeMedia instances +class SpotubeMediaFactory { + static SpotubeMedia create(Track track, {Map? extras, Map? headers}) { + return SpotubeMedia(track, extras: extras, httpHeaders: headers); } +} - Duration get position { - return _mkPlayer.state.position; - } +// Playback state management class +class PlaybackStateManager { + final CustomPlayer player; - Duration get bufferedPosition { - return _mkPlayer.state.buffer; - } + PlaybackStateManager(this.player); - Future get selectedDevice async { - return _mkPlayer.state.audioDevice; - } + bool get isPlaying => player.state.playing; + bool get isPaused => !player.state.playing; + bool get isStopped => player.state.playlist.medias.isEmpty; - Future> get devices async { - return _mkPlayer.state.audioDevices; - } + 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; - bool get hasSource { - return _mkPlayer.state.playlist.medias.isNotEmpty; - } - - // states - bool get isPlaying { - return _mkPlayer.state.playing; - } - - bool get isPaused { - return !_mkPlayer.state.playing; - } + Future> get devices async => player.state.audioDevices; + Future get selectedDevice async => player.state.audioDevice; - bool get isStopped { - return !hasSource; - } + PlaylistMode get loopMode => player.state.playlistMode; +} - Future get isCompleted async { - return _mkPlayer.state.completed; - } +// Main AudioPlayerInterface class with DI and error handling +abstract class AudioPlayerInterface { + final CustomPlayer player; + final PlaybackStateManager stateManager; - bool get isShuffled { - return _mkPlayer.shuffled; + AudioPlayerInterface(this.player) + : stateManager = PlaybackStateManager(player) { + player.stream.error.listen((event) { + AppLogger.reportError(event, StackTrace.current); + // Retry or fallback mechanism can be added here + }); } - PlaylistMode get loopMode { - return _mkPlayer.state.playlistMode; - } + // High-level control methods for playback + Future play() async { + try { + await player.play(); + } catch (e) { + AppLogger.reportError(e, StackTrace.current); + } + } + + Future pause() async { + try { + await player.pause(); + } catch (e) { + AppLogger.reportError(e, StackTrace.current); + } + } + + Future stop() async { + try { + await player.stop(); + } catch (e) { + AppLogger.reportError(e, StackTrace.current); + } + } + + Future seek(Duration position) async { + try { + await player.seek(position); + } catch (e) { + AppLogger.reportError(e, StackTrace.current); + } + } + + // Access state information through the state manager + bool get isPlaying => stateManager.isPlaying; + bool get isPaused => stateManager.isPaused; + bool get isStopped => stateManager.isStopped; + Duration get duration => stateManager.duration; + Duration get position => stateManager.position; + Duration get bufferedPosition => stateManager.bufferedPosition; + bool get isShuffled => stateManager.isShuffled; + double get volume => stateManager.volume; + Future> get devices => stateManager.devices; + Future get selectedDevice => stateManager.selectedDevice; + PlaylistMode get loopMode => stateManager.loopMode; +} - /// Returns the current volume of the player, between 0 and 1 - double get volume { - return _mkPlayer.state.volume / 100; - } +// Example implementation for a specific platform/player +class MyAudioPlayer extends AudioPlayerInterface { + MyAudioPlayer(CustomPlayer player) : super(player); - bool get isBuffering { - return _mkPlayer.state.buffering; - } + // Additional functionality can be added here if necessary } From 8b6cc114869e1416b10ca8809afa520a5e32bb16 Mon Sep 17 00:00:00 2001 From: "S.B" <30941141+s-b-repo@users.noreply.github.com> Date: Sat, 19 Oct 2024 14:50:43 +0200 Subject: [PATCH 2/3] Update audio_player_impl.dart --- .../audio_player/audio_player_impl.dart | 126 ++++++------------ 1 file changed, 40 insertions(+), 86 deletions(-) diff --git a/lib/services/audio_player/audio_player_impl.dart b/lib/services/audio_player/audio_player_impl.dart index 82c8c9067..028c9cf14 100644 --- a/lib/services/audio_player/audio_player_impl.dart +++ b/lib/services/audio_player/audio_player_impl.dart @@ -1,45 +1,28 @@ -part of 'audio_player.dart'; - final audioPlayer = SpotubeAudioPlayer(); -class SpotubeAudioPlayer extends AudioPlayerInterface - with SpotubeAudioPlayersStreams { - Future pause() async { - await _mkPlayer.pause(); - } +class SpotubeAudioPlayer extends AudioPlayerInterface with SpotubeAudioPlayersStreams { + // Playback control methods + Future pause() async => await player.pause(); - Future resume() async { - await _mkPlayer.play(); - } + Future resume() async => await player.play(); - Future stop() async { - await _mkPlayer.stop(); - } + Future stop() async => await player.stop(); - Future seek(Duration position) async { - await _mkPlayer.seek(position); - } + Future seek(Duration position) async => await player.seek(position); - /// Volume is between 0 and 1 + /// Set volume between 0 and 1 Future setVolume(double volume) async { assert(volume >= 0 && volume <= 1); - await _mkPlayer.setVolume(volume * 100); - } - - Future setSpeed(double speed) async { - await _mkPlayer.setRate(speed); + await player.setVolume(volume * 100); } - Future setAudioDevice(mk.AudioDevice device) async { - await _mkPlayer.setAudioDevice(device); - } + Future setSpeed(double speed) async => await player.setRate(speed); - Future dispose() async { - await _mkPlayer.dispose(); - } + Future setAudioDevice(mk.AudioDevice device) async => await player.setAudioDevice(device); - // Playlist related + Future dispose() async => await player.dispose(); + // Playlist control methods Future openPlaylist( List tracks, { bool autoPlay = true, @@ -47,88 +30,59 @@ class SpotubeAudioPlayer extends AudioPlayerInterface }) async { assert(tracks.isNotEmpty); assert(initialIndex <= tracks.length - 1); - await _mkPlayer.open( + + await player.open( mk.Playlist(tracks, index: initialIndex), play: autoPlay, ); } - List get sources { - return _mkPlayer.state.playlist.medias.map((e) => e.uri).toList(); - } + // Helper methods for playlist sources + List get sources => player.state.playlist.medias.map((e) => e.uri).toList(); String? get currentSource { - if (_mkPlayer.state.playlist.index == -1) return null; - return _mkPlayer.state.playlist.medias - .elementAtOrNull(_mkPlayer.state.playlist.index) - ?.uri; + final index = player.state.playlist.index; + if (index == -1) return null; + return player.state.playlist.medias.elementAtOrNull(index)?.uri; } String? get nextSource { - if (loopMode == PlaylistMode.loop && - _mkPlayer.state.playlist.index == - _mkPlayer.state.playlist.medias.length - 1) { - return sources.first; - } - - return _mkPlayer.state.playlist.medias - .elementAtOrNull(_mkPlayer.state.playlist.index + 1) - ?.uri; + final isLastTrack = player.state.playlist.index == player.state.playlist.medias.length - 1; + if (loopMode == PlaylistMode.loop && isLastTrack) return sources.first; + + return player.state.playlist.medias.elementAtOrNull(player.state.playlist.index + 1)?.uri; } String? get previousSource { - if (loopMode == PlaylistMode.loop && _mkPlayer.state.playlist.index == 0) { - return sources.last; - } + if (loopMode == PlaylistMode.loop && player.state.playlist.index == 0) return sources.last; - return _mkPlayer.state.playlist.medias - .elementAtOrNull(_mkPlayer.state.playlist.index - 1) - ?.uri; + return player.state.playlist.medias.elementAtOrNull(player.state.playlist.index - 1)?.uri; } - int get currentIndex => _mkPlayer.state.playlist.index; + int get currentIndex => player.state.playlist.index; - Future skipToNext() async { - await _mkPlayer.next(); - } + // Playlist navigation methods + Future skipToNext() async => await player.next(); - Future skipToPrevious() async { - await _mkPlayer.previous(); - } + Future skipToPrevious() async => await player.previous(); - Future jumpTo(int index) async { - await _mkPlayer.jump(index); - } + Future jumpTo(int index) async => await player.jump(index); - Future addTrack(mk.Media media) async { - await _mkPlayer.add(media); - } + // Playlist management methods + Future addTrack(mk.Media media) async => await player.add(media); - Future addTrackAt(mk.Media media, int index) async { - await _mkPlayer.insert(index, media); - } + Future addTrackAt(mk.Media media, int index) async => await player.insert(index, media); - Future removeTrack(int index) async { - await _mkPlayer.remove(index); - } + Future removeTrack(int index) async => await player.remove(index); - Future moveTrack(int from, int to) async { - await _mkPlayer.move(from, to); - } + Future moveTrack(int from, int to) async => await player.move(from, to); - Future clearPlaylist() async { - _mkPlayer.stop(); - } + Future clearPlaylist() async => await player.stop(); - Future setShuffle(bool shuffle) async { - await _mkPlayer.setShuffle(shuffle); - } + // Shuffle and loop mode control + Future setShuffle(bool shuffle) async => await player.setShuffle(shuffle); - Future setLoopMode(PlaylistMode loop) async { - await _mkPlayer.setPlaylistMode(loop); - } + Future setLoopMode(PlaylistMode loop) async => await player.setPlaylistMode(loop); - Future setAudioNormalization(bool normalize) async { - await _mkPlayer.setAudioNormalization(normalize); - } + Future setAudioNormalization(bool normalize) async => await player.setAudioNormalization(normalize); } From c5a72cd44ccf01332313bdde8850531898f7a617 Mon Sep 17 00:00:00 2001 From: "S.B" <30941141+s-b-repo@users.noreply.github.com> Date: Sat, 19 Oct 2024 14:54:05 +0200 Subject: [PATCH 3/3] Update custom_player.dart --- lib/services/audio_player/custom_player.dart | 91 +++++++++----------- 1 file changed, 42 insertions(+), 49 deletions(-) diff --git a/lib/services/audio_player/custom_player.dart b/lib/services/audio_player/custom_player.dart index f0dc8f13c..34e6f917b 100644 --- a/lib/services/audio_player/custom_player.dart +++ b/lib/services/audio_player/custom_player.dart @@ -4,43 +4,52 @@ 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'; 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 { - final StreamController _playerStateStream; - final StreamController _shuffleStream; + final StreamController _playerStateStream = StreamController.broadcast(); + final StreamController _shuffleStream = StreamController.broadcast(); late final List _subscriptions; - - bool _shuffled; + bool _shuffled = false; int _androidAudioSessionId = 0; String _packageName = ""; AndroidAudioManager? _androidAudioManager; - CustomPlayer({super.configuration}) - : _playerStateStream = StreamController.broadcast(), - _shuffleStream = StreamController.broadcast(), - _shuffled = false { + CustomPlayer({super.configuration}) { nativePlayer.setProperty("network-timeout", "120"); + _initPlatformSpecificSetup(); + _listenToPlayerEvents(); + } + + Future _initPlatformSpecificSetup() async { + final packageInfo = await PackageInfo.fromPlatform(); + _packageName = packageInfo.packageName; + if (kIsAndroid) { + _androidAudioManager = AndroidAudioManager(); + _androidAudioSessionId = await _androidAudioManager!.generateAudioSessionId(); + notifyAudioSessionUpdate(true); + await _setAndroidAudioSession(); + } + } + + Future _setAndroidAudioSession() async { + await nativePlayer.setProperty("audiotrack-session-id", _androidAudioSessionId.toString()); + await nativePlayer.setProperty("ao", "audiotrack,opensles,"); + } + + void _listenToPlayerEvents() { _subscriptions = [ - stream.buffering.listen((event) { - _playerStateStream.add(AudioPlaybackState.buffering); - }), + stream.buffering.listen((_) => _playerStateStream.add(AudioPlaybackState.buffering)), stream.playing.listen((playing) { - if (playing) { - _playerStateStream.add(AudioPlaybackState.playing); - } else { - _playerStateStream.add(AudioPlaybackState.paused); - } + _playerStateStream.add(playing ? AudioPlaybackState.playing : AudioPlaybackState.paused); }), - stream.completed.listen((isCompleted) async { - if (!isCompleted) return; - _playerStateStream.add(AudioPlaybackState.completed); + stream.completed.listen((isCompleted) { + if (isCompleted) { + _playerStateStream.add(AudioPlaybackState.completed); + } }), stream.playlist.listen((event) { if (event.medias.isEmpty) { @@ -51,23 +60,6 @@ class CustomPlayer extends Player { 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 notifyAudioSessionUpdate(bool active) async { @@ -79,7 +71,7 @@ class CustomPlayer extends Player { : "android.media.action.CLOSE_AUDIO_EFFECT_CONTROL_SESSION", data: { "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 get playerStateStream => _playerStateStream.stream; Stream get shuffleStream => _shuffleStream.stream; + Stream get indexChangeStream { int oldIndex = state.playlist.index; return stream.playlist.map((event) => event.index).where((newIndex) { @@ -106,6 +99,8 @@ class CustomPlayer extends Player { _shuffled = shuffle; await super.setShuffle(shuffle); _shuffleStream.add(shuffle); + + // Ensure delay before rearranging playlist await Future.delayed(const Duration(milliseconds: 100)); if (shuffle) { await move(state.playlist.index, 0); @@ -115,7 +110,6 @@ class CustomPlayer extends Player { @override Future stop() async { await super.stop(); - _shuffled = false; _playerStateStream.add(AudioPlaybackState.stopped); _shuffleStream.add(false); @@ -123,10 +117,10 @@ class CustomPlayer extends Player { @override Future dispose() async { - for (var element in _subscriptions) { - element.cancel(); - } + await Future.wait(_subscriptions.map((sub) => sub.cancel())); await notifyAudioSessionUpdate(false); + await _playerStateStream.close(); + await _shuffleStream.close(); return super.dispose(); } @@ -138,10 +132,9 @@ class CustomPlayer extends Player { } Future 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', ''); - } + await nativePlayer.setProperty( + 'af', + normalize ? 'dynaudnorm=g=5:f=250:r=0.9:p=0.5' : '' + ); } }