Playerctl for Flutter (Linux)
A Flutter plugin for Linux that provides robust media playback control using the playerctl command-line tool. Built with SOLID principles, Pure Dart (no FFI), and state-management-agnostic architecture.
Features
✅ Real-time Media Information
- Song title, artist, album
- Album artwork with local HTTP server (cross-device access)
- Playback status (Playing, Paused, Stopped)
- Player name detection (Spotify, VLC, Brave, etc.)
- Track position and length
- Shuffle and loop status
✅ Playback Controls
- Play/Pause/Stop
- Next/Previous track
- Volume control (0-100)
- Shuffle toggle
- Loop cycling (None → Track → Playlist)
- Seek/scrub functionality (position control in microseconds)
- Forward/backward skip by seconds
✅ Multi-Player Support
- Detect all active MPRIS-compatible players
- Switch between different media players
- Automatic player switching when current player closes
- Real-time synchronization across multiple players
✅ Robust Error Handling
- Automatic process restart (up to 5 attempts)
- Handles playerctl crashes gracefully
- Checks if playerctl is installed
- Handles no active players gracefully
- Special character support in metadata (pipe characters, etc.)
✅ State-Agnostic Architecture
- Use with any state management solution (GetX, Riverpod, Bloc, Provider, etc.)
- Clean SOLID architecture
- Service-oriented design with dependency injection
- Optional GetX wrapper included
✅ Advanced Synchronization
- Triple-layer sync (real-time stream + periodic metadata refresh + volume sync)
- External volume changes detected automatically
- Debounced player switching to prevent glitches
✅ Configurable Logging
- Level-wise logging (none, error, warning, info, debug)
- Emoji-based categorization (🔍 DEBUG, ℹ️ INFO, ⚠️ WARNING, ❌ ERROR, etc.)
- Production-ready with silent mode
- Customizable log levels at runtime
Requirements
- Platform: Linux only
- playerctl: Must be installed on the system
Installing playerctl
# Debian/Ubuntu
sudo apt install playerctl
# Arch Linux
sudo pacman -S playerctl
# Fedora
sudo dnf install playerctl
# openSUSE
sudo zypper install playerctl
Installation
Add this to your package's pubspec.yaml file:
dependencies:
playerctl:
git:
url: https://github.com/yourusername/playerctl.git
Or if you're developing locally:
dependencies:
playerctl:
path: ../playerctl
Usage
Option 1: Using the Core Manager (State-Agnostic)
import 'package:playerctl/playerctl.dart';
// Create the manager
final manager = MediaPlayerManager();
// Listen to state changes
manager.stateStream.listen((state) {
print('Title: ${state.currentMedia.title}');
print('Artist: ${state.currentMedia.artist}');
print('Status: ${state.playbackStatus}');
print('Volume: ${state.volume}');
print('Shuffle: ${state.shuffleStatus}');
print('Loop: ${state.loopStatus}');
});
// Initialize
await manager.initialize();
// Control playback
await manager.play();
await manager.pause();
await manager.next();
await manager.previous();
await manager.setVolume(75);
await manager.toggleShuffle();
await manager.cycleLoop();
// Switch players
await manager.switchPlayer('spotify');
// Cleanup
manager.dispose();
Option 2: Using GetX Wrapper
import 'package:playerctl/playerctl.dart';
import 'package:get/get.dart';
// Initialize the controller
final MediaController controller = Get.put(MediaController());
// The controller automatically:
// - Checks if playerctl is installed
// - Detects active media players
// - Starts listening to metadata changes
// - Handles player disconnections/reconnections
// - Syncs volume and metadata periodically
### Accessing Media Information (GetX)
```dart
Obx(() {
final media = controller.currentMedia.value;
return Column(
children: [
Text('Title: ${media.title}'),
Text('Artist: ${media.artist}'),
Text('Album: ${media.album}'),
Text('Status: ${media.status}'),
Text('Player: ${media.playerName}'),
Text('Shuffle: ${controller.shuffleStatus.value}'),
Text('Loop: ${controller.loopStatus.value}'),
],
);
});
Playback Controls (GetX)
// Play/Pause toggle
ElevatedButton(
onPressed: () => controller.playPause(),
child: Text('Play/Pause'),
);
// Individual controls
controller.play();
controller.pause();
controller.stop();
controller.next();
controller.previous();
// Shuffle and loop
controller.toggleShuffle();
controller.cycleLoop(); // Cycles through None → Track → Playlist → None
Volume Control (GetX)
Obx(() => Slider(
value: controller.volume.value.toDouble(),
min: 0,
max: 100,
onChanged: (value) => controller.setVolume(value.toInt()),
));
Seek/Position Control
// Get current position in microseconds
final position = await manager.getPosition(); // Returns int? (microseconds)
// Seek to absolute position (30 seconds = 30,000,000 microseconds)
await manager.seekTo(30000000);
// Seek relative to current position (forward 10 seconds)
await manager.seek(10000000);
// Seek backward (negative offset)
await manager.seek(-10000000);
// Convenience methods for seconds-based seeking
await manager.seekForward(10); // Skip forward 10 seconds
await manager.seekBackward(5); // Skip backward 5 seconds
// All seek methods support optional player parameter
await manager.seekForward(10, 'spotify');
Note: Position values are in microseconds (MPRIS standard). To convert:
- Seconds → Microseconds:
seconds * 1,000,000 - Microseconds → Seconds:
microseconds / 1,000,000
Album Art Server
The plugin automatically starts a local HTTP server (on port 8765) to serve album artwork from local files. This allows you to access album art from other devices on your network.
Features:
- Automatically converts
file://URLs to local HTTP URLs - Online URLs (https://) from services like Spotify remain unchanged
- Server runs on
0.0.0.0:8765for network accessibility - CORS enabled for cross-origin requests
Cross-Device Access:
Album art URLs use 0.0.0.0 which you can replace with your machine's IP address:
// Original URL from plugin
final artUrl = 'http://0.0.0.0:8765/art/abc123.jpg';
// Replace with your machine's IP for access from other devices
final networkUrl = artUrl.replaceAll('0.0.0.0', '192.168.1.100');
// Now accessible as: http://192.168.1.100:8765/art/abc123.jpg
Example Usage:
// Get album art URL from metadata
final artUrl = state.currentMedia.artUrl;
// Display in Image widget
if (artUrl.isNotEmpty) {
Image.network(
artUrl.replaceAll('0.0.0.0', 'YOUR_MACHINE_IP'),
errorBuilder: (context, error, stackTrace) {
return Icon(Icons.album); // Fallback icon
},
);
}
Server Management:
- Server starts automatically when metadata is first fetched
- Server stops automatically when the manager is disposed
- Health check available at
http://0.0.0.0:8765/
Player Selection (GetX)
Obx(() {
if (controller.availablePlayers.length > 1) {
return DropdownButton<String>(
value: controller.selectedPlayer.value,
items: controller.availablePlayers.map((player) {
return DropdownMenuItem(
value: player,
child: Text(player),
);
}).toList(),
onChanged: (player) {
if (player != null) controller.switchPlayer(player);
},
);
}
return Container();
});
Error Handling
Obx(() {
// Check if playerctl is installed
if (!controller.isPlayerctlInstalled.value) {
return Text('Please install playerctl');
}
// Check for active players
if (!controller.hasActivePlayer.value) {
return Text('No active media players');
}
// Show any error messages
if (controller.errorMessage.value.isNotEmpty) {
return Text('Error: ${controller.errorMessage.value}');
}
return YourMediaWidget();
});
Architecture
This plugin follows SOLID principles with a clean, layered architecture:
Core Layer (State-Agnostic)
-
MediaPlayerManager: Main coordinator class
- State-management-agnostic API
- Manages player lifecycle
- Handles automatic reconnection
- Triple-layer synchronization (stream + metadata refresh + volume sync)
- Debounced player switching
-
PlayerState: Immutable state container
- All player information in one place
- Includes shuffle/loop status
- Easy to serialize/persist
Service Layer
-
PlayerctlService: Main facade service
- Combines all specialized services
- Provides unified API
-
MetadataProvider: Real-time metadata streaming
- Automatic process restart on failure
- Special character handling (triple-pipe delimiter)
- Up to 5 restart attempts
-
PlayerDetector: Player discovery and management
- Lists available MPRIS players
- Monitors player availability
-
PlaybackController: Playback command execution
- Play/pause/stop/next/previous
- Shuffle toggle and status
- Loop cycling (None/Track/Playlist)
-
VolumeController: Volume management
- Get/set volume (0-100)
- Periodic sync to detect external changes
-
CommandExecutor: Low-level command execution
- Process management
- Error handling
State Management Wrappers
- MediaController: Optional GetX wrapper
- Reactive observables
- Automatic lifecycle management
- Easy integration with GetX apps
Models
- MediaInfo: Metadata container
- Title, artist, album, status, player name
- Track position and length
Logging Configuration
The playerctl package includes a comprehensive logging system with configurable log levels.
Setting Log Level
There are 4 ways to configure logging:
Method 1: Global Configuration (Before creating managers)
import 'package:playerctl/playerctl.dart';
void main() {
PlayerctlLogger.level = LogLevel.info; // Options: none, error, warning, info, debug
// Or: PlayerctlLogger.disableAll(); // Silent
// Or: PlayerctlLogger.enableAll(); // Verbose
runApp(MyApp());
}
Method 2: Constructor Configuration
final manager = MediaPlayerManager(
logLevel: LogLevel.info, // Set initial log level
);
Method 3: Runtime Configuration
// Change log level anytime
manager.setLogLevel(LogLevel.error);
// Check current level
LogLevel current = manager.logLevel;
Method 4: Direct Logger Access
PlayerctlLogger.level = LogLevel.warning;
Log Levels
- LogLevel.none - No logging (production)
- LogLevel.error - Only errors
- LogLevel.warning - Warnings and errors
- LogLevel.info - Info, warnings, and errors (recommended)
- LogLevel.debug - All logs including verbose output
Default Behavior:
- Debug mode:
LogLevel.debug(all logs shown) - Release mode:
LogLevel.error(only errors shown)
Log Categories
Logs are categorized with emoji indicators for easy identification:
- 🔍 DEBUG - Verbose debugging information
- ℹ️ INFO - General information
- ⚠️ WARNING - Warning messages
- ❌ ERROR - Error messages
- ✅ SUCCESS - Success confirmations
- 🎵 METADATA - Metadata updates
- 📋 PLAYER - Player events
- 🔊 VOLUME - Volume changes
- 🔄 SYNC - Synchronization events
See LOGGING.md for detailed documentation and examples.
Advanced Usage
Direct Service Access
If you need lower-level access without GetX:
final service = PlayerctlService();
// Check installation
bool installed = await service.isPlayerctlInstalled();
// Get players
List<String> players = await service.getAvailablePlayers();
// Listen to metadata
service.listenToMetadata().listen((metadata) {
print('Title: ${metadata['title']}');
print('Artist: ${metadata['artist']}');
});
// Send commands
await service.play();
await service.next();
await service.setVolume(75);
// Cleanup
service.dispose();
Targeting Specific Players
// Listen to specific player
service.listenToMetadata('spotify');
// Send command to specific player
await service.play('vlc');
await service.next('spotify');
Example App
A complete example app is included in the example/ directory. To run it:
cd example
flutter run -d linux
The example demonstrates:
- Installation checking
- Player detection and switching
- All playback controls (play/pause/next/previous)
- Volume control with external sync
- Shuffle and loop controls
- Multi-player management
- Error handling
- Real-time metadata updates
- Automatic player switching
Troubleshooting
"playerctl is not installed"
Install playerctl using your distribution's package manager (see Requirements section).
"No active media players found"
Start a media player that supports MPRIS (like Spotify, VLC, Firefox, Chromium, etc.) and play some media.
Commands not working
Some players may not support all MPRIS commands. Check the player's MPRIS implementation.
Stream not updating
The plugin has triple-layer synchronization. If real-time updates stop, periodic refresh (every 3 seconds) will continue to update state.
Glitching when switching players
The plugin includes debouncing to prevent rapid consecutive switches. If issues persist, check debug output for timer lifecycle messages.
Supported Players
Any MPRIS-compatible media player, including:
- Spotify
- VLC
- Firefox
- Chromium/Chrome
- MPV
- Audacious
- Rhythmbox
- And many more...
License
This project is licensed under the MIT License - see the LICENSE file for details.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Credits
Built with:
Libraries
- core/exceptions
- Custom exceptions for playerctl operations
- core/logger
- core/media_player_manager
- core/player_state
- interfaces/playerctl_interfaces
- models/media_info
- playerctl
- services/album_art_server
- services/command_executor
- services/metadata_provider
- services/playback_controller
- services/player_detector
- services/playerctl_service
- services/system_checker
- services/volume_controller