feat: re-do os media controls

This commit is contained in:
edde746
2025-11-08 19:53:54 +01:00
parent 260681aa24
commit 2d706956da
13 changed files with 189 additions and 654 deletions
+1 -19
View File
@@ -3,8 +3,7 @@
<!-- Internet access permissions -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- Audio service permissions -->
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- Media session permissions for OS media controls -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
@@ -45,23 +44,6 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<!-- Audio service -->
<service android:name="com.ryanheise.audioservice.AudioService"
android:foregroundServiceType="mediaPlayback"
android:exported="true" tools:ignore="Instantiatable">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<!-- Media button receiver -->
<receiver android:name="com.ryanheise.audioservice.MediaButtonReceiver"
android:exported="true" tools:ignore="Instantiatable">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
@@ -1,5 +1,5 @@
package com.edde746.plezy
import com.ryanheise.audioservice.AudioServiceActivity
import io.flutter.embedding.android.FlutterActivity
class MainActivity : AudioServiceActivity()
class MainActivity : FlutterActivity()
+6 -13
View File
@@ -1,14 +1,11 @@
PODS:
- audio_service (0.0.1):
- Flutter
- FlutterMacOS
- audio_session (0.0.1):
- Flutter
- Flutter (1.0.0)
- media_kit_libs_ios_video (1.0.4):
- Flutter
- media_kit_video (0.0.1):
- Flutter
- os_media_controls (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
@@ -28,11 +25,10 @@ PODS:
- Flutter
DEPENDENCIES:
- audio_service (from `.symlinks/plugins/audio_service/darwin`)
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- Flutter (from `Flutter`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
- os_media_controls (from `.symlinks/plugins/os_media_controls/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
@@ -42,16 +38,14 @@ DEPENDENCIES:
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
EXTERNAL SOURCES:
audio_service:
:path: ".symlinks/plugins/audio_service/darwin"
audio_session:
:path: ".symlinks/plugins/audio_session/ios"
Flutter:
:path: Flutter
media_kit_libs_ios_video:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_video:
:path: ".symlinks/plugins/media_kit_video/ios"
os_media_controls:
:path: ".symlinks/plugins/os_media_controls/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
@@ -68,11 +62,10 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
os_media_controls: 86dceab6245a5325af90fc0fdebe243c42d789b4
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
+13
View File
@@ -1,5 +1,6 @@
import Flutter
import UIKit
import AVFoundation
@main
@objc class AppDelegate: FlutterAppDelegate {
@@ -8,6 +9,18 @@ import UIKit
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
// Configure audio session for media playback
do {
let session = AVAudioSession.sharedInstance()
try session.setCategory(.playback, mode: .default)
try session.setActive(true)
} catch {
print("Failed to configure audio session: \(error)")
}
application.beginReceivingRemoteControlEvents()
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
+141 -54
View File
@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:provider/provider.dart';
import 'package:os_media_controls/os_media_controls.dart';
import '../models/plex_metadata.dart';
import '../models/plex_user_profile.dart';
import '../providers/plex_client_provider.dart';
@@ -14,7 +15,6 @@ import '../widgets/video_controls/video_controls.dart';
import '../utils/language_codes.dart';
import '../utils/app_logger.dart';
import '../services/settings_service.dart';
import '../services/audio_service_manager.dart';
import '../utils/orientation_helper.dart';
import '../utils/video_player_navigation.dart';
import '../utils/platform_detector.dart';
@@ -56,6 +56,8 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
StreamSubscription<String>? _errorSubscription;
StreamSubscription<bool>? _playingSubscription;
StreamSubscription<bool>? _completedSubscription;
StreamSubscription<Duration>? _positionSubscription;
StreamSubscription<dynamic>? _mediaControlSubscription;
bool _isReplacingWithVideo =
false; // Flag to skip orientation restoration during video-to-video navigation
@@ -215,14 +217,19 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
// Listen to MPV errors
_errorSubscription = player!.stream.error.listen(_onPlayerError);
// Listen to position updates for media controls
_positionSubscription = player!.stream.position.listen((_) {
_updateMediaControlsPosition();
});
// Initialize OS media controls
_initializeMediaControls();
// Start periodic progress updates
_startProgressTracking();
// Load next/previous episodes
_loadAdjacentEpisodes();
// Initialize audio service for OS media controls
_initializeAudioService();
} catch (e) {
appLogger.e('Failed to initialize player', error: e);
if (mounted) {
@@ -279,52 +286,12 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
_nextEpisode = next;
_previousEpisode = previous;
});
// Update audio handler navigation availability
_updateAudioHandlerNavigation();
}
} catch (e) {
// Silently handle errors
}
}
Future<void> _initializeAudioService() async {
try {
if (player == null) return;
final clientProvider = context.plexClient;
final client = clientProvider.client;
if (client == null) return;
final audioManager = AudioServiceManager.instance;
// Initialize audio service (will only happen once)
await audioManager.initialize(
player: player!,
plexServerUrl: client.config.baseUrl,
authToken: client.config.token ?? '',
onSkipToNext: _nextEpisode != null ? _playNext : null,
onSkipToPrevious: _previousEpisode != null ? _playPrevious : null,
);
// Update media item with current content
audioManager.updateMediaItem(widget.metadata);
appLogger.d('Audio service initialized/updated successfully');
} catch (e) {
appLogger.e('Failed to initialize audio service', error: e);
// Continue playback even if audio service fails
}
}
void _updateAudioHandlerNavigation() {
final audioManager = AudioServiceManager.instance;
audioManager.updateNavigation(
onNext: _nextEpisode != null ? _playNext : null,
onPrevious: _previousEpisode != null ? _playPrevious : null,
);
}
Future<void> _startPlayback() async {
try {
final clientProvider = context.plexClient;
@@ -519,17 +486,11 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
_completedSubscription?.cancel();
_logSubscription?.cancel();
_errorSubscription?.cancel();
_positionSubscription?.cancel();
_mediaControlSubscription?.cancel();
// Handle audio service based on navigation intent
if (_isReplacingWithVideo) {
// Switching to another episode: pause but keep notification visible
// The new VideoPlayerScreen will update the notification
AudioServiceManager.instance.pause();
} else {
// Truly exiting player: clear notification but keep singleton alive
// This allows controls to reappear when playing again
AudioServiceManager.instance.clearNotification();
}
// Clear OS media controls completely
OsMediaControls.clear();
// Send final stopped state
_sendProgress('stopped');
@@ -1123,6 +1084,9 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
void _onPlayingStateChanged(bool isPlaying) {
// Send timeline update when playback state changes
_sendProgress(isPlaying ? 'playing' : 'paused');
// Update OS media controls playback state
_updateMediaControlsPlaybackState();
}
void _sendProgress(String state) {
@@ -1185,6 +1149,129 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
appLogger.e('[MPV ERROR] $error');
}
// OS Media Controls Integration
void _initializeMediaControls() async {
// Listen to media control events
_mediaControlSubscription = OsMediaControls.controlEvents.listen((event) {
if (event is PlayEvent) {
player?.play();
} else if (event is PauseEvent) {
player?.pause();
} else if (event is SeekEvent) {
player?.seek(event.position);
} else if (event is NextTrackEvent) {
if (_nextEpisode != null) {
_playNext();
}
} else if (event is PreviousTrackEvent) {
if (_previousEpisode != null) {
_playPrevious();
}
}
});
// Enable/disable next/previous track controls based on content type
final isEpisode = widget.metadata.type.toLowerCase() == 'episode';
if (isEpisode) {
// Enable next/previous track controls for episodes
await OsMediaControls.enableControls([
MediaControl.next,
MediaControl.previous,
]);
} else {
// Disable next/previous track controls for movies
await OsMediaControls.disableControls([
MediaControl.next,
MediaControl.previous,
]);
}
// Set initial metadata
await _updateMediaMetadata();
}
Future<void> _updateMediaMetadata() async {
if (!mounted) {
appLogger.w('Cannot update media metadata: widget not mounted');
return;
}
final metadata = widget.metadata;
final clientProvider = context.plexClient;
final client = clientProvider.client;
// Get artwork URL
String? artworkUrl;
if (client == null) {
appLogger.w('Cannot get artwork URL for media controls: Plex client is null');
} else {
final thumbUrl = metadata.type.toLowerCase() == 'episode'
? metadata.grandparentThumb ?? metadata.thumb
: metadata.thumb;
if (thumbUrl != null) {
try {
artworkUrl = client.getThumbnailUrl(thumbUrl);
appLogger.d('Artwork URL for media controls: $artworkUrl');
} catch (e) {
appLogger.w('Failed to get artwork URL for media controls', error: e);
}
} else {
appLogger.d('No thumbnail URL available for media controls');
}
}
// Build title/artist based on content type
String title = metadata.title;
String? artist;
String? album;
if (metadata.type.toLowerCase() == 'episode') {
title = metadata.title;
artist = metadata.grandparentTitle; // Show name
if (metadata.parentIndex != null) {
album = 'Season ${metadata.parentIndex}';
}
}
await OsMediaControls.setMetadata(MediaMetadata(
title: title,
artist: artist,
album: album,
duration: metadata.duration != null
? Duration(milliseconds: metadata.duration!)
: null,
artworkUrl: artworkUrl,
));
// Set initial playback state
_updateMediaControlsPlaybackState();
}
void _updateMediaControlsPlaybackState() {
if (player == null) return;
OsMediaControls.setPlaybackState(MediaPlaybackState(
state: player!.state.playing ? PlaybackState.playing : PlaybackState.paused,
position: player!.state.position,
speed: player!.state.rate,
));
}
void _updateMediaControlsPosition() {
if (player == null) return;
// Only update if playing to avoid excessive updates
if (player!.state.playing) {
OsMediaControls.setPlaybackState(MediaPlaybackState(
state: PlaybackState.playing,
position: player!.state.position,
speed: player!.state.rate,
));
}
}
Future<void> _playNext() async {
if (_nextEpisode == null || _isLoadingNext) return;
-186
View File
@@ -1,186 +0,0 @@
import 'package:audio_service/audio_service.dart';
import 'package:media_kit/media_kit.dart';
import '../models/plex_metadata.dart';
import '../utils/app_logger.dart';
import 'media_kit_audio_handler.dart';
/// Global singleton manager for audio service
/// Ensures AudioService.init() is only called once and the handler is reused
class AudioServiceManager {
static AudioServiceManager? _instance;
MediaKitAudioHandler? _handler;
bool _isInitialized = false;
bool _isInitializing = false;
AudioServiceManager._();
static AudioServiceManager get instance {
_instance ??= AudioServiceManager._();
return _instance!;
}
/// Initialize the audio service with the given player and configuration
/// This should only be called once - subsequent calls will reuse the existing handler
Future<void> initialize({
required Player player,
required String plexServerUrl,
required String authToken,
Future<void> Function()? onSkipToNext,
Future<void> Function()? onSkipToPrevious,
}) async {
// If already initialized, update the player reference and callbacks
if (_isInitialized && _handler != null) {
appLogger.d('Audio service already initialized, updating player reference and callbacks');
// Update the player reference (critical for episode switching)
await _handler!.updatePlayer(player);
// Update navigation callbacks
_handler!.onSkipToNext = onSkipToNext;
_handler!.onSkipToPrevious = onSkipToPrevious;
return;
}
// Prevent concurrent initialization
if (_isInitializing) {
appLogger.d('Audio service initialization already in progress, waiting...');
// Wait for initialization to complete
while (_isInitializing) {
await Future.delayed(const Duration(milliseconds: 100));
}
return;
}
try {
_isInitializing = true;
appLogger.d('Initializing audio service for the first time');
final handler = await AudioService.init(
builder: () => MediaKitAudioHandler(
player: player,
plexServerUrl: plexServerUrl,
authToken: authToken,
onSkipToNext: onSkipToNext,
onSkipToPrevious: onSkipToPrevious,
),
config: const AudioServiceConfig(
androidNotificationChannelId: 'com.edde746.plezy.audio',
androidNotificationChannelName: 'Plezy Playback',
androidNotificationOngoing: true,
androidShowNotificationBadge: true,
),
);
_handler = handler;
_isInitialized = true;
appLogger.d('Audio service initialized successfully');
} catch (e) {
appLogger.e('Failed to initialize audio service', error: e);
rethrow;
} finally {
_isInitializing = false;
}
}
/// Update the current media item being played
void updateMediaItem(PlexMetadata metadata) {
if (_handler == null) {
appLogger.w('Cannot update media item: audio handler not initialized');
return;
}
_handler!.updateCurrentMediaItem(metadata);
}
/// Update navigation callbacks for next/previous episode
void updateNavigation({
Future<void> Function()? onNext,
Future<void> Function()? onPrevious,
}) {
if (_handler == null) {
appLogger.w('Cannot update navigation: audio handler not initialized');
return;
}
_handler!.updateNavigationCallbacks(
onNext: onNext,
onPrevious: onPrevious,
);
}
/// Pause playback but keep the media session active
Future<void> pause() async {
if (_handler == null) {
appLogger.w('Cannot pause: audio handler not initialized');
return;
}
await _handler!.pause();
}
/// Clear the notification but keep the audio service singleton alive
/// This is used when exiting the video player but not closing the app
Future<void> clearNotification() async {
if (_handler == null) {
appLogger.w('Cannot clear notification: audio handler not initialized');
return;
}
appLogger.d('Clearing notification while keeping audio service alive');
// Broadcast idle state to remove notification
_handler!.playbackState.add(PlaybackState(
processingState: AudioProcessingState.idle,
playing: false,
controls: [],
systemActions: const {},
updatePosition: Duration.zero,
bufferedPosition: Duration.zero,
speed: 1.0,
));
// Clear media item
_handler!.mediaItem.add(null);
// IMPORTANT: DON'T reset _isInitialized or _handler
// Keep the singleton alive for next playback
appLogger.d('Notification cleared, audio service remains initialized');
}
/// Stop playback completely and remove the notification
/// This should only be called when the app is closing or user explicitly stops
Future<void> shutdown() async {
if (_handler == null) {
appLogger.w('Cannot shutdown: audio handler not initialized');
return;
}
appLogger.d('Shutting down audio service and removing notification');
// Broadcast idle state to remove notification
_handler!.playbackState.add(PlaybackState(
processingState: AudioProcessingState.idle,
playing: false,
controls: [],
systemActions: const {},
updatePosition: Duration.zero,
bufferedPosition: Duration.zero,
speed: 1.0,
));
// Clear media item
_handler!.mediaItem.add(null);
// Dispose the handler
await _handler!.dispose();
_handler = null;
_isInitialized = false;
appLogger.d('Audio service shutdown complete');
}
/// Check if the audio service is initialized
bool get isInitialized => _isInitialized;
/// Get the current handler (for advanced use cases)
MediaKitAudioHandler? get handler => _handler;
}
-314
View File
@@ -1,314 +0,0 @@
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:media_kit/media_kit.dart';
import 'package:rxdart/rxdart.dart';
import '../models/plex_metadata.dart';
import '../utils/app_logger.dart';
/// Audio handler that bridges media_kit player with OS media controls
class MediaKitAudioHandler extends BaseAudioHandler with SeekHandler {
Player _player;
final String plexServerUrl;
final String authToken;
// Getter for player
Player get player => _player;
// Callback functions for episode navigation
Future<void> Function()? onSkipToNext;
Future<void> Function()? onSkipToPrevious;
// Stream subscriptions
StreamSubscription<bool>? _playingSubscription;
StreamSubscription<Duration>? _positionSubscription;
StreamSubscription<Duration>? _durationSubscription;
StreamSubscription<Duration>? _bufferSubscription;
// Queue management
final BehaviorSubject<List<MediaItem>> _queueSubject = BehaviorSubject.seeded([]);
MediaKitAudioHandler({
required Player player,
required this.plexServerUrl,
required this.authToken,
this.onSkipToNext,
this.onSkipToPrevious,
}) : _player = player {
_init();
}
void _init() {
// Map player state to playback state
_playingSubscription = _player.stream.playing.listen((isPlaying) {
_updatePlaybackState();
});
_positionSubscription = _player.stream.position.listen((_) {
_updatePlaybackState();
});
_durationSubscription = _player.stream.duration.listen((_) {
_updatePlaybackState();
});
_bufferSubscription = _player.stream.buffer.listen((_) {
_updatePlaybackState();
});
// Set initial state
_updatePlaybackState();
}
/// Update media item with current playing content
void updateCurrentMediaItem(PlexMetadata metadata) {
final artworkUrl = _getArtworkUrl(metadata);
final newMediaItem = MediaItem(
id: metadata.ratingKey,
title: metadata.title,
album: _getAlbumName(metadata),
artist: _getArtistName(metadata),
duration: metadata.duration != null
? Duration(milliseconds: metadata.duration!)
: null,
artUri: artworkUrl != null ? Uri.parse(artworkUrl) : null,
extras: {
'ratingKey': metadata.ratingKey,
'type': metadata.type,
},
);
mediaItem.add(newMediaItem);
appLogger.d('Updated media item: ${metadata.title}');
}
/// Update navigation callbacks
void updateNavigationCallbacks({
Future<void> Function()? onNext,
Future<void> Function()? onPrevious,
}) {
onSkipToNext = onNext;
onSkipToPrevious = onPrevious;
_updatePlaybackState();
}
/// Update the queue with episodes
void updateEpisodeQueue(List<PlexMetadata> episodes, int currentIndex) {
final items = episodes.map((episode) {
final artworkUrl = _getArtworkUrl(episode);
return MediaItem(
id: episode.ratingKey,
title: episode.title,
album: _getAlbumName(episode),
artist: _getArtistName(episode),
duration: episode.duration != null
? Duration(milliseconds: episode.duration!)
: null,
artUri: artworkUrl != null ? Uri.parse(artworkUrl) : null,
extras: {
'ratingKey': episode.ratingKey,
'type': episode.type,
},
);
}).toList();
_queueSubject.add(items);
queue.add(items);
if (currentIndex >= 0 && currentIndex < items.length) {
mediaItem.add(items[currentIndex]);
}
appLogger.d('Updated queue with ${items.length} episodes, current index: $currentIndex');
}
String? _getArtworkUrl(PlexMetadata metadata) {
String? thumbPath;
// For episodes, prefer show poster over episode thumbnail
if (metadata.type.toLowerCase() == 'episode') {
thumbPath = metadata.grandparentThumb ?? metadata.thumb;
} else {
thumbPath = metadata.thumb;
}
if (thumbPath == null) return null;
// Build full URL with authentication
return '$plexServerUrl$thumbPath?X-Plex-Token=$authToken';
}
String _getAlbumName(PlexMetadata metadata) {
// For episodes: "Show Name - Season X"
if (metadata.type.toLowerCase() == 'episode') {
final showName = metadata.grandparentTitle ?? '';
final seasonNum = metadata.parentIndex;
if (seasonNum != null) {
return '$showName - Season $seasonNum';
}
return showName;
}
// For movies: studio or year
return metadata.studio ?? metadata.year?.toString() ?? '';
}
String _getArtistName(PlexMetadata metadata) {
// For episodes: show episode info
if (metadata.type.toLowerCase() == 'episode') {
final seasonNum = metadata.parentIndex;
final episodeNum = metadata.index;
if (seasonNum != null && episodeNum != null) {
return 'S${seasonNum.toString().padLeft(2, '0')}E${episodeNum.toString().padLeft(2, '0')}';
}
}
return '';
}
void _updatePlaybackState() {
final isPlaying = _player.state.playing;
final position = _player.state.position;
final bufferedPosition = _player.state.buffer;
// Determine processing state
AudioProcessingState processingState;
if (_player.state.buffering) {
processingState = AudioProcessingState.buffering;
} else if (_player.state.completed) {
processingState = AudioProcessingState.completed;
} else {
processingState = AudioProcessingState.ready;
}
// Build controls based on state and available navigation
final controls = <MediaControl>[
if (onSkipToPrevious != null) MediaControl.skipToPrevious,
MediaControl.rewind,
if (isPlaying) MediaControl.pause else MediaControl.play,
MediaControl.fastForward,
if (onSkipToNext != null) MediaControl.skipToNext,
];
playbackState.add(PlaybackState(
controls: controls,
systemActions: const {
MediaAction.seek,
MediaAction.seekForward,
MediaAction.seekBackward,
},
androidCompactActionIndices: [
if (onSkipToPrevious != null) 0,
onSkipToPrevious != null ? 2 : 1, // Play/Pause button
if (onSkipToNext != null)
onSkipToPrevious != null ? 4 : 3,
].where((i) => i < controls.length).toList(),
processingState: processingState,
playing: isPlaying,
updatePosition: position,
bufferedPosition: bufferedPosition,
speed: 1.0,
queueIndex: 0, // TODO: Update based on actual queue position
));
}
@override
Future<void> play() async {
appLogger.d('Audio handler: play');
await _player.play();
}
@override
Future<void> pause() async {
appLogger.d('Audio handler: pause');
await _player.pause();
}
@override
Future<void> seek(Duration position) async {
appLogger.d('Audio handler: seek to ${position.inSeconds}s');
await _player.seek(position);
}
@override
Future<void> stop() async {
appLogger.d('Audio handler: stop');
await _player.stop();
}
@override
Future<void> fastForward() async {
appLogger.d('Audio handler: fast forward');
final newPosition = _player.state.position + const Duration(seconds: 10);
await _player.seek(newPosition);
}
@override
Future<void> rewind() async {
appLogger.d('Audio handler: rewind');
final newPosition = _player.state.position - const Duration(seconds: 10);
await _player.seek(newPosition > Duration.zero ? newPosition : Duration.zero);
}
@override
Future<void> skipToNext() async {
appLogger.d('Audio handler: skip to next');
if (onSkipToNext != null) {
await onSkipToNext!();
}
}
@override
Future<void> skipToPrevious() async {
appLogger.d('Audio handler: skip to previous');
if (onSkipToPrevious != null) {
await onSkipToPrevious!();
}
}
/// Update the player reference when switching episodes
/// This ensures the handler stays in sync with the current player instance
Future<void> updatePlayer(Player newPlayer) async {
appLogger.d('Audio handler: updating player reference');
// Cancel old subscriptions
await _playingSubscription?.cancel();
await _positionSubscription?.cancel();
await _durationSubscription?.cancel();
await _bufferSubscription?.cancel();
// Update player reference
_player = newPlayer;
// Create new subscriptions with the new player
_playingSubscription = _player.stream.playing.listen((isPlaying) {
_updatePlaybackState();
});
_positionSubscription = _player.stream.position.listen((_) {
_updatePlaybackState();
});
_durationSubscription = _player.stream.duration.listen((_) {
_updatePlaybackState();
});
_bufferSubscription = _player.stream.buffer.listen((_) {
_updatePlaybackState();
});
// Update playback state immediately
_updatePlaybackState();
appLogger.d('Audio handler: player reference updated successfully');
}
Future<void> dispose() async {
// Cancel subscriptions
await _playingSubscription?.cancel();
await _positionSubscription?.cancel();
await _durationSubscription?.cancel();
await _bufferSubscription?.cancel();
await _queueSubject.close();
}
}
@@ -5,12 +5,11 @@
import FlutterMacOS
import Foundation
import audio_service
import audio_session
import hotkey_manager_macos
import macos_window_utils
import media_kit_libs_macos_video
import media_kit_video
import os_media_controls
import package_info_plus
import path_provider_foundation
import screen_retriever_macos
@@ -22,12 +21,11 @@ import wakelock_plus
import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin"))
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
OsMediaControlsPlugin.register(with: registry.registrar(forPlugin: "OsMediaControlsPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
+6 -13
View File
@@ -1,9 +1,4 @@
PODS:
- audio_service (0.0.1):
- Flutter
- FlutterMacOS
- audio_session (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0)
- HotKey (0.2.1)
- hotkey_manager_macos (0.0.1):
@@ -15,6 +10,8 @@ PODS:
- FlutterMacOS
- media_kit_video (0.0.1):
- FlutterMacOS
- os_media_controls (0.0.1):
- FlutterMacOS
- package_info_plus (0.0.1):
- FlutterMacOS
- path_provider_foundation (0.0.1):
@@ -38,13 +35,12 @@ PODS:
- FlutterMacOS
DEPENDENCIES:
- audio_service (from `Flutter/ephemeral/.symlinks/plugins/audio_service/darwin`)
- audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`)
- macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`)
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
- media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
- os_media_controls (from `Flutter/ephemeral/.symlinks/plugins/os_media_controls/macos`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`)
@@ -60,10 +56,6 @@ SPEC REPOS:
- HotKey
EXTERNAL SOURCES:
audio_service:
:path: Flutter/ephemeral/.symlinks/plugins/audio_service/darwin
audio_session:
:path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos
FlutterMacOS:
:path: Flutter/ephemeral
hotkey_manager_macos:
@@ -74,6 +66,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos
media_kit_video:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos
os_media_controls:
:path: Flutter/ephemeral/.symlinks/plugins/os_media_controls/macos
package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
path_provider_foundation:
@@ -94,14 +88,13 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
SPEC CHECKSUMS:
audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd
audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
hotkey_manager_macos: a4317849af96d2430fa89944d3c58977ca089fbe
macos_window_utils: 23f54331a0fd51eea9e0ed347253bf48fd379d1d
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758
os_media_controls: c07c04c4afdf59dda0a3f398457a46823c4ce0ed
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
+11 -43
View File
@@ -41,39 +41,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.13.0"
audio_service:
dependency: "direct main"
description:
path: audio_service
ref: "4ca42f391183854095188a5a10fa6b59cd641e68"
resolved-ref: "4ca42f391183854095188a5a10fa6b59cd641e68"
url: "https://github.com/marckornberger/audio_service.git"
source: git
version: "0.18.18"
audio_service_platform_interface:
dependency: transitive
description:
name: audio_service_platform_interface
sha256: "6283782851f6c8b501b60904a32fc7199dc631172da0629d7301e66f672ab777"
url: "https://pub.dev"
source: hosted
version: "0.1.3"
audio_service_web:
dependency: transitive
description:
name: audio_service_web
sha256: b8ea9243201ee53383157fbccf13d5d2a866b5dda922ec19d866d1d5d70424df
url: "https://pub.dev"
source: hosted
version: "0.1.4"
audio_session:
dependency: "direct main"
description:
name: audio_session
sha256: "2b7fff16a552486d078bfc09a8cde19f426dc6d6329262b684182597bec5b1ac"
url: "https://pub.dev"
source: hosted
version: "0.1.25"
boolean_selector:
dependency: transitive
description:
@@ -433,14 +400,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
js:
dependency: transitive
description:
name: js
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
json_annotation:
dependency: "direct main"
description:
@@ -632,6 +591,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
os_media_controls:
dependency: "direct main"
description:
path: "."
ref: main
resolved-ref: "95ee0786deca2b6a45ac227df20d1055c9204f1a"
url: "https://github.com/edde746/os-media-controls"
source: git
version: "0.0.1"
package_config:
dependency: transitive
description:
@@ -793,7 +761,7 @@ packages:
source: hosted
version: "4.1.0"
rxdart:
dependency: "direct main"
dependency: transitive
description:
name: rxdart
sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
@@ -1278,5 +1246,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.9.0 <4.0.0"
dart: ">=3.9.2 <4.0.0"
flutter: ">=3.35.0"
+3 -6
View File
@@ -26,13 +26,10 @@ dependencies:
hotkey_manager: ^0.2.3
flex_color_picker: ^3.6.0
qr_flutter: ^4.1.0
audio_session: ^0.1.25
rxdart: ^0.27.2
audio_service:
os_media_controls:
git:
url: https://github.com/marckornberger/audio_service.git
ref: 4ca42f391183854095188a5a10fa6b59cd641e68
path: audio_service
url: https://github.com/edde746/os-media-controls
ref: main
dependency_overrides:
media_kit:
@@ -9,6 +9,7 @@
#include <hotkey_manager_windows/hotkey_manager_windows_plugin_c_api.h>
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
#include <media_kit_video/media_kit_video_plugin_c_api.h>
#include <os_media_controls/os_media_controls_plugin.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <volume_controller/volume_controller_plugin_c_api.h>
@@ -21,6 +22,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi"));
MediaKitVideoPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
OsMediaControlsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("OsMediaControlsPlugin"));
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
+1
View File
@@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
hotkey_manager_windows
media_kit_libs_windows_video
media_kit_video
os_media_controls
screen_retriever_windows
url_launcher_windows
volume_controller