Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions devtools_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
16 changes: 7 additions & 9 deletions lib/core/services/audio_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,14 @@ class BusicAudioHandler extends BaseAudioHandler with SeekHandler {
mediaItem.add(null);
return;
}
mediaItem.add(MediaItem(
id: '${track.bvid}_${track.cid}',
title: track.title,
artist: track.artist,
artUri: track.coverUrl != null ? Uri.tryParse(track.coverUrl!) : null,
duration: duration ?? track.duration,
));

mediaItem.add(
MediaItem(
id: '${track.bvid}_${track.cid}',
title: track.title,
artist: track.artist,
artUri: track.coverUrl != null ? Uri.tryParse(track.coverUrl!) : null,
duration: duration ?? track.duration,
),
);
}

/// Update the playback state shown in the media session.
Expand Down
185 changes: 185 additions & 0 deletions lib/core/services/mpris_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import 'dart:io';
import 'package:busic/features/player/domain/models/audio_track.dart';
import 'package:busic/features/player/domain/models/play_mode.dart';
import 'package:dbus/dbus.dart';
import 'package:flutter/foundation.dart';
import 'mpris_service_dbus.dart';

/// Service managing MPRIS (Media Player Remote Interfacing Specification) on Linux.
///
/// It registers Busic as a media player on the system D-Bus, enabling integration
/// with system-level media widgets, shell extensions, and hardware media keys.
class MprisService {
DBusClient? _dbusClient;
MprisServiceDbus? _mprisObject;

// Callbacks hooked into PlayerNotifier to bridge system actions to the player core.
VoidCallback? onPlay;
VoidCallback? onPause;
VoidCallback? onToggle;
VoidCallback? onNext;
VoidCallback? onPrevious;
ValueSetter<Duration>? onSeek;
ValueSetter<double>? setVolume;
ValueSetter<PlayMode>? setMode;

/// Initializes the MPRIS service and registers the D-Bus object on Linux.
///
/// Binds the incoming control callbacks from [PlayerNotifier] and exposes
/// the media controller interface under `/org/mpris/MediaPlayer2`.
void init({
required VoidCallback onPlay,
required VoidCallback onPause,
required VoidCallback onNext,
required VoidCallback onPrevious,
required ValueSetter<Duration> onSeek,
required ValueSetter<double> setVolume,
required ValueSetter<PlayMode> setMode,
}) {
if (!Platform.isLinux) return;

this.onPlay = () {
updatePlaybackStatus(true);
onPlay();
};
this.onPause = () {
updatePlaybackStatus(false);
onPause();
};
onToggle = () {
if (_mprisObject!.playbackState == 'Playing') {
this.onPause?.call();
} else if (_mprisObject!.playbackState == 'Paused') {
this.onPlay?.call();
}
};
this.onNext = onNext;
this.onPrevious = onPrevious;
this.onSeek = (duration) {
updatePosition(duration);
onSeek(duration);
};
this.setVolume = setVolume;
this.setMode = setMode;
try {
// 1. Establish connection to the D-Bus Session Bus.
_dbusClient = DBusClient.session();

// 2. Instantiate the generated DBusObject on the standard MPRIS object path.
_mprisObject = MprisServiceDbus(
path: DBusObjectPath('/org/mpris/MediaPlayer2'));

// 3. Expose the object to external clients on D-Bus.
_dbusClient!.registerObject(_mprisObject!);

// 4. Request the well-known name corresponding to this media player.
_dbusClient!.requestName('org.mpris.MediaPlayer2.busic').then((_) {
_bindSystemCommands();
DBusBoolean bTrue = const DBusBoolean(true);
_mprisObject?.updatePlayerProperties({
'CanControl': bTrue,
'CanGoNext': bTrue,
'CanGoPrevious': bTrue,
'CanPause': bTrue,
'CanPlay': bTrue,
'CanSeek': bTrue,
});
});
} catch (e) {
debugPrint('Failed to initialize MPRIS Service: $e');
}
}

/// Binds player control actions from D-Bus commands to our local handlers.
void _bindSystemCommands() {
if (_mprisObject == null) return;
_mprisObject!.onPlay = onPlay!;
_mprisObject!.onNext = onNext!;
_mprisObject!.onPause = onPause!;
_mprisObject!.onSeek = onSeek!;
_mprisObject!.onPrevious = onPrevious!;
_mprisObject!.onToggle = onToggle!;
_mprisObject!.setVolume = setVolume!;
_mprisObject!.setMode = setMode!;
}

/// Maps and forwards current [AudioTrack] metadata updates to D-Bus properties.
void updateCurrentTrack(AudioTrack? track, {Duration? duration}) {
if (track == null) return;
updateMetadata(
title: track.title,
artist: track.artist,
artUrl: track.coverUrl,
duration: duration ?? track.duration,
);
}

/// Packs raw song metadata into the standard MPRIS Dict format (a{sv})
/// and notifies the D-Bus daemon of property updates.
void updateMetadata({
required String title,
required String artist,
String? album,
String? artUrl,
Duration? duration,
}) {
if (_mprisObject == null) return;

// MPRIS specification expects metadata as a string-variant dictionary.
_mprisObject!.metadata = {
'mpris:trackid': DBusObjectPath('/'),
'mpris:length': DBusInt64(duration?.inMicroseconds ?? 0), // MPRIS uses microseconds for duration.
'xesam:title': DBusString(title),
'xesam:artist': DBusArray.string([artist]),
'xesam:album': DBusString(album ?? ''),
'mpris:artUrl': DBusString(artUrl ?? ''),
};

_mprisObject?.updatePlayerProperties({
'Metadata': DBusDict.stringVariant(_mprisObject!.metadata)
});
}

/// Updates the player's playback state (Playing / Paused / Stopped) on D-Bus.
void updatePlaybackStatus(bool isPlaying) {
if (_mprisObject == null) return;
String status = isPlaying ? 'Playing' : 'Paused';
_mprisObject!.playbackState = status;
_mprisObject?.updatePlayerProperties({
'PlaybackStatus': DBusString(status)
});
}

/// Synchronizes the current playback position (in microseconds) with D-Bus.
void updatePosition(Duration position) {
if (_mprisObject == null) return;
_mprisObject!.position = position.inMicroseconds;
}
/// Synchronizes the player's loop status (enum PlayMode)on D-Bus.
void updateLoopStatus(PlayMode mode) {
String status = switch(mode){
PlayMode.sequential => 'None',
PlayMode.repeatOne=> 'Track',
PlayMode.shuffle => 'Shuffle',
PlayMode.repeatAll => 'Playlist'
};
if (status == 'Shuffle') {
_mprisObject?.shuffle = true;
_mprisObject?.updatePlayerProperties({'Shuffle' : const DBusBoolean(true)});
} else {
_mprisObject?.loopStatus = status;
_mprisObject?.updatePlayerProperties({'LoopStatus': DBusString(status)});
}
}
/// Synchronizes the player's volume (0.0 to 1.0) with D-Bus.
void updateVolume(double volume) {
_mprisObject?.volume = volume > 1.0 ? 1.0 : volume < 0.0 ? 0.0 : volume;
_mprisObject?.updatePlayerProperties({'Volume': DBusDouble(volume)});
}
/// Closes active D-Bus connections and frees resources when stopping.
void dispose() {
_dbusClient?.close();
_dbusClient = null;
_mprisObject = null;
}
}
Loading
Loading