refactor: rewrite os media controls

# Conflicts:
#	lib/screens/video_player_screen.dart
#	lib/services/media_service_manager.dart
#	pubspec.yaml
This commit is contained in:
edde746
2025-11-08 10:04:51 +01:00
parent 0d4f7a9e9f
commit 6228eb2690
9 changed files with 507 additions and 416 deletions
+54 -51
View File
@@ -14,7 +14,7 @@ import '../widgets/video_controls/video_controls.dart';
import '../utils/language_codes.dart';
import '../utils/app_logger.dart';
import '../services/settings_service.dart';
import '../services/media_service_manager.dart';
import '../services/audio_service_manager.dart';
import '../utils/orientation_helper.dart';
import '../utils/video_player_navigation.dart';
import '../utils/platform_detector.dart';
@@ -166,9 +166,6 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
final savedVolume = settingsService.getVolume();
player!.setVolume(savedVolume);
// Update media service manager with new player
await _updateMediaService();
// Notify that player is ready
if (mounted) {
setState(() {
@@ -223,6 +220,9 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
// 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) {
@@ -233,39 +233,6 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
}
}
Future<void> _updateMediaService() async {
if (!mounted) return;
try {
final mediaService = MediaServiceManager.instance;
// Build thumbnail URL if available
String? thumbnailUrl;
final clientProvider = context.plexClient;
final client = clientProvider.client;
if (widget.metadata.thumb != null && client != null) {
final baseUrl = client.config.baseUrl;
final token = client.config.token;
if (token != null) {
thumbnailUrl = '$baseUrl${widget.metadata.thumb}?X-Plex-Token=$token';
}
}
// CRITICAL: Set mediaItem FIRST before updating player
// Android requires mediaItem to exist before playbackState broadcasts
mediaService.updateMediaItem(widget.metadata, thumbnailUrl);
if (!mounted) return;
await mediaService.updatePlayer(
player: player,
onNext: _playNext,
onPrevious: _playPrevious,
);
} catch (e) {
appLogger.w('Failed to update media service', error: e);
}
}
Future<void> _loadAdjacentEpisodes() async {
if (widget.metadata.type.toLowerCase() != 'episode') {
@@ -295,9 +262,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
widget.metadata.ratingKey,
loopQueue: loopQueue,
);
previous = playbackState.getPreviousEpisode(
widget.metadata.ratingKey,
);
previous = playbackState.getPreviousEpisode(widget.metadata.ratingKey);
} else {
// Use chronological order even in shuffle mode
next = await client.findAdjacentEpisode(widget.metadata, 1);
@@ -315,17 +280,51 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
_previousEpisode = previous;
});
// Update media service navigation controls
MediaServiceManager.instance.updateNavigationActions(
hasNext: _nextEpisode != null,
hasPrevious: _previousEpisode != null,
);
// 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;
@@ -446,10 +445,6 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
// Start playback after seeking
await player!.play();
// Force media service state update to trigger Android notification
// This ensures the notification appears even if streams don't fire reliably
MediaServiceManager.instance.forceStateUpdate();
// Wait for tracks to be loaded, then apply preferred tracks
_waitForTracksAndApply();
} else {
@@ -525,8 +520,16 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
_logSubscription?.cancel();
_errorSubscription?.cancel();
// Stop media service and clear OS controls
MediaServiceManager.instance.stop();
// 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();
}
// Send final stopped state
_sendProgress('stopped');