feat: os media controls

This commit is contained in:
edde746
2025-11-06 15:14:40 +01:00
parent 2a6098c3ba
commit 47f5454f61
27 changed files with 609 additions and 11 deletions
+26
View File
@@ -3,6 +3,11 @@
<!-- Internet access permissions --> <!-- Internet access permissions -->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<!-- Permissions for audio_service (OS media controls) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- Explicitly remove media permissions that media_kit may add --> <!-- Explicitly remove media permissions that media_kit may add -->
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" tools:node="remove" /> <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" tools:node="remove" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" tools:node="remove" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" tools:node="remove" />
@@ -40,6 +45,27 @@
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
<!-- Audio service for OS media controls -->
<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 for headset controls -->
<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> </application>
<!-- Required to query activities that can process text, see: <!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and https://developer.android.com/training/package-visibility and
@@ -1,5 +1,5 @@
package com.edde746.plezy package com.edde746.plezy
import io.flutter.embedding.android.FlutterActivity import com.ryanheise.audioservice.AudioServiceFragmentActivity
class MainActivity : FlutterActivity() class MainActivity : AudioServiceFragmentActivity()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+13
View File
@@ -1,4 +1,9 @@
PODS: PODS:
- audio_service (0.0.1):
- Flutter
- FlutterMacOS
- audio_session (0.0.1):
- Flutter
- Flutter (1.0.0) - Flutter (1.0.0)
- media_kit_libs_ios_video (1.0.4): - media_kit_libs_ios_video (1.0.4):
- Flutter - Flutter
@@ -23,6 +28,8 @@ PODS:
- Flutter - Flutter
DEPENDENCIES: DEPENDENCIES:
- audio_service (from `.symlinks/plugins/audio_service/darwin`)
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
@@ -35,6 +42,10 @@ DEPENDENCIES:
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
EXTERNAL SOURCES: EXTERNAL SOURCES:
audio_service:
:path: ".symlinks/plugins/audio_service/darwin"
audio_session:
:path: ".symlinks/plugins/audio_session/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
media_kit_libs_ios_video: media_kit_libs_ios_video:
@@ -57,6 +68,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock_plus/ios" :path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
+4
View File
@@ -39,6 +39,10 @@
<string>This app needs to connect to your Plex Media Server on your local network.</string> <string>This app needs to connect to your Plex Media Server on your local network.</string>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
+4
View File
@@ -13,6 +13,7 @@ import 'services/macos_titlebar_service.dart';
import 'services/fullscreen_state_manager.dart'; import 'services/fullscreen_state_manager.dart';
import 'services/update_service.dart'; import 'services/update_service.dart';
import 'services/settings_service.dart'; import 'services/settings_service.dart';
import 'services/media_service_manager.dart';
import 'providers/user_profile_provider.dart'; import 'providers/user_profile_provider.dart';
import 'providers/plex_client_provider.dart'; import 'providers/plex_client_provider.dart';
import 'providers/theme_provider.dart'; import 'providers/theme_provider.dart';
@@ -42,6 +43,9 @@ void main() async {
// Initialize MediaKit // Initialize MediaKit
MediaKit.ensureInitialized(); MediaKit.ensureInitialized();
// Initialize OS media controls
await MediaServiceManager.instance.initialize();
// Note: Orientation will be set dynamically based on device type in MainApp // Note: Orientation will be set dynamically based on device type in MainApp
await StorageService.getInstance(); await StorageService.getInstance();
+98 -9
View File
@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
import 'package:media_kit/media_kit.dart'; import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart'; import 'package:media_kit_video/media_kit_video.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:audio_session/audio_session.dart';
import '../models/plex_metadata.dart'; import '../models/plex_metadata.dart';
import '../models/plex_user_profile.dart'; import '../models/plex_user_profile.dart';
import '../providers/plex_client_provider.dart'; import '../providers/plex_client_provider.dart';
@@ -13,6 +14,7 @@ import '../widgets/video_controls/video_controls.dart';
import '../utils/language_codes.dart'; import '../utils/language_codes.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../services/settings_service.dart'; import '../services/settings_service.dart';
import '../services/media_service_manager.dart';
import '../utils/orientation_helper.dart'; import '../utils/orientation_helper.dart';
import '../utils/video_player_navigation.dart'; import '../utils/video_player_navigation.dart';
import '../utils/platform_detector.dart'; import '../utils/platform_detector.dart';
@@ -164,6 +166,12 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
final savedVolume = settingsService.getVolume(); final savedVolume = settingsService.getVolume();
player!.setVolume(savedVolume); player!.setVolume(savedVolume);
// Initialize audio session for proper OS integration
await _initializeAudioSession();
// Update media service manager with new player
await _updateMediaService();
// Notify that player is ready // Notify that player is ready
if (mounted) { if (mounted) {
setState(() { setState(() {
@@ -228,6 +236,82 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
} }
} }
Future<void> _initializeAudioSession() async {
try {
final session = await AudioSession.instance;
await session.configure(const AudioSessionConfiguration(
avAudioSessionCategory: AVAudioSessionCategory.playback,
avAudioSessionMode: AVAudioSessionMode.moviePlayback,
androidAudioAttributes: AndroidAudioAttributes(
contentType: AndroidAudioContentType.movie,
usage: AndroidAudioUsage.media,
),
androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
));
// Handle interruptions (phone calls, other apps, etc.)
session.interruptionEventStream.listen((event) {
if (event.begin) {
// Pause on interruption
player?.pause();
appLogger.i('Playback paused due to interruption');
} else {
// Resume after interruption if needed (user can manually play)
switch (event.type) {
case AudioInterruptionType.pause:
case AudioInterruptionType.duck:
// Don't auto-resume for these types
break;
case AudioInterruptionType.unknown:
break;
}
}
});
// Handle audio becoming noisy (headphones unplugged)
session.becomingNoisyEventStream.listen((_) {
player?.pause();
appLogger.i('Playback paused due to audio becoming noisy (headphones unplugged?)');
});
} catch (e) {
appLogger.w('Failed to configure audio session', error: e);
}
}
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 { Future<void> _loadAdjacentEpisodes() async {
if (widget.metadata.type.toLowerCase() != 'episode') { if (widget.metadata.type.toLowerCase() != 'episode') {
return; return;
@@ -260,6 +344,12 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
_nextEpisode = next; _nextEpisode = next;
_previousEpisode = previous; _previousEpisode = previous;
}); });
// Update media service navigation controls
MediaServiceManager.instance.updateNavigationActions(
hasNext: _nextEpisode != null,
hasPrevious: _previousEpisode != null,
);
} }
} catch (e) { } catch (e) {
// Silently handle errors // Silently handle errors
@@ -386,6 +476,10 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
// Start playback after seeking // Start playback after seeking
await player!.play(); 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 // Wait for tracks to be loaded, then apply preferred tracks
_waitForTracksAndApply(); _waitForTracksAndApply();
} else { } else {
@@ -427,21 +521,13 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
setState(() { setState(() {
_boxFitMode = (_boxFitMode + 1) % 3; _boxFitMode = (_boxFitMode + 1) % 3;
}); });
final modes = [
'contain (letterbox)',
'cover (fill screen)',
'fill (stretch)',
];
appLogger.d('BoxFit mode: ${modes[_boxFitMode]}');
} }
/// Toggle between contain and cover modes only (for pinch gesture) /// Toggle between contain and cover modes only (for pinch gesture)
void _toggleContainCover() { void _toggleContainCover() {
setState(() { setState(() {
_boxFitMode = _boxFitMode == 0 ? 1 : 0; // Toggle between 0 and 1 _boxFitMode = _boxFitMode == 0 ? 1 : 0;
}); });
final modes = ['contain (letterbox)', 'cover (fill screen)'];
appLogger.d('BoxFit mode toggled: ${modes[_boxFitMode]}');
} }
/// Get current BoxFit based on mode /// Get current BoxFit based on mode
@@ -469,6 +555,9 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
_logSubscription?.cancel(); _logSubscription?.cancel();
_errorSubscription?.cancel(); _errorSubscription?.cancel();
// Stop media service and clear OS controls
MediaServiceManager.instance.stop();
// Send final stopped state // Send final stopped state
_sendProgress('stopped'); _sendProgress('stopped');
+282
View File
@@ -0,0 +1,282 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:audio_service/audio_service.dart';
import 'package:media_kit/media_kit.dart';
import '../models/plex_metadata.dart';
import '../utils/app_logger.dart';
/// AudioHandler that bridges media_kit Player with OS media controls
class MediaKitAudioHandler extends BaseAudioHandler with SeekHandler {
Player? _player;
VoidCallback? _onNext;
VoidCallback? _onPrevious;
StreamSubscription<bool>? _playingSubscription;
StreamSubscription<Duration>? _positionSubscription;
StreamSubscription<bool>? _completedSubscription;
MediaKitAudioHandler({
required Player? player,
VoidCallback? onNext,
VoidCallback? onPrevious,
}) : _player = player,
_onNext = onNext,
_onPrevious = onPrevious {
if (_player != null) {
_initializeListeners();
}
}
void _initializeListeners() {
if (_player == null) return;
_playingSubscription = _player!.stream.playing.listen((_) {
_broadcastState();
});
_positionSubscription = _player!.stream.position.listen((_) {
_broadcastState();
});
_completedSubscription = _player!.stream.completed.listen((completed) {
if (completed) {
_broadcastState();
}
});
_broadcastState();
}
/// Update the player reference and callbacks (for video changes)
Future<void> updatePlayer({
required Player? player,
VoidCallback? onNext,
VoidCallback? onPrevious,
}) async {
await _cleanupSubscriptions();
// Update player and callbacks
_player = player;
_onNext = onNext;
_onPrevious = onPrevious;
// Set up new subscriptions if player is not null
if (_player != null) {
_initializeListeners();
} else {
// No player, broadcast idle state
_broadcastIdleState();
}
}
/// Clean up current subscriptions
Future<void> _cleanupSubscriptions() async {
await _playingSubscription?.cancel();
await _positionSubscription?.cancel();
await _completedSubscription?.cancel();
_playingSubscription = null;
_positionSubscription = null;
_completedSubscription = null;
}
void _broadcastState() {
if (_player == null || mediaItem.value == null) {
if (_player == null) {
_broadcastIdleState();
}
return;
}
final playing = _player!.state.playing;
final position = _player!.state.position;
final duration = _player!.state.duration;
final rate = _player!.state.rate;
// Determine processing state
AudioProcessingState processingState;
if (_player!.state.completed) {
processingState = AudioProcessingState.completed;
} else if (duration.inMilliseconds > 0) {
processingState = AudioProcessingState.ready;
} else {
processingState = AudioProcessingState.loading;
}
// Build control list
final controls = <MediaControl>[
if (_onPrevious != null) MediaControl.skipToPrevious,
playing ? MediaControl.pause : MediaControl.play,
MediaControl.stop,
if (_onNext != null) MediaControl.skipToNext,
];
final compactIndices = _calculateCompactActionIndices(controls);
playbackState.add(PlaybackState(
controls: controls,
systemActions: const {
MediaAction.seek,
MediaAction.seekForward,
MediaAction.seekBackward,
},
androidCompactActionIndices: compactIndices,
playing: playing,
updatePosition: position,
speed: rate,
processingState: processingState,
));
}
void _broadcastIdleState() {
playbackState.add(PlaybackState(
controls: [],
systemActions: const {},
playing: false,
processingState: AudioProcessingState.idle,
));
}
/// Calculate compact action indices for Android notification
/// Returns indices for the most important controls to show in compact view
List<int> _calculateCompactActionIndices(List<MediaControl> controls) {
final indices = <int>[];
// Find indices of key controls
int? previousIndex;
int? playPauseIndex;
int? nextIndex;
for (int i = 0; i < controls.length; i++) {
if (controls[i] == MediaControl.skipToPrevious) {
previousIndex = i;
} else if (controls[i] == MediaControl.play ||
controls[i] == MediaControl.pause) {
playPauseIndex = i;
} else if (controls[i] == MediaControl.skipToNext) {
nextIndex = i;
}
}
// Build compact indices: Previous (if exists), Play/Pause, Next (if exists)
if (previousIndex != null) indices.add(previousIndex);
if (playPauseIndex != null) indices.add(playPauseIndex);
if (nextIndex != null) indices.add(nextIndex);
// Ensure we have at least the play/pause button
if (indices.isEmpty && playPauseIndex != null) {
indices.add(playPauseIndex);
}
return indices;
}
/// Update the media item shown in OS controls
void setMediaItemFromMetadata(PlexMetadata metadata, String? thumbnailUrl) {
final title = metadata.type.toLowerCase() == 'episode'
? metadata.title
: metadata.title;
final artist = metadata.type.toLowerCase() == 'episode'
? metadata.grandparentTitle ?? metadata.year?.toString()
: metadata.year?.toString();
final album = metadata.type.toLowerCase() == 'episode'
? 'S${metadata.parentIndex} · E${metadata.index} · ${metadata.parentTitle ?? ""}'
: metadata.studio;
final duration = metadata.duration != null
? Duration(milliseconds: metadata.duration!)
: Duration.zero;
mediaItem.add(MediaItem(
id: metadata.ratingKey,
title: title,
artist: artist,
album: album,
duration: duration,
artUri: thumbnailUrl != null ? Uri.parse(thumbnailUrl) : null,
extras: {
'ratingKey': metadata.ratingKey,
'type': metadata.type,
},
));
appLogger.i('Media item updated: $title${artist != null ? " - $artist" : ""}');
}
/// Update whether next/previous actions are available
void updateNavigationActions({bool? hasNext, bool? hasPrevious}) {
_broadcastState();
}
/// Force an immediate state update
/// Use this after playback starts to ensure notification appears
void forceStateUpdate() {
_broadcastState();
}
// BaseAudioHandler implementations
@override
Future<void> play() async {
if (_player == null) return;
await _player!.play();
}
@override
Future<void> pause() async {
if (_player == null) return;
await _player!.pause();
}
@override
Future<void> stop() async {
if (_player != null) {
await _player!.pause();
}
playbackState.add(PlaybackState(
controls: [],
systemActions: const {},
playing: false,
processingState: AudioProcessingState.idle,
));
await super.stop();
}
@override
Future<void> seek(Duration position) async {
if (_player == null) return;
await _player!.seek(position);
}
@override
Future<void> skipToNext() async {
_onNext?.call();
}
@override
Future<void> skipToPrevious() async {
_onPrevious?.call();
}
@override
Future<void> fastForward() async {
if (_player == null) return;
final newPosition = _player!.state.position + const Duration(seconds: 15);
await _player!.seek(newPosition);
}
@override
Future<void> rewind() async {
if (_player == null) return;
final newPosition = _player!.state.position - const Duration(seconds: 15);
await _player!.seek(newPosition > Duration.zero ? newPosition : Duration.zero);
}
Future<void> dispose() async {
appLogger.d('Disposing MediaKitAudioHandler');
await _cleanupSubscriptions();
await stop();
}
}
+121
View File
@@ -0,0 +1,121 @@
import 'package:flutter/foundation.dart';
import 'package:audio_service/audio_service.dart';
import 'package:media_kit/media_kit.dart';
import 'media_kit_audio_handler.dart';
import '../utils/app_logger.dart';
/// Singleton manager for OS media controls integration
/// Manages a single AudioHandler instance for the entire app lifecycle
class MediaServiceManager {
static MediaServiceManager? _instance;
static MediaKitAudioHandler? _audioHandler;
static bool _isInitialized = false;
MediaServiceManager._();
static MediaServiceManager get instance {
_instance ??= MediaServiceManager._();
return _instance!;
}
/// Initialize the audio service once at app startup
Future<void> initialize() async {
if (_isInitialized) {
appLogger.w('MediaServiceManager already initialized');
return;
}
try {
appLogger.i('Initializing MediaServiceManager');
_audioHandler = await AudioService.init(
builder: () => MediaKitAudioHandler(
player: null, // Will be set when first video plays
onNext: null,
onPrevious: null,
),
config: const AudioServiceConfig(
androidNotificationChannelId: 'com.plezy.app.channel.audio',
androidNotificationChannelName: 'Plezy Playback',
androidNotificationOngoing: false,
androidStopForegroundOnPause: true,
androidNotificationIcon: 'drawable/ic_stat_notification',
),
);
_isInitialized = true;
appLogger.i('MediaServiceManager initialized successfully');
} catch (e, stackTrace) {
appLogger.e(
'❌ Failed to initialize MediaServiceManager',
error: e,
stackTrace: stackTrace,
);
// Non-fatal, app can continue without OS media controls
}
}
/// Update the audio handler with a new player and callbacks
Future<void> updatePlayer({
required Player? player,
VoidCallback? onNext,
VoidCallback? onPrevious,
}) async {
if (!_isInitialized || _audioHandler == null) {
appLogger.w('MediaServiceManager not initialized, cannot update player');
return;
}
try {
await _audioHandler!.updatePlayer(
player: player,
onNext: onNext,
onPrevious: onPrevious,
);
} catch (e) {
appLogger.e('Failed to update player', error: e);
}
}
/// Stop playback and clear OS controls
Future<void> stop() async {
if (_audioHandler == null) return;
try {
await _audioHandler!.stop();
} catch (e) {
appLogger.e('Failed to stop audio service', error: e);
}
}
/// Update the media item shown in OS controls
void updateMediaItem(dynamic metadata, String? thumbnailUrl) {
if (_audioHandler == null) return;
try {
_audioHandler!.setMediaItemFromMetadata(metadata, thumbnailUrl);
} catch (e) {
appLogger.e('Failed to update media item', error: e);
}
}
/// Update navigation actions availability
void updateNavigationActions({bool? hasNext, bool? hasPrevious}) {
_audioHandler?.updateNavigationActions(
hasNext: hasNext,
hasPrevious: hasPrevious,
);
}
/// Force an immediate state update to trigger notification
/// Call this after playback starts to ensure Android shows the notification
void forceStateUpdate() {
_audioHandler?.forceStateUpdate();
}
/// Check if the service is initialized
bool get isInitialized => _isInitialized;
/// Get the audio handler (for advanced usage)
MediaKitAudioHandler? get audioHandler => _audioHandler;
}
@@ -5,6 +5,8 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import audio_service
import audio_session
import hotkey_manager_macos import hotkey_manager_macos
import macos_window_utils import macos_window_utils
import media_kit_libs_macos_video import media_kit_libs_macos_video
@@ -20,6 +22,8 @@ import wakelock_plus
import window_manager import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 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")) HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin")) MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
+13
View File
@@ -1,4 +1,9 @@
PODS: PODS:
- audio_service (0.0.1):
- Flutter
- FlutterMacOS
- audio_session (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0) - FlutterMacOS (1.0.0)
- HotKey (0.2.1) - HotKey (0.2.1)
- hotkey_manager_macos (0.0.1): - hotkey_manager_macos (0.0.1):
@@ -33,6 +38,8 @@ PODS:
- FlutterMacOS - FlutterMacOS
DEPENDENCIES: 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`) - FlutterMacOS (from `Flutter/ephemeral`)
- hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`) - hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`)
- macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`) - macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`)
@@ -53,6 +60,10 @@ SPEC REPOS:
- HotKey - HotKey
EXTERNAL SOURCES: EXTERNAL SOURCES:
audio_service:
:path: Flutter/ephemeral/.symlinks/plugins/audio_service/darwin
audio_session:
:path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos
FlutterMacOS: FlutterMacOS:
:path: Flutter/ephemeral :path: Flutter/ephemeral
hotkey_manager_macos: hotkey_manager_macos:
@@ -83,6 +94,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
SPEC CHECKSUMS: SPEC CHECKSUMS:
audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd
audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
hotkey_manager_macos: a4317849af96d2430fa89944d3c58977ca089fbe hotkey_manager_macos: a4317849af96d2430fa89944d3c58977ca089fbe
+40
View File
@@ -41,6 +41,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.0" version: "2.13.0"
audio_service:
dependency: "direct main"
description:
name: audio_service
sha256: cb122c7c2639d2a992421ef96b67948ad88c5221da3365ccef1031393a76e044
url: "https://pub.dev"
source: hosted
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: "8f96a7fecbb718cb093070f868b4cdcb8a9b1053dce342ff8ab2fde10eb9afb7"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -400,6 +432,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.5" version: "1.0.5"
js:
dependency: transitive
description:
name: js
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
json_annotation: json_annotation:
dependency: "direct main" dependency: "direct main"
description: description:
+2
View File
@@ -25,6 +25,8 @@ dependencies:
provider: ^6.1.2 provider: ^6.1.2
hotkey_manager: ^0.2.3 hotkey_manager: ^0.2.3
flex_color_picker: ^3.6.0 flex_color_picker: ^3.6.0
audio_service: ^0.18.18
audio_session: ^0.2.1
dependency_overrides: dependency_overrides:
media_kit: media_kit: