From d7135db5ad8951eb12f3ae80e7f6c163f1a617ce Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 28 Apr 2023 21:11:49 +0600 Subject: [PATCH] refactor(audi_service): unify all platform audio services --- lib/provider/playlist_queue_provider.dart | 66 +- .../audio_services/audio_services.dart | 69 ++ .../audio_services/linux_audio_service.dart | 124 +++ .../mobile_audio_service.dart | 0 .../windows_audio_service.dart | 0 lib/services/linux_audio_service.dart | 738 ------------------ pubspec.lock | 17 + pubspec.yaml | 4 +- 8 files changed, 222 insertions(+), 796 deletions(-) create mode 100644 lib/services/audio_services/audio_services.dart create mode 100644 lib/services/audio_services/linux_audio_service.dart rename lib/services/{ => audio_services}/mobile_audio_service.dart (100%) rename lib/services/{ => audio_services}/windows_audio_service.dart (100%) delete mode 100644 lib/services/linux_audio_service.dart diff --git a/lib/provider/playlist_queue_provider.dart b/lib/provider/playlist_queue_provider.dart index f68933d1..934eb09e 100644 --- a/lib/provider/playlist_queue_provider.dart +++ b/lib/provider/playlist_queue_provider.dart @@ -1,4 +1,3 @@ -import 'package:audio_service/audio_service.dart'; import 'package:audioplayers/audioplayers.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -12,11 +11,8 @@ import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/services/audio_player.dart'; -import 'package:spotube/services/linux_audio_service.dart'; -import 'package:spotube/services/mobile_audio_service.dart'; -import 'package:spotube/services/windows_audio_service.dart'; +import 'package:spotube/services/audio_services/audio_services.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; -import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart' hide Playlist; import 'package:collection/collection.dart'; @@ -139,9 +135,8 @@ class PlaylistQueue { class PlaylistQueueNotifier extends PersistedStateNotifier { final Ref ref; - MobileAudioService? mobileService; - LinuxAudioService? linuxService; - WindowsAudioService? windowsService; + + AudioServices? audioServices; static final provider = StateNotifierProvider( @@ -155,32 +150,7 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { } void configure() async { - if (kIsMobile || kIsMacOS) { - mobileService = await AudioService.init( - builder: () => MobileAudioService( - this, - ref.read(VolumeProvider.provider.notifier), - ), - config: const AudioServiceConfig( - androidNotificationChannelId: 'com.krtirtho.Spotube', - androidNotificationChannelName: 'Spotube', - androidNotificationOngoing: true, - ), - ); - } - if (kIsLinux) { - linuxService = LinuxAudioService(ref, this); - } - if (kIsWindows) { - windowsService = WindowsAudioService(ref, this); - } - addListener((state) { - linuxService?.player.updateProperties(); - }); - - audioPlayer.onPlayerStateChanged.listen((event) { - linuxService?.player.updateProperties(); - }); + audioServices = await AudioServices.create(ref, this); audioPlayer.onPlayerComplete.listen((event) async { if (!isLoaded) return; @@ -196,7 +166,6 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { audioPlayer.onPositionChanged.listen((pos) async { if (!isLoaded) return; - await linuxService?.player.updateProperties(); final currentDuration = await audioPlayer.getDuration() ?? Duration.zero; // skip all the activeTrack.skipSegments @@ -368,23 +337,8 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { Future play() async { if (!isLoaded) return; await pause(); - await mobileService?.session?.setActive(true); - final mediaItem = MediaItem( - id: state!.activeTrack.id!, - title: state!.activeTrack.name!, - album: state!.activeTrack.album?.name, - artist: TypeConversionUtils.artists_X_String( - state!.activeTrack.artists ?? []), - artUri: Uri.parse( - TypeConversionUtils.image_X_UrlString( - state!.activeTrack.album?.images, - placeholder: ImagePlaceholder.online, - ), - ), - duration: state!.activeTrack.duration, - ); - mobileService?.addItem(mediaItem); - windowsService?.addTrack(state!.activeTrack); + audioServices?.activateSession(); + await audioServices?.addTrack(state!.activeTrack); if (state!.activeTrack is LocalTrack) { await audioPlayer.play( DeviceFileSource((state!.activeTrack as LocalTrack).path), @@ -409,9 +363,7 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { ); } - mobileService?.addItem(mediaItem.copyWith( - duration: (state!.activeTrack as SpotubeTrack).ytTrack.duration, - )); + audioServices?.addTrack(state!.activeTrack); final cached = await DefaultCacheManager().getFileFromCache(state!.activeTrack.id!); @@ -463,7 +415,7 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { } Future stop() async { - (mobileService)?.session?.setActive(false); + audioServices?.deactivateSession(); state = null; return audioPlayer.stop(); @@ -576,7 +528,7 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { @override void dispose() { - windowsService?.dispose(); + audioServices?.dispose(); super.dispose(); } } diff --git a/lib/services/audio_services/audio_services.dart b/lib/services/audio_services/audio_services.dart new file mode 100644 index 00000000..10ff68e3 --- /dev/null +++ b/lib/services/audio_services/audio_services.dart @@ -0,0 +1,69 @@ +import 'package:audio_service/audio_service.dart'; +import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/models/spotube_track.dart'; +import 'package:spotube/provider/playlist_queue_provider.dart'; +import 'package:spotube/services/audio_services/linux_audio_service.dart'; +import 'package:spotube/services/audio_services/mobile_audio_service.dart'; +import 'package:spotube/services/audio_services/windows_audio_service.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; + +class AudioServices { + final MobileAudioService? mobile; + final WindowsAudioService? smtc; + final LinuxAudioService? mpris; + + AudioServices(this.mobile, this.smtc, this.mpris); + + static Future create( + Ref ref, PlaylistQueueNotifier playlistQueueNotifier) async { + final mobile = + !DesktopTools.platform.isMobile && !!DesktopTools.platform.isMacOS + ? null + : MobileAudioService( + playlistQueueNotifier, + ref.read(VolumeProvider.provider.notifier), + ); + final smtc = !DesktopTools.platform.isWindows + ? null + : WindowsAudioService(ref, playlistQueueNotifier); + final mpris = !DesktopTools.platform.isLinux + ? null + : LinuxAudioService(ref, playlistQueueNotifier); + + return AudioServices(mobile, smtc, mpris); + } + + Future addTrack(Track track) async { + await smtc?.addTrack(track); + await mpris?.addTrack(track); + mobile?.addItem(MediaItem( + id: track.id!, + album: track.album?.name ?? "", + title: track.name!, + artist: TypeConversionUtils.artists_X_String(track.artists ?? []), + duration: track is SpotubeTrack + ? track.ytTrack.duration! + : Duration(milliseconds: track.durationMs ?? 0), + artUri: Uri.parse(TypeConversionUtils.image_X_UrlString( + track.album?.images ?? [], + placeholder: ImagePlaceholder.albumArt, + )), + playable: true, + )); + } + + void activateSession() { + mobile?.session?.setActive(true); + } + + void deactivateSession() { + mobile?.session?.setActive(false); + } + + void dispose() { + smtc?.dispose(); + mpris?.dispose(); + } +} diff --git a/lib/services/audio_services/linux_audio_service.dart b/lib/services/audio_services/linux_audio_service.dart new file mode 100644 index 00000000..972be0a6 --- /dev/null +++ b/lib/services/audio_services/linux_audio_service.dart @@ -0,0 +1,124 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:audioplayers/audioplayers.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:mpris_service/mpris_service.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/models/spotube_track.dart'; +import 'package:spotube/provider/playlist_queue_provider.dart'; +import 'package:spotube/services/audio_player.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; + +class LinuxAudioService { + late final MPRIS mpris; + final Ref ref; + final PlaylistQueueNotifier playlistNotifier; + + final subscriptions = []; + + LinuxAudioService(this.ref, this.playlistNotifier) { + MPRIS + .create( + busName: 'org.mpris.MediaPlayer2.spotube', + identity: 'Spotube', + desktopEntry: Platform.resolvedExecutable, + ) + .then((value) => mpris = value) + .then((_) { + mpris.playbackStatus = MPRISPlaybackStatus.stopped; + mpris.setEventHandler(MPRISEventHandler( + loopStatus: (value) async { + if (value == MPRISLoopStatus.none) { + playlistNotifier.unloop(); + } else if (value == MPRISLoopStatus.track) { + playlistNotifier.loop(); + } + }, + next: playlistNotifier.next, + pause: playlistNotifier.pause, + play: playlistNotifier.resume, + playPause: () async { + if (PlaylistQueueNotifier.isPlaying) { + await playlistNotifier.pause(); + } else { + await playlistNotifier.resume(); + } + }, + seek: playlistNotifier.seek, + shuffle: (value) async { + if (value) { + playlistNotifier.shuffle(); + } else { + playlistNotifier.unshuffle(); + } + }, + stop: playlistNotifier.stop, + volume: (value) async { + await ref.read(VolumeProvider.provider.notifier).setVolume(value); + }, + previous: playlistNotifier.previous, + )); + + final playerStateStream = + audioPlayer.onPlayerStateChanged.listen((state) async { + switch (state) { + case PlayerState.playing: + mpris.playbackStatus = MPRISPlaybackStatus.playing; + break; + case PlayerState.paused: + mpris.playbackStatus = MPRISPlaybackStatus.paused; + break; + case PlayerState.stopped: + case PlayerState.completed: + mpris.playbackStatus = MPRISPlaybackStatus.stopped; + break; + default: + break; + } + }); + + final positionStream = audioPlayer.onPositionChanged.listen((pos) async { + mpris.position = pos; + }); + + final durationStream = + audioPlayer.onDurationChanged.listen((duration) async { + mpris.metadata = mpris.metadata.copyWith(length: duration); + }); + + subscriptions.addAll([ + playerStateStream, + positionStream, + durationStream, + ]); + }); + } + + Future addTrack(Track track) async { + mpris.metadata = MPRISMetadata( + track is SpotubeTrack ? Uri.parse(track.ytUri) : Uri.parse(track.uri!), + album: track.album?.name ?? "", + albumArtist: [track.album?.artists?.first.name ?? ""], + artUrl: Uri.parse(TypeConversionUtils.image_X_UrlString( + track.album?.images ?? [], + placeholder: ImagePlaceholder.albumArt, + )), + artist: track.artists?.map((e) => e.name!).toList(), + contentCreated: DateTime.tryParse(track.album?.releaseDate ?? ""), + discNumber: track.discNumber, + length: track is SpotubeTrack + ? track.ytTrack.duration! + : Duration(milliseconds: track.durationMs!), + title: track.name!, + trackNumber: track.trackNumber, + ); + } + + void dispose() { + mpris.dispose(); + for (var element in subscriptions) { + element.cancel(); + } + } +} diff --git a/lib/services/mobile_audio_service.dart b/lib/services/audio_services/mobile_audio_service.dart similarity index 100% rename from lib/services/mobile_audio_service.dart rename to lib/services/audio_services/mobile_audio_service.dart diff --git a/lib/services/windows_audio_service.dart b/lib/services/audio_services/windows_audio_service.dart similarity index 100% rename from lib/services/windows_audio_service.dart rename to lib/services/audio_services/windows_audio_service.dart diff --git a/lib/services/linux_audio_service.dart b/lib/services/linux_audio_service.dart deleted file mode 100644 index 9b30b28e..00000000 --- a/lib/services/linux_audio_service.dart +++ /dev/null @@ -1,738 +0,0 @@ -import 'dart:io'; - -import 'package:dbus/dbus.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import 'package:spotube/provider/dbus_provider.dart'; -import 'package:spotube/models/spotube_track.dart'; -import 'package:spotube/provider/playlist_queue_provider.dart'; -import 'package:spotube/services/audio_player.dart'; -import 'package:spotube/utils/type_conversion_utils.dart'; -import 'package:window_manager/window_manager.dart'; - -class _MprisMediaPlayer2 extends DBusObject { - /// Creates a new object to expose on [path]. - _MprisMediaPlayer2() : super(DBusObjectPath('/org/mpris/MediaPlayer2')) { - dbus.registerObject(this); - } - - void dispose() { - dbus.unregisterObject(this); - } - - /// Gets value of property org.mpris.MediaPlayer2.CanQuit - Future getCanQuit() async { - return DBusMethodSuccessResponse([const DBusBoolean(true)]); - } - - /// Gets value of property org.mpris.MediaPlayer2.Fullscreen - Future getFullscreen() async { - return DBusMethodSuccessResponse([const DBusBoolean(false)]); - } - - /// Sets property org.mpris.MediaPlayer2.Fullscreen - Future setFullscreen(bool value) async { - return DBusMethodSuccessResponse(); - } - - /// Gets value of property org.mpris.MediaPlayer2.CanSetFullscreen - Future getCanSetFullscreen() async { - return DBusMethodSuccessResponse([const DBusBoolean(false)]); - } - - /// Gets value of property org.mpris.MediaPlayer2.CanRaise - Future getCanRaise() async { - return DBusMethodSuccessResponse([const DBusBoolean(false)]); - } - - /// Gets value of property org.mpris.MediaPlayer2.HasTrackList - Future getHasTrackList() async { - return DBusMethodSuccessResponse([const DBusBoolean(false)]); - } - - /// Gets value of property org.mpris.MediaPlayer2.Identity - Future getIdentity() async { - return DBusMethodSuccessResponse([const DBusString("Spotube")]); - } - - /// Gets value of property org.mpris.MediaPlayer2.DesktopEntry - Future getDesktopEntry() async { - return DBusMethodSuccessResponse([const DBusString("spotube")]); - } - - /// Gets value of property org.mpris.MediaPlayer2.SupportedUriSchemes - Future getSupportedUriSchemes() async { - return DBusMethodSuccessResponse([ - DBusArray.string(["http"]) - ]); - } - - /// Gets value of property org.mpris.MediaPlayer2.SupportedMimeTypes - Future getSupportedMimeTypes() async { - return DBusMethodSuccessResponse([ - DBusArray.string(["audio/mpeg"]) - ]); - } - - /// Implementation of org.mpris.MediaPlayer2.Raise() - Future doRaise() async { - return DBusMethodSuccessResponse(); - } - - /// Implementation of org.mpris.MediaPlayer2.Quit() - Future doQuit() async { - await windowManager.close(); - return DBusMethodSuccessResponse(); - } - - @override - List introspect() { - return [ - DBusIntrospectInterface('org.mpris.MediaPlayer2', methods: [ - DBusIntrospectMethod('Raise'), - DBusIntrospectMethod('Quit') - ], properties: [ - DBusIntrospectProperty('CanQuit', DBusSignature('b'), - access: DBusPropertyAccess.read), - DBusIntrospectProperty('Fullscreen', DBusSignature('b'), - access: DBusPropertyAccess.readwrite), - DBusIntrospectProperty('CanSetFullscreen', DBusSignature('b'), - access: DBusPropertyAccess.read), - DBusIntrospectProperty('CanRaise', DBusSignature('b'), - access: DBusPropertyAccess.read), - DBusIntrospectProperty('HasTrackList', DBusSignature('b'), - access: DBusPropertyAccess.read), - DBusIntrospectProperty('Identity', DBusSignature('s'), - access: DBusPropertyAccess.read), - DBusIntrospectProperty('DesktopEntry', DBusSignature('s'), - access: DBusPropertyAccess.read), - DBusIntrospectProperty('SupportedUriSchemes', DBusSignature('as'), - access: DBusPropertyAccess.read), - DBusIntrospectProperty('SupportedMimeTypes', DBusSignature('as'), - access: DBusPropertyAccess.read) - ]) - ]; - } - - @override - Future handleMethodCall(DBusMethodCall methodCall) async { - if (methodCall.interface == 'org.mpris.MediaPlayer2') { - if (methodCall.name == 'Raise') { - if (methodCall.values.isNotEmpty) { - return DBusMethodErrorResponse.invalidArgs(); - } - return doRaise(); - } else if (methodCall.name == 'Quit') { - if (methodCall.values.isNotEmpty) { - return DBusMethodErrorResponse.invalidArgs(); - } - return doQuit(); - } else { - return DBusMethodErrorResponse.unknownMethod(); - } - } else { - return DBusMethodErrorResponse.unknownInterface(); - } - } - - @override - Future getProperty(String interface, String name) async { - if (interface == 'org.mpris.MediaPlayer2') { - if (name == 'CanQuit') { - return getCanQuit(); - } else if (name == 'Fullscreen') { - return getFullscreen(); - } else if (name == 'CanSetFullscreen') { - return getCanSetFullscreen(); - } else if (name == 'CanRaise') { - return getCanRaise(); - } else if (name == 'HasTrackList') { - return getHasTrackList(); - } else if (name == 'Identity') { - return getIdentity(); - } else if (name == 'DesktopEntry') { - return getDesktopEntry(); - } else if (name == 'SupportedUriSchemes') { - return getSupportedUriSchemes(); - } else if (name == 'SupportedMimeTypes') { - return getSupportedMimeTypes(); - } else { - return DBusMethodErrorResponse.unknownProperty(); - } - } else { - return DBusMethodErrorResponse.unknownProperty(); - } - } - - @override - Future setProperty( - String interface, String name, DBusValue value) async { - if (interface == 'org.mpris.MediaPlayer2') { - if (name == 'CanQuit') { - return DBusMethodErrorResponse.propertyReadOnly(); - } else if (name == 'Fullscreen') { - if (value.signature != DBusSignature('b')) { - return DBusMethodErrorResponse.invalidArgs(); - } - return setFullscreen((value as DBusBoolean).value); - } else if (name == 'CanSetFullscreen') { - return DBusMethodErrorResponse.propertyReadOnly(); - } else if (name == 'CanRaise') { - return DBusMethodErrorResponse.propertyReadOnly(); - } else if (name == 'HasTrackList') { - return DBusMethodErrorResponse.propertyReadOnly(); - } else if (name == 'Identity') { - return DBusMethodErrorResponse.propertyReadOnly(); - } else if (name == 'DesktopEntry') { - return DBusMethodErrorResponse.propertyReadOnly(); - } else if (name == 'SupportedUriSchemes') { - return DBusMethodErrorResponse.propertyReadOnly(); - } else if (name == 'SupportedMimeTypes') { - return DBusMethodErrorResponse.propertyReadOnly(); - } else { - return DBusMethodErrorResponse.unknownProperty(); - } - } else { - return DBusMethodErrorResponse.unknownProperty(); - } - } - - @override - Future getAllProperties(String interface) async { - var properties = {}; - if (interface == 'org.mpris.MediaPlayer2') { - properties['CanQuit'] = (await getCanQuit()).returnValues[0]; - properties['Fullscreen'] = (await getFullscreen()).returnValues[0]; - properties['CanSetFullscreen'] = - (await getCanSetFullscreen()).returnValues[0]; - properties['CanRaise'] = (await getCanRaise()).returnValues[0]; - properties['HasTrackList'] = (await getHasTrackList()).returnValues[0]; - properties['Identity'] = (await getIdentity()).returnValues[0]; - properties['DesktopEntry'] = (await getDesktopEntry()).returnValues[0]; - properties['SupportedUriSchemes'] = - (await getSupportedUriSchemes()).returnValues[0]; - properties['SupportedMimeTypes'] = - (await getSupportedMimeTypes()).returnValues[0]; - } - return DBusMethodSuccessResponse([DBusDict.stringVariant(properties)]); - } -} - -class _MprisMediaPlayer2Player extends DBusObject { - final Ref ref; - final PlaylistQueueNotifier playlistNotifier; - - /// Creates a new object to expose on [path]. - _MprisMediaPlayer2Player(this.ref, this.playlistNotifier) - : super(DBusObjectPath("/org/mpris/MediaPlayer2")) { - (() async { - final nameStatus = - await dbus.requestName("org.mpris.MediaPlayer2.spotube"); - if (nameStatus == DBusRequestNameReply.exists) { - await dbus.requestName("org.mpris.MediaPlayer2.spotube.instance$pid"); - } - await dbus.registerObject(this); - }()); - } - - PlaylistQueue? get playlist => playlistNotifier.state; - double get volume => ref.read(VolumeProvider.provider); - VolumeProvider get volumeNotifier => - ref.read(VolumeProvider.provider.notifier); - - void dispose() { - dbus.unregisterObject(this); - } - - /// Gets value of property org.mpris.MediaPlayer2.Player.PlaybackStatus - Future getPlaybackStatus() async { - final status = PlaylistQueueNotifier.isPlaying - ? "Playing" - : playlist == null - ? "Stopped" - : "Paused"; - return DBusMethodSuccessResponse([DBusString(status)]); - } - - // TODO: Implement Track Loop - - /// Gets value of property org.mpris.MediaPlayer2.Player.LoopStatus - Future getLoopStatus() async { - return DBusMethodSuccessResponse([ - /* playlistNotifier.isLoop */ false - ? const DBusString("Track") - : const DBusString("None"), - ]); - } - - /// Sets property org.mpris.MediaPlayer2.Player.LoopStatus - Future setLoopStatus(String value) async { - // playlistNotifier.setIsLoop(value == "Track"); - return DBusMethodSuccessResponse(); - } - - /// Gets value of property org.mpris.MediaPlayer2.Player.Rate - Future getRate() async { - return DBusMethodSuccessResponse([const DBusDouble(1)]); - } - - /// Sets property org.mpris.MediaPlayer2.Player.Rate - Future setRate(double value) async { - return DBusMethodSuccessResponse(); - } - - /// Gets value of property org.mpris.MediaPlayer2.Player.Shuffle - Future getShuffle() async { - return DBusMethodSuccessResponse( - [DBusBoolean(playlist?.isShuffled ?? false)]); - } - - /// Sets property org.mpris.MediaPlayer2.Player.Shuffle - Future setShuffle(bool value) async { - if (value) { - playlistNotifier.shuffle(); - } else { - playlistNotifier.unshuffle(); - } - return DBusMethodSuccessResponse(); - } - - /// Gets value of property org.mpris.MediaPlayer2.Player.Metadata - Future getMetadata() async { - if (playlist == null || playlist!.isLoading) { - return DBusMethodSuccessResponse([DBusDict.stringVariant({})]); - } - final id = playlist!.active; - - return DBusMethodSuccessResponse([ - DBusDict.stringVariant({ - "mpris:trackid": DBusString("${path.value}/Track/$id"), - "mpris:length": - DBusInt32((await audioPlayer.getDuration())?.inMicroseconds ?? 0), - "mpris:artUrl": DBusString( - TypeConversionUtils.image_X_UrlString( - playlist?.activeTrack.album?.images, - placeholder: ImagePlaceholder.albumArt, - ), - ), - "xesam:album": DBusString(playlist!.activeTrack.album!.name!), - "xesam:artist": DBusArray.string( - playlist!.activeTrack.artists!.map((artist) => artist.name!), - ), - "xesam:title": DBusString(playlist!.activeTrack.name!), - "xesam:url": DBusString( - playlist!.activeTrack is SpotubeTrack - ? (playlist!.activeTrack as SpotubeTrack).ytUri - : playlist!.activeTrack.previewUrl!, - ), - "xesam:genre": const DBusString("Unknown"), - }), - ]); - } - - /// Gets value of property org.mpris.MediaPlayer2.Player.Volume - Future getVolume() async { - return DBusMethodSuccessResponse([DBusDouble(volume)]); - } - - /// Sets property org.mpris.MediaPlayer2.Player.Volume - Future setVolume(double value) async { - await volumeNotifier.setVolume(value); - return DBusMethodSuccessResponse(); - } - - /// Gets value of property org.mpris.MediaPlayer2.Player.Position - Future getPosition() async { - return DBusMethodSuccessResponse([ - DBusInt64((await audioPlayer.getDuration())?.inMicroseconds ?? 0), - ]); - } - - /// Gets value of property org.mpris.MediaPlayer2.Player.MinimumRate - Future getMinimumRate() async { - return DBusMethodSuccessResponse([const DBusDouble(1)]); - } - - /// Gets value of property org.mpris.MediaPlayer2.Player.MaximumRate - Future getMaximumRate() async { - return DBusMethodSuccessResponse([const DBusDouble(1)]); - } - - /// Gets value of property org.mpris.MediaPlayer2.Player.CanGoNext - Future getCanGoNext() async { - return DBusMethodSuccessResponse([ - DBusBoolean( - (playlist?.tracks.length ?? 0) > 1, - ) - ]); - } - - /// Gets value of property org.mpris.MediaPlayer2.Player.CanGoPrevious - Future getCanGoPrevious() async { - return DBusMethodSuccessResponse([ - DBusBoolean( - (playlist?.tracks.length ?? 0) > 1, - ) - ]); - } - - /// Gets value of property org.mpris.MediaPlayer2.Player.CanPlay - Future getCanPlay() async { - return DBusMethodSuccessResponse([const DBusBoolean(true)]); - } - - /// Gets value of property org.mpris.MediaPlayer2.Player.CanPause - Future getCanPause() async { - return DBusMethodSuccessResponse([const DBusBoolean(true)]); - } - - /// Gets value of property org.mpris.MediaPlayer2.Player.CanSeek - Future getCanSeek() async { - return DBusMethodSuccessResponse([const DBusBoolean(true)]); - } - - /// Gets value of property org.mpris.MediaPlayer2.Player.CanControl - Future getCanControl() async { - return DBusMethodSuccessResponse([const DBusBoolean(true)]); - } - - /// Implementation of org.mpris.MediaPlayer2.Player.Next() - Future doNext() async { - await playlistNotifier.next(); - return DBusMethodSuccessResponse(); - } - - /// Implementation of org.mpris.MediaPlayer2.Player.Previous() - Future doPrevious() async { - await playlistNotifier.previous(); - return DBusMethodSuccessResponse(); - } - - /// Implementation of org.mpris.MediaPlayer2.Player.Pause() - Future doPause() async { - playlistNotifier.pause(); - return DBusMethodSuccessResponse(); - } - - /// Implementation of org.mpris.MediaPlayer2.Player.PlayPause() - Future doPlayPause() async { - PlaylistQueueNotifier.isPlaying - ? await playlistNotifier.pause() - : await playlistNotifier.resume(); - return DBusMethodSuccessResponse(); - } - - /// Implementation of org.mpris.MediaPlayer2.Player.Stop() - Future doStop() async { - playlistNotifier.stop(); - return DBusMethodSuccessResponse(); - } - - /// Implementation of org.mpris.MediaPlayer2.Player.Play() - Future doPlay() async { - playlistNotifier.resume(); - return DBusMethodSuccessResponse(); - } - - /// Implementation of org.mpris.MediaPlayer2.Player.Seek() - Future doSeek(int offset) async { - await playlistNotifier.seek(Duration(microseconds: offset)); - return DBusMethodSuccessResponse(); - } - - /// Implementation of org.mpris.MediaPlayer2.Player.SetPosition() - Future doSetPosition(String TrackId, int Position) async { - return DBusMethodSuccessResponse(); - } - - /// Implementation of org.mpris.MediaPlayer2.Player.OpenUri() - Future doOpenUri(String Uri) async { - return DBusMethodSuccessResponse(); - } - - /// Emits signal org.mpris.MediaPlayer2.Player.Seeked - Future emitSeeked(int position) async { - await emitSignal( - 'org.mpris.MediaPlayer2.Player', - 'Seeked', - [DBusInt64(position)], - ); - } - - Future updateProperties() async { - return emitPropertiesChanged( - "org.mpris.MediaPlayer2.Player", - changedProperties: { - "PlaybackStatus": (await getPlaybackStatus()).returnValues.first, - "LoopStatus": (await getLoopStatus()).returnValues.first, - "Rate": (await getRate()).returnValues.first, - "Shuffle": (await getShuffle()).returnValues.first, - "Metadata": (await getMetadata()).returnValues.first, - "Volume": (await getVolume()).returnValues.first, - "Position": (await getPosition()).returnValues.first, - "MinimumRate": (await getMinimumRate()).returnValues.first, - "MaximumRate": (await getMaximumRate()).returnValues.first, - "CanGoNext": (await getCanGoNext()).returnValues.first, - "CanGoPrevious": (await getCanGoPrevious()).returnValues.first, - "CanPlay": (await getCanPlay()).returnValues.first, - "CanPause": (await getCanPause()).returnValues.first, - "CanSeek": (await getCanSeek()).returnValues.first, - "CanControl": (await getCanControl()).returnValues.first, - }, - ); - } - - @override - List introspect() { - return [ - DBusIntrospectInterface('org.mpris.MediaPlayer2.Player', methods: [ - DBusIntrospectMethod('Next'), - DBusIntrospectMethod('Previous'), - DBusIntrospectMethod('Pause'), - DBusIntrospectMethod('PlayPause'), - DBusIntrospectMethod('Stop'), - DBusIntrospectMethod('Play'), - DBusIntrospectMethod('Seek', args: [ - DBusIntrospectArgument(DBusSignature('x'), DBusArgumentDirection.in_, - name: 'Offset') - ]), - DBusIntrospectMethod('SetPosition', args: [ - DBusIntrospectArgument(DBusSignature('o'), DBusArgumentDirection.in_, - name: 'TrackId'), - DBusIntrospectArgument(DBusSignature('x'), DBusArgumentDirection.in_, - name: 'Position') - ]), - DBusIntrospectMethod('OpenUri', args: [ - DBusIntrospectArgument(DBusSignature('s'), DBusArgumentDirection.in_, - name: 'Uri') - ]) - ], signals: [ - DBusIntrospectSignal('Seeked', args: [ - DBusIntrospectArgument(DBusSignature('x'), DBusArgumentDirection.out, - name: 'Position') - ]) - ], properties: [ - DBusIntrospectProperty('PlaybackStatus', DBusSignature('s'), - access: DBusPropertyAccess.read), - DBusIntrospectProperty('LoopStatus', DBusSignature('s'), - access: DBusPropertyAccess.readwrite), - DBusIntrospectProperty('Rate', DBusSignature('d'), - access: DBusPropertyAccess.readwrite), - DBusIntrospectProperty('Shuffle', DBusSignature('b'), - access: DBusPropertyAccess.readwrite), - DBusIntrospectProperty('Metadata', DBusSignature('a{sv}'), - access: DBusPropertyAccess.read), - DBusIntrospectProperty('Volume', DBusSignature('d'), - access: DBusPropertyAccess.readwrite), - DBusIntrospectProperty('Position', DBusSignature('x'), - access: DBusPropertyAccess.read), - DBusIntrospectProperty('MinimumRate', DBusSignature('d'), - access: DBusPropertyAccess.read), - DBusIntrospectProperty('MaximumRate', DBusSignature('d'), - access: DBusPropertyAccess.read), - DBusIntrospectProperty('CanGoNext', DBusSignature('b'), - access: DBusPropertyAccess.read), - DBusIntrospectProperty('CanGoPrevious', DBusSignature('b'), - access: DBusPropertyAccess.read), - DBusIntrospectProperty('CanPlay', DBusSignature('b'), - access: DBusPropertyAccess.read), - DBusIntrospectProperty('CanPause', DBusSignature('b'), - access: DBusPropertyAccess.read), - DBusIntrospectProperty('CanSeek', DBusSignature('b'), - access: DBusPropertyAccess.read), - DBusIntrospectProperty('CanControl', DBusSignature('b'), - access: DBusPropertyAccess.read) - ]) - ]; - } - - @override - Future handleMethodCall(DBusMethodCall methodCall) async { - if (methodCall.interface == 'org.mpris.MediaPlayer2.Player') { - if (methodCall.name == 'Next') { - if (methodCall.values.isNotEmpty) { - return DBusMethodErrorResponse.invalidArgs(); - } - return doNext(); - } else if (methodCall.name == 'Previous') { - if (methodCall.values.isNotEmpty) { - return DBusMethodErrorResponse.invalidArgs(); - } - return doPrevious(); - } else if (methodCall.name == 'Pause') { - if (methodCall.values.isNotEmpty) { - return DBusMethodErrorResponse.invalidArgs(); - } - return doPause(); - } else if (methodCall.name == 'PlayPause') { - if (methodCall.values.isNotEmpty) { - return DBusMethodErrorResponse.invalidArgs(); - } - return doPlayPause(); - } else if (methodCall.name == 'Stop') { - if (methodCall.values.isNotEmpty) { - return DBusMethodErrorResponse.invalidArgs(); - } - return doStop(); - } else if (methodCall.name == 'Play') { - if (methodCall.values.isNotEmpty) { - return DBusMethodErrorResponse.invalidArgs(); - } - return doPlay(); - } else if (methodCall.name == 'Seek') { - if (methodCall.signature != DBusSignature('x')) { - return DBusMethodErrorResponse.invalidArgs(); - } - return doSeek((methodCall.values[0] as DBusInt64).value); - } else if (methodCall.name == 'SetPosition') { - if (methodCall.signature != DBusSignature('ox')) { - return DBusMethodErrorResponse.invalidArgs(); - } - return doSetPosition((methodCall.values[0] as DBusObjectPath).value, - (methodCall.values[1] as DBusInt64).value); - } else if (methodCall.name == 'OpenUri') { - if (methodCall.signature != DBusSignature('s')) { - return DBusMethodErrorResponse.invalidArgs(); - } - return doOpenUri((methodCall.values[0] as DBusString).value); - } else { - return DBusMethodErrorResponse.unknownMethod(); - } - } else { - return DBusMethodErrorResponse.unknownInterface(); - } - } - - @override - Future getProperty(String interface, String name) async { - if (interface == 'org.mpris.MediaPlayer2.Player') { - if (name == 'PlaybackStatus') { - return getPlaybackStatus(); - } else if (name == 'LoopStatus') { - return getLoopStatus(); - } else if (name == 'Rate') { - return getRate(); - } else if (name == 'Shuffle') { - return getShuffle(); - } else if (name == 'Metadata') { - return getMetadata(); - } else if (name == 'Volume') { - return getVolume(); - } else if (name == 'Position') { - return getPosition(); - } else if (name == 'MinimumRate') { - return getMinimumRate(); - } else if (name == 'MaximumRate') { - return getMaximumRate(); - } else if (name == 'CanGoNext') { - return getCanGoNext(); - } else if (name == 'CanGoPrevious') { - return getCanGoPrevious(); - } else if (name == 'CanPlay') { - return getCanPlay(); - } else if (name == 'CanPause') { - return getCanPause(); - } else if (name == 'CanSeek') { - return getCanSeek(); - } else if (name == 'CanControl') { - return getCanControl(); - } else { - return DBusMethodErrorResponse.unknownProperty(); - } - } else { - return DBusMethodErrorResponse.unknownProperty(); - } - } - - @override - Future setProperty( - String interface, String name, DBusValue value) async { - if (interface == 'org.mpris.MediaPlayer2.Player') { - if (name == 'PlaybackStatus') { - return DBusMethodErrorResponse.propertyReadOnly(); - } else if (name == 'LoopStatus') { - if (value.signature != DBusSignature('s')) { - return DBusMethodErrorResponse.invalidArgs(); - } - return setLoopStatus((value as DBusString).value); - } else if (name == 'Rate') { - if (value.signature != DBusSignature('d')) { - return DBusMethodErrorResponse.invalidArgs(); - } - return setRate((value as DBusDouble).value); - } else if (name == 'Shuffle') { - if (value.signature != DBusSignature('b')) { - return DBusMethodErrorResponse.invalidArgs(); - } - return setShuffle((value as DBusBoolean).value); - } else if (name == 'Metadata') { - return DBusMethodErrorResponse.propertyReadOnly(); - } else if (name == 'Volume') { - if (value.signature != DBusSignature('d')) { - return DBusMethodErrorResponse.invalidArgs(); - } - return setVolume((value as DBusDouble).value); - } else if (name == 'Position') { - return DBusMethodErrorResponse.propertyReadOnly(); - } else if (name == 'MinimumRate') { - return DBusMethodErrorResponse.propertyReadOnly(); - } else if (name == 'MaximumRate') { - return DBusMethodErrorResponse.propertyReadOnly(); - } else if (name == 'CanGoNext') { - return DBusMethodErrorResponse.propertyReadOnly(); - } else if (name == 'CanGoPrevious') { - return DBusMethodErrorResponse.propertyReadOnly(); - } else if (name == 'CanPlay') { - return DBusMethodErrorResponse.propertyReadOnly(); - } else if (name == 'CanPause') { - return DBusMethodErrorResponse.propertyReadOnly(); - } else if (name == 'CanSeek') { - return DBusMethodErrorResponse.propertyReadOnly(); - } else if (name == 'CanControl') { - return DBusMethodErrorResponse.propertyReadOnly(); - } else { - return DBusMethodErrorResponse.unknownProperty(); - } - } else { - return DBusMethodErrorResponse.unknownProperty(); - } - } - - @override - Future getAllProperties(String interface) async { - var properties = {}; - if (interface == 'org.mpris.MediaPlayer2.Player') { - properties['PlaybackStatus'] = - (await getPlaybackStatus()).returnValues[0]; - properties['LoopStatus'] = (await getLoopStatus()).returnValues[0]; - properties['Rate'] = (await getRate()).returnValues[0]; - properties['Shuffle'] = (await getShuffle()).returnValues[0]; - properties['Metadata'] = (await getMetadata()).returnValues[0]; - properties['Volume'] = (await getVolume()).returnValues[0]; - properties['Position'] = (await getPosition()).returnValues[0]; - properties['MinimumRate'] = (await getMinimumRate()).returnValues[0]; - properties['MaximumRate'] = (await getMaximumRate()).returnValues[0]; - properties['CanGoNext'] = (await getCanGoNext()).returnValues[0]; - properties['CanGoPrevious'] = (await getCanGoPrevious()).returnValues[0]; - properties['CanPlay'] = (await getCanPlay()).returnValues[0]; - properties['CanPause'] = (await getCanPause()).returnValues[0]; - properties['CanSeek'] = (await getCanSeek()).returnValues[0]; - properties['CanControl'] = (await getCanControl()).returnValues[0]; - } - return DBusMethodSuccessResponse([DBusDict.stringVariant(properties)]); - } -} - -class LinuxAudioService { - _MprisMediaPlayer2 mp2; - _MprisMediaPlayer2Player player; - - LinuxAudioService(Ref ref, PlaylistQueueNotifier playlistNotifier) - : mp2 = _MprisMediaPlayer2(), - player = _MprisMediaPlayer2Player(ref, playlistNotifier); - - void dispose() { - mp2.dispose(); - player.dispose(); - } -} diff --git a/pubspec.lock b/pubspec.lock index 5345a65a..f9d915b1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -209,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + base_x: + dependency: transitive + description: + name: base_x + sha256: "3f1043679659f1759c651f900da6f24f0a8062c28daa6f9625e8d580002e187b" + url: "https://pub.dev" + source: hosted + version: "2.0.0" boolean_selector: dependency: transitive description: @@ -1065,6 +1073,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + mpris_service: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "3a4a72140087d3df130271c9213e61fa413d5740" + url: "https://github.com/alexmercerind/mpris_service" + source: git + version: "1.0.0" mutex: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5508ec16..1ca40885 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,7 +24,6 @@ dependencies: collection: ^1.15.0 cupertino_icons: ^1.0.5 curved_navigation_bar: ^1.0.3 - dbus: ^0.7.8 envied: ^0.3.0 file_picker: ^5.2.2 fl_query: ^1.0.0-alpha.2 @@ -89,6 +88,9 @@ dependencies: url: https://github.com/KRTirtho/smtc_windows.git ref: 6cc93624b8fab8d7727c8693e91577a7413ccd13 path: packages/smtc_windows + mpris_service: + git: + url: https://github.com/alexmercerind/mpris_service dev_dependencies: build_runner: ^2.3.2