diff --git a/lib/main.dart b/lib/main.dart index ff9f8314..7ed0850c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'dart:io' show Platform; -import 'package:media_kit/media_kit.dart'; import 'package:window_manager/window_manager.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -49,9 +48,6 @@ void main() async { // Configure macOS window with custom titlebar await MacOSTitlebarService.setupCustomTitlebar(); - // Initialize MediaKit - MediaKit.ensureInitialized(); - // Note: Orientation will be set dynamically based on device type in MainApp await StorageService.getInstance(); diff --git a/lib/mpv/mpv.dart b/lib/mpv/mpv.dart new file mode 100644 index 00000000..6371babb --- /dev/null +++ b/lib/mpv/mpv.dart @@ -0,0 +1,69 @@ +/// MPV Player library for Flutter. +/// +/// This library provides a platform-agnostic interface for video playback +/// using MPV as the underlying player engine. +/// +/// ## Features +/// +/// - Stream-based reactive state updates +/// - Audio passthrough support for lossless audio +/// - Subtitle rendering with libass +/// - Hardware-accelerated decoding +/// - Cross-platform support (macOS, iOS, Android, Windows, Linux) +/// +/// ## Usage +/// +/// ```dart +/// import 'package:flutter_application_1/mpv/mpv.dart'; +/// +/// // Create a player +/// final player = MpvPlayer(); +/// +/// // Configure player properties +/// await player.setProperty('hwdec', 'auto'); +/// await player.setProperty('demuxer-max-bytes', '150000000'); +/// await player.setAudioPassthrough(true); +/// +/// // Open and play media +/// await player.open(MpvMedia('https://example.com/video.mp4')); +/// +/// // Listen to state changes +/// player.streams.position.listen((position) { +/// print('Position: $position'); +/// }); +/// +/// // Display video +/// MpvVideo( +/// player: player, +/// controls: (context) => MyCustomControls(), +/// ) +/// +/// // Clean up +/// await player.dispose(); +/// ``` +library; + +// Import for type alias +import 'src/models/mpv_track_selection.dart' as track_selection; + +// Player +export 'src/player/mpv_player.dart'; +export 'src/player/mpv_player_state.dart'; +export 'src/player/mpv_player_streams.dart'; +export 'src/player/mpv_player_stub.dart'; + +// Models +export 'src/models/mpv_media.dart'; +export 'src/models/mpv_audio_device.dart'; +export 'src/models/mpv_audio_track.dart'; +export 'src/models/mpv_subtitle_track.dart'; +export 'src/models/mpv_tracks.dart'; +export 'src/models/mpv_track_selection.dart'; +export 'src/models/mpv_log.dart'; +export 'src/models/mpv_log_level.dart'; + +// Video +export 'src/video/mpv_video.dart'; + +// Type alias for compatibility (must be after exports) +typedef MpvTrack = track_selection.MpvTrackSelection; diff --git a/lib/mpv/src/models/mpv_audio_device.dart b/lib/mpv/src/models/mpv_audio_device.dart new file mode 100644 index 00000000..4cee9845 --- /dev/null +++ b/lib/mpv/src/models/mpv_audio_device.dart @@ -0,0 +1,29 @@ +/// Represents an audio output device. +class MpvAudioDevice { + /// Unique identifier for the device. + final String name; + + /// Human-readable description of the device. + final String description; + + const MpvAudioDevice({ + required this.name, + this.description = '', + }); + + /// Default/auto audio device. + static const auto = MpvAudioDevice(name: 'auto', description: 'Auto'); + + @override + String toString() => 'MpvAudioDevice($name, $description)'; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MpvAudioDevice && + runtimeType == other.runtimeType && + name == other.name; + + @override + int get hashCode => name.hashCode; +} diff --git a/lib/mpv/src/models/mpv_audio_track.dart b/lib/mpv/src/models/mpv_audio_track.dart new file mode 100644 index 00000000..5b8284c9 --- /dev/null +++ b/lib/mpv/src/models/mpv_audio_track.dart @@ -0,0 +1,70 @@ +/// Represents an audio track in the media. +class MpvAudioTrack { + /// Unique identifier for the track. + final String id; + + /// Human-readable title of the track. + final String? title; + + /// Language code (e.g., 'eng', 'jpn'). + final String? language; + + /// Audio codec (e.g., 'aac', 'ac3', 'dts'). + final String? codec; + + /// Number of audio channels. + final int? channels; + + /// Alias for channels (media_kit compatibility). + int? get channelsCount => channels; + + /// Sample rate in Hz. + final int? sampleRate; + + /// Bitrate in bits per second. + final int? bitrate; + + /// Whether this is the default track. + final bool isDefault; + + /// Whether this track is forced. + final bool isForced; + + const MpvAudioTrack({ + required this.id, + this.title, + this.language, + this.codec, + this.channels, + this.sampleRate, + this.bitrate, + this.isDefault = false, + this.isForced = false, + }); + + /// Auto-select track. + static const auto = MpvAudioTrack(id: 'auto', title: 'Auto'); + + /// Disable audio. + static const off = MpvAudioTrack(id: 'no', title: 'Off'); + + /// Returns a display name for the track. + String get displayName { + if (title != null && title!.isNotEmpty) return title!; + if (language != null && language!.isNotEmpty) return language!; + return 'Track $id'; + } + + @override + String toString() => 'MpvAudioTrack($id, $displayName)'; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MpvAudioTrack && + runtimeType == other.runtimeType && + id == other.id; + + @override + int get hashCode => id.hashCode; +} diff --git a/lib/mpv/src/models/mpv_log.dart b/lib/mpv/src/models/mpv_log.dart new file mode 100644 index 00000000..ce47883f --- /dev/null +++ b/lib/mpv/src/models/mpv_log.dart @@ -0,0 +1,22 @@ +import 'mpv_log_level.dart'; + +/// A log entry from the MPV player. +class MpvLog { + /// The log level of this message. + final MpvLogLevel level; + + /// The prefix/category of the log message (e.g., 'cplayer', 'ffmpeg'). + final String prefix; + + /// The log message text. + final String text; + + const MpvLog({ + required this.level, + required this.prefix, + required this.text, + }); + + @override + String toString() => '[$prefix] ${level.name}: $text'; +} diff --git a/lib/mpv/src/models/mpv_log_level.dart b/lib/mpv/src/models/mpv_log_level.dart new file mode 100644 index 00000000..147e90a3 --- /dev/null +++ b/lib/mpv/src/models/mpv_log_level.dart @@ -0,0 +1,26 @@ +/// Log level for MPV player messages. +enum MpvLogLevel { + /// No logging. + none, + + /// Fatal errors only. + fatal, + + /// Errors. + error, + + /// Warnings. + warn, + + /// Informational messages. + info, + + /// Verbose output. + verbose, + + /// Debug messages. + debug, + + /// Trace-level output (very verbose). + trace, +} diff --git a/lib/mpv/src/models/mpv_media.dart b/lib/mpv/src/models/mpv_media.dart new file mode 100644 index 00000000..ef74e566 --- /dev/null +++ b/lib/mpv/src/models/mpv_media.dart @@ -0,0 +1,31 @@ +/// Represents a media source for the MPV player. +class MpvMedia { + /// The URI of the media (file path, HTTP URL, etc.). + final String uri; + + /// Optional HTTP headers for network requests. + final Map? headers; + + /// Optional start position for playback. + final Duration? start; + + const MpvMedia( + this.uri, { + this.headers, + this.start, + }); + + @override + String toString() => 'MpvMedia($uri)'; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MpvMedia && + runtimeType == other.runtimeType && + uri == other.uri && + start == other.start; + + @override + int get hashCode => uri.hashCode ^ start.hashCode; +} diff --git a/lib/mpv/src/models/mpv_subtitle_track.dart b/lib/mpv/src/models/mpv_subtitle_track.dart new file mode 100644 index 00000000..bcc98fad --- /dev/null +++ b/lib/mpv/src/models/mpv_subtitle_track.dart @@ -0,0 +1,79 @@ +/// Represents a subtitle track in the media. +class MpvSubtitleTrack { + /// Unique identifier for the track. + final String id; + + /// Human-readable title of the track. + final String? title; + + /// Language code (e.g., 'eng', 'jpn'). + final String? language; + + /// Subtitle codec/format (e.g., 'subrip', 'ass', 'pgs'). + final String? codec; + + /// Whether this is the default track. + final bool isDefault; + + /// Whether this track is forced (e.g., for foreign language segments). + final bool isForced; + + /// Whether this is an external subtitle file. + final bool isExternal; + + /// URI of external subtitle file (if isExternal is true). + final String? uri; + + const MpvSubtitleTrack({ + required this.id, + this.title, + this.language, + this.codec, + this.isDefault = false, + this.isForced = false, + this.isExternal = false, + this.uri, + }); + + /// Create a subtitle track from an external URI. + factory MpvSubtitleTrack.uri( + String uri, { + String? title, + String? language, + }) { + return MpvSubtitleTrack( + id: 'external:$uri', + title: title, + language: language, + isExternal: true, + uri: uri, + ); + } + + /// Auto-select track. + static const auto = MpvSubtitleTrack(id: 'auto', title: 'Auto'); + + /// Disable subtitles. + static const off = MpvSubtitleTrack(id: 'no', title: 'Off'); + + /// Returns a display name for the track. + String get displayName { + if (title != null && title!.isNotEmpty) return title!; + if (language != null && language!.isNotEmpty) return language!; + if (isExternal) return 'External'; + return 'Track $id'; + } + + @override + String toString() => 'MpvSubtitleTrack($id, $displayName)'; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MpvSubtitleTrack && + runtimeType == other.runtimeType && + id == other.id; + + @override + int get hashCode => id.hashCode; +} diff --git a/lib/mpv/src/models/mpv_track_selection.dart b/lib/mpv/src/models/mpv_track_selection.dart new file mode 100644 index 00000000..47399c9c --- /dev/null +++ b/lib/mpv/src/models/mpv_track_selection.dart @@ -0,0 +1,30 @@ +import 'mpv_audio_track.dart'; +import 'mpv_subtitle_track.dart'; + +/// Represents the currently selected tracks. +class MpvTrackSelection { + /// Currently selected audio track. + final MpvAudioTrack? audio; + + /// Currently selected subtitle track. + final MpvSubtitleTrack? subtitle; + + const MpvTrackSelection({ + this.audio, + this.subtitle, + }); + + /// Creates a copy with the given fields replaced. + MpvTrackSelection copyWith({ + MpvAudioTrack? audio, + MpvSubtitleTrack? subtitle, + }) { + return MpvTrackSelection( + audio: audio ?? this.audio, + subtitle: subtitle ?? this.subtitle, + ); + } + + @override + String toString() => 'MpvTrackSelection(audio: $audio, subtitle: $subtitle)'; +} diff --git a/lib/mpv/src/models/mpv_tracks.dart b/lib/mpv/src/models/mpv_tracks.dart new file mode 100644 index 00000000..7b208b34 --- /dev/null +++ b/lib/mpv/src/models/mpv_tracks.dart @@ -0,0 +1,31 @@ +import 'mpv_audio_track.dart'; +import 'mpv_subtitle_track.dart'; + +/// Container for all available tracks in the media. +class MpvTracks { + /// Available audio tracks. + final List audio; + + /// Available subtitle tracks. + final List subtitle; + + const MpvTracks({ + this.audio = const [], + this.subtitle = const [], + }); + + /// Creates a copy with the given fields replaced. + MpvTracks copyWith({ + List? audio, + List? subtitle, + }) { + return MpvTracks( + audio: audio ?? this.audio, + subtitle: subtitle ?? this.subtitle, + ); + } + + @override + String toString() => + 'MpvTracks(audio: ${audio.length}, subtitle: ${subtitle.length})'; +} diff --git a/lib/mpv/src/player/mpv_player.dart b/lib/mpv/src/player/mpv_player.dart new file mode 100644 index 00000000..23b05c3e --- /dev/null +++ b/lib/mpv/src/player/mpv_player.dart @@ -0,0 +1,198 @@ +import 'dart:io' show Platform; + +import '../models/mpv_audio_device.dart'; +import '../models/mpv_media.dart'; +import '../models/mpv_audio_track.dart'; +import '../models/mpv_subtitle_track.dart'; +import 'mpv_player_macos.dart'; +import 'mpv_player_state.dart'; +import 'mpv_player_streams.dart'; +import 'mpv_player_stub.dart'; + +/// Abstract interface for the MPV player. +/// +/// This interface defines all playback control methods, state access, +/// and reactive streams for the video player. +/// +/// Example usage: +/// ```dart +/// final player = MpvPlayer(); +/// await player.open(MpvMedia('https://example.com/video.mp4')); +/// await player.play(); +/// +/// // Configure player properties +/// await player.setProperty('hwdec', 'auto'); +/// await player.setProperty('demuxer-max-bytes', '150000000'); +/// +/// // Listen to position updates +/// player.streams.position.listen((position) { +/// print('Position: $position'); +/// }); +/// +/// // Access current state +/// print('Playing: ${player.state.playing}'); +/// ``` +abstract class MpvPlayer { + /// Current synchronous state snapshot. + /// + /// Use this for immediate state access in UI. + MpvPlayerState get state; + + /// Reactive streams for state changes. + /// + /// Use these for reactive UI updates. + MpvPlayerStreams get streams; + + /// Texture ID for Flutter's Texture widget (video rendering). + /// + /// This is set by the platform implementation when video + /// rendering is initialized. Returns null if not ready. + int? get textureId; + + // ============================================ + // Playback Control + // ============================================ + + /// Open a media source for playback. + /// + /// [media] - The media source to open. + /// [play] - Whether to start playback immediately (default: true). + Future open(MpvMedia media, {bool play = true}); + + /// Start or resume playback. + Future play(); + + /// Pause playback. + Future pause(); + + /// Toggle between play and pause. + Future playOrPause(); + + /// Stop playback and reset position. + Future stop(); + + /// Seek to a specific position. + Future seek(Duration position); + + // ============================================ + // Track Selection + // ============================================ + + /// Select an audio track. + Future selectAudioTrack(MpvAudioTrack track); + + /// Select a subtitle track. + /// + /// Pass [MpvSubtitleTrack.off] to disable subtitles. + Future selectSubtitleTrack(MpvSubtitleTrack track); + + /// Add an external subtitle track. + /// + /// [uri] - URL or path to the subtitle file. + /// [title] - Optional display title. + /// [language] - Optional language code. + /// [select] - Whether to select this track immediately. + Future addSubtitleTrack({ + required String uri, + String? title, + String? language, + bool select = false, + }); + + // ============================================ + // Volume and Rate + // ============================================ + + /// Set the playback volume. + /// + /// [volume] - Volume level from 0.0 (muted) to 100.0 (max). + Future setVolume(double volume); + + /// Set the playback rate/speed. + /// + /// [rate] - Playback rate from 0.25 to 4.0 (1.0 = normal speed). + Future setRate(double rate); + + /// Set the audio output device. + /// + /// [device] - The audio device to use. + Future setAudioDevice(MpvAudioDevice device); + + // ============================================ + // MPV Properties (Advanced) + // ============================================ + + /// Set an MPV property by name. + /// + /// Common properties: + /// - 'hwdec': Hardware decoding mode ('auto', 'no', 'videotoolbox', etc.) + /// - 'demuxer-max-bytes': Buffer size in bytes + /// - 'audio-delay': Audio sync offset in seconds (e.g., '0.5') + /// - 'sub-delay': Subtitle sync offset in seconds + /// - 'sub-font': Subtitle font name + /// - 'sub-font-size': Subtitle font size + /// - 'sub-color': Subtitle text color + /// - 'sub-back-color': Subtitle background color + /// - 'sub-border-size': Subtitle border size + /// - 'sub-margin-y': Vertical subtitle margin + /// - 'sub-ass': Enable/disable ASS subtitle rendering ('yes'/'no') + /// - 'audio-exclusive': Exclusive audio mode ('yes'/'no') + /// - 'audio-spdif': Audio passthrough formats (e.g., 'ac3,eac3,dts,truehd') + Future setProperty(String name, String value); + + /// Get an MPV property value by name. + Future getProperty(String name); + + /// Execute a raw MPV command. + /// + /// [args] - Command and arguments as a list of strings. + Future command(List args); + + // ============================================ + // Passthrough Mode (Audio) + // ============================================ + + /// Enable or disable audio passthrough mode. + /// + /// When enabled, supported audio codecs (AC3, DTS, etc.) will be + /// passed through to the audio device without decoding. + Future setAudioPassthrough(bool enabled); + + // ============================================ + // Visibility (macOS Metal Layer) + // ============================================ + + /// Show or hide the video rendering layer. + /// + /// On macOS, this controls the Metal layer visibility. + /// On other platforms, this may have no effect. + /// + /// Returns true if the operation was successful. + Future setVisible(bool visible); + + // ============================================ + // Lifecycle + // ============================================ + + /// Dispose of the player and release resources. + /// + /// After calling this, the player instance should not be used. + Future dispose(); + + // ============================================ + // Factory + // ============================================ + + /// Creates a new MPV player instance. + /// + /// Returns a platform-specific implementation: + /// - macOS: [MpvPlayerMacOS] using MPVKit with Metal rendering + /// - Other platforms: [MpvPlayerStub] (placeholder) + factory MpvPlayer() { + if (Platform.isMacOS) { + return MpvPlayerMacOS(); + } + // Future: Add iOS, Android, Windows, Linux implementations + return MpvPlayerStub(); + } +} diff --git a/lib/mpv/src/player/mpv_player_macos.dart b/lib/mpv/src/player/mpv_player_macos.dart new file mode 100644 index 00000000..a586ed73 --- /dev/null +++ b/lib/mpv/src/player/mpv_player_macos.dart @@ -0,0 +1,506 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; + +import '../models/mpv_audio_device.dart'; +import '../models/mpv_audio_track.dart'; +import '../models/mpv_log.dart'; +import '../models/mpv_log_level.dart'; +import '../models/mpv_media.dart'; +import '../models/mpv_subtitle_track.dart'; +import '../models/mpv_track_selection.dart'; +import '../models/mpv_tracks.dart'; +import 'mpv_player.dart'; +import 'mpv_player_state.dart'; +import 'mpv_player_streams.dart'; + +/// macOS implementation of [MpvPlayer] using MPVKit via platform channels. +class MpvPlayerMacOS implements MpvPlayer { + static const _methodChannel = MethodChannel('com.plezy/mpv_player'); + static const _eventChannel = EventChannel('com.plezy/mpv_player/events'); + + MpvPlayerState _state = const MpvPlayerState(); + + @override + MpvPlayerState get state => _state; + + late final MpvPlayerStreams _streams; + + @override + MpvPlayerStreams get streams => _streams; + + @override + int? get textureId => null; // macOS uses direct Metal layer, not Flutter texture + + // Stream controllers + final _playingController = StreamController.broadcast(); + final _completedController = StreamController.broadcast(); + final _bufferingController = StreamController.broadcast(); + final _positionController = StreamController.broadcast(); + final _durationController = StreamController.broadcast(); + final _bufferController = StreamController.broadcast(); + final _volumeController = StreamController.broadcast(); + final _rateController = StreamController.broadcast(); + final _tracksController = StreamController.broadcast(); + final _trackController = StreamController.broadcast(); + final _logController = StreamController.broadcast(); + final _errorController = StreamController.broadcast(); + final _audioDeviceController = StreamController.broadcast(); + final _audioDevicesController = + StreamController>.broadcast(); + + StreamSubscription? _eventSubscription; + bool _disposed = false; + bool _initialized = false; + bool _isVisible = false; + + MpvPlayerMacOS() { + _streams = MpvPlayerStreams( + playing: _playingController.stream, + completed: _completedController.stream, + buffering: _bufferingController.stream, + position: _positionController.stream, + duration: _durationController.stream, + buffer: _bufferController.stream, + volume: _volumeController.stream, + rate: _rateController.stream, + tracks: _tracksController.stream, + track: _trackController.stream, + log: _logController.stream, + error: _errorController.stream, + audioDevice: _audioDeviceController.stream, + audioDevices: _audioDevicesController.stream, + ); + + _setupEventListener(); + } + + void _setupEventListener() { + _eventSubscription = _eventChannel.receiveBroadcastStream().listen( + _handleEvent, + onError: (error) { + _errorController.add(error.toString()); + }, + ); + } + + void _handleEvent(dynamic event) { + if (event is! Map) return; + + final type = event['type'] as String?; + final name = event['name'] as String?; + + if (type == 'property' && name != null) { + _handlePropertyChange(name, event['value']); + } else if (type == 'event' && name != null) { + _handleMpvEvent(name, event['data'] as Map?); + } + } + + void _handlePropertyChange(String name, dynamic value) { + switch (name) { + case 'pause': + final playing = value == false; + _state = _state.copyWith(playing: playing); + _playingController.add(playing); + break; + + case 'eof-reached': + final completed = value == true; + _state = _state.copyWith(completed: completed); + _completedController.add(completed); + break; + + case 'paused-for-cache': + final buffering = value == true; + _state = _state.copyWith(buffering: buffering); + _bufferingController.add(buffering); + break; + + case 'time-pos': + if (value is num) { + final position = Duration(milliseconds: (value * 1000).toInt()); + _state = _state.copyWith(position: position); + _positionController.add(position); + } + break; + + case 'duration': + if (value is num) { + final duration = Duration(milliseconds: (value * 1000).toInt()); + _state = _state.copyWith(duration: duration); + _durationController.add(duration); + } + break; + + case 'demuxer-cache-time': + if (value is num) { + final buffer = Duration(milliseconds: (value * 1000).toInt()); + _state = _state.copyWith(buffer: buffer); + _bufferController.add(buffer); + } + break; + + case 'volume': + if (value is num) { + final volume = value.toDouble(); + _state = _state.copyWith(volume: volume); + _volumeController.add(volume); + } + break; + + case 'speed': + if (value is num) { + final rate = value.toDouble(); + _state = _state.copyWith(rate: rate); + _rateController.add(rate); + } + break; + + case 'track-list': + if (value is List) { + final tracks = _parseTrackList(value); + _state = _state.copyWith(tracks: tracks); + _tracksController.add(tracks); + } + break; + + case 'aid': + _updateSelectedAudioTrack(value); + break; + + case 'sid': + _updateSelectedSubtitleTrack(value); + break; + } + } + + void _handleMpvEvent(String name, Map? data) { + switch (name) { + case 'end-file': + final reason = data?['reason'] as String?; + if (reason == 'eof') { + _state = _state.copyWith(completed: true); + _completedController.add(true); + } else if (reason == 'error') { + _errorController.add('Playback error'); + } + break; + + case 'file-loaded': + // Reset completed state when new file is loaded + _state = _state.copyWith(completed: false); + _completedController.add(false); + break; + } + } + + MpvTracks _parseTrackList(List trackList) { + final audioTracks = []; + final subtitleTracks = []; + + for (final track in trackList) { + if (track is! Map) continue; + + final type = track['type'] as String?; + final id = track['id']?.toString() ?? ''; + + if (type == 'audio') { + audioTracks.add(MpvAudioTrack( + id: id, + title: track['title'] as String?, + language: track['lang'] as String?, + codec: track['codec'] as String?, + channels: (track['demux-channel-count'] as num?)?.toInt(), + sampleRate: (track['demux-samplerate'] as num?)?.toInt(), + isDefault: track['default'] as bool? ?? false, + )); + } else if (type == 'sub') { + subtitleTracks.add(MpvSubtitleTrack( + id: id, + title: track['title'] as String?, + language: track['lang'] as String?, + codec: track['codec'] as String?, + isExternal: track['external'] as bool? ?? false, + uri: track['external-filename'] as String?, + )); + } + } + + return MpvTracks(audio: audioTracks, subtitle: subtitleTracks); + } + + void _updateSelectedAudioTrack(dynamic trackId) { + final id = trackId?.toString(); + MpvAudioTrack? selectedTrack; + + if (id != null && id != 'no') { + selectedTrack = _state.tracks.audio.cast().firstWhere( + (t) => t?.id == id, + orElse: () => null, + ); + } + + _state = _state.copyWith( + track: _state.track.copyWith(audio: selectedTrack), + ); + _trackController.add(_state.track); + } + + void _updateSelectedSubtitleTrack(dynamic trackId) { + final id = trackId?.toString(); + MpvSubtitleTrack? selectedTrack; + + if (id != null && id != 'no') { + selectedTrack = + _state.tracks.subtitle.cast().firstWhere( + (t) => t?.id == id, + orElse: () => null, + ); + } + + _state = _state.copyWith( + track: _state.track.copyWith(subtitle: selectedTrack), + ); + _trackController.add(_state.track); + } + + Future _ensureInitialized() async { + if (_initialized) return; + + try { + final result = await _methodChannel.invokeMethod('initialize'); + _initialized = result == true; + if (!_initialized) { + throw Exception('Failed to initialize MPV player'); + } + + // Subscribe to MPV properties + await _observeProperty('time-pos', 'double'); + await _observeProperty('duration', 'double'); + await _observeProperty('pause', 'flag'); + await _observeProperty('paused-for-cache', 'flag'); + await _observeProperty('track-list', 'node'); + await _observeProperty('eof-reached', 'flag'); + await _observeProperty('volume', 'double'); + await _observeProperty('aid', 'string'); + await _observeProperty('sid', 'string'); + } catch (e) { + _errorController.add('Initialization failed: $e'); + rethrow; + } + } + + Future _observeProperty(String name, String format) async { + await _methodChannel.invokeMethod('observeProperty', { + 'name': name, + 'format': format, + }); + } + + void _checkDisposed() { + if (_disposed) { + throw StateError('MpvPlayer has been disposed'); + } + } + + // ============================================ + // Playback Control + // ============================================ + + @override + Future open(MpvMedia media, {bool play = true}) async { + _checkDisposed(); + await _ensureInitialized(); + + // Show the video layer + await _methodChannel.invokeMethod('setVisible', {'visible': true}); + _isVisible = true; + + await command(['loadfile', media.uri, 'replace']); + if (!play) { + await setProperty('pause', 'yes'); + } + } + + @override + Future play() async { + _checkDisposed(); + await setProperty('pause', 'no'); + } + + @override + Future pause() async { + _checkDisposed(); + await setProperty('pause', 'yes'); + } + + @override + Future playOrPause() async { + _checkDisposed(); + if (_state.playing) { + await pause(); + } else { + await play(); + } + } + + @override + Future stop() async { + _checkDisposed(); + await command(['stop']); + await _methodChannel.invokeMethod('setVisible', {'visible': false}); + _isVisible = false; + } + + @override + Future seek(Duration position) async { + _checkDisposed(); + await command([ + 'seek', + (position.inMilliseconds / 1000.0).toString(), + 'absolute', + ]); + } + + // ============================================ + // Track Selection + // ============================================ + + @override + Future selectAudioTrack(MpvAudioTrack track) async { + _checkDisposed(); + await setProperty('aid', track.id); + } + + @override + Future selectSubtitleTrack(MpvSubtitleTrack track) async { + _checkDisposed(); + await setProperty('sid', track.id); + } + + @override + Future addSubtitleTrack({ + required String uri, + String? title, + String? language, + bool select = false, + }) async { + _checkDisposed(); + final args = ['sub-add', uri, select ? 'select' : 'auto']; + if (title != null) args.add('title=$title'); + if (language != null) args.add('lang=$language'); + await command(args); + } + + // ============================================ + // Volume and Rate + // ============================================ + + @override + Future setVolume(double volume) async { + _checkDisposed(); + await setProperty('volume', volume.toString()); + } + + @override + Future setRate(double rate) async { + _checkDisposed(); + await setProperty('speed', rate.toString()); + } + + @override + Future setAudioDevice(MpvAudioDevice device) async { + _checkDisposed(); + await setProperty('audio-device', device.name); + } + + // ============================================ + // MPV Properties + // ============================================ + + @override + Future setProperty(String name, String value) async { + _checkDisposed(); + await _methodChannel.invokeMethod('setProperty', { + 'name': name, + 'value': value, + }); + } + + @override + Future getProperty(String name) async { + _checkDisposed(); + return await _methodChannel.invokeMethod('getProperty', { + 'name': name, + }); + } + + @override + Future command(List args) async { + _checkDisposed(); + await _methodChannel.invokeMethod('command', {'args': args}); + } + + // ============================================ + // Passthrough + // ============================================ + + @override + Future setAudioPassthrough(bool enabled) async { + _checkDisposed(); + if (enabled) { + await setProperty('audio-spdif', 'ac3,eac3,dts,dts-hd,truehd'); + await setProperty('audio-exclusive', 'yes'); + } else { + await setProperty('audio-spdif', ''); + await setProperty('audio-exclusive', 'no'); + } + } + + // ============================================ + // Visibility + // ============================================ + + @override + Future setVisible(bool visible) async { + _checkDisposed(); + + try { + await _methodChannel.invokeMethod('setVisible', {'visible': visible}); + _isVisible = visible; + return true; + } catch (e) { + _errorController.add('Failed to set visibility: $e'); + return false; + } + } + + // ============================================ + // Lifecycle + // ============================================ + + @override + Future dispose() async { + if (_disposed) return; + _disposed = true; + + await _eventSubscription?.cancel(); + await _methodChannel.invokeMethod('dispose'); + + await _playingController.close(); + await _completedController.close(); + await _bufferingController.close(); + await _positionController.close(); + await _durationController.close(); + await _bufferController.close(); + await _volumeController.close(); + await _rateController.close(); + await _tracksController.close(); + await _trackController.close(); + await _logController.close(); + await _errorController.close(); + await _audioDeviceController.close(); + await _audioDevicesController.close(); + } +} diff --git a/lib/mpv/src/player/mpv_player_state.dart b/lib/mpv/src/player/mpv_player_state.dart new file mode 100644 index 00000000..bad56556 --- /dev/null +++ b/lib/mpv/src/player/mpv_player_state.dart @@ -0,0 +1,113 @@ +import '../models/mpv_audio_device.dart'; +import '../models/mpv_tracks.dart'; +import '../models/mpv_track_selection.dart'; + +/// Immutable snapshot of the current player state. +/// +/// This class provides synchronous access to the player's current state. +/// For reactive updates, use [MpvPlayerStreams]. +class MpvPlayerState { + /// Whether playback is currently active. + final bool playing; + + /// Whether the media has completed playback. + final bool completed; + + /// Whether the player is currently buffering. + final bool buffering; + + /// Current playback position. + final Duration position; + + /// Total duration of the media. + final Duration duration; + + /// Amount of media buffered ahead of current position. + final Duration buffer; + + /// Current volume level (0.0 to 100.0). + final double volume; + + /// Current playback rate (1.0 = normal speed). + final double rate; + + /// Available tracks in the media. + final MpvTracks tracks; + + /// Currently selected tracks. + final MpvTrackSelection track; + + /// Audio delay/sync offset in seconds. + final double audioDelay; + + /// Subtitle delay/sync offset in seconds. + final double subtitleDelay; + + /// Whether audio passthrough is currently enabled. + final bool audioPassthrough; + + /// Current audio output device. + final MpvAudioDevice audioDevice; + + /// Available audio output devices. + final List audioDevices; + + const MpvPlayerState({ + this.playing = false, + this.completed = false, + this.buffering = false, + this.position = Duration.zero, + this.duration = Duration.zero, + this.buffer = Duration.zero, + this.volume = 100.0, + this.rate = 1.0, + this.tracks = const MpvTracks(), + this.track = const MpvTrackSelection(), + this.audioDelay = 0.0, + this.subtitleDelay = 0.0, + this.audioPassthrough = false, + this.audioDevice = MpvAudioDevice.auto, + this.audioDevices = const [], + }); + + /// Creates a copy with the given fields replaced. + MpvPlayerState copyWith({ + bool? playing, + bool? completed, + bool? buffering, + Duration? position, + Duration? duration, + Duration? buffer, + double? volume, + double? rate, + MpvTracks? tracks, + MpvTrackSelection? track, + double? audioDelay, + double? subtitleDelay, + bool? audioPassthrough, + MpvAudioDevice? audioDevice, + List? audioDevices, + }) { + return MpvPlayerState( + playing: playing ?? this.playing, + completed: completed ?? this.completed, + buffering: buffering ?? this.buffering, + position: position ?? this.position, + duration: duration ?? this.duration, + buffer: buffer ?? this.buffer, + volume: volume ?? this.volume, + rate: rate ?? this.rate, + tracks: tracks ?? this.tracks, + track: track ?? this.track, + audioDelay: audioDelay ?? this.audioDelay, + subtitleDelay: subtitleDelay ?? this.subtitleDelay, + audioPassthrough: audioPassthrough ?? this.audioPassthrough, + audioDevice: audioDevice ?? this.audioDevice, + audioDevices: audioDevices ?? this.audioDevices, + ); + } + + @override + String toString() => + 'MpvPlayerState(playing: $playing, position: $position, duration: $duration)'; +} diff --git a/lib/mpv/src/player/mpv_player_streams.dart b/lib/mpv/src/player/mpv_player_streams.dart new file mode 100644 index 00000000..7a240f36 --- /dev/null +++ b/lib/mpv/src/player/mpv_player_streams.dart @@ -0,0 +1,69 @@ +import '../models/mpv_audio_device.dart'; +import '../models/mpv_log.dart'; +import '../models/mpv_tracks.dart'; +import '../models/mpv_track_selection.dart'; + +/// Reactive streams for player state changes. +/// +/// Subscribe to these streams to receive updates when the player state changes. +/// For synchronous state access, use [MpvPlayerState]. +class MpvPlayerStreams { + /// Stream of playing state changes. + final Stream playing; + + /// Stream of completion state changes. + final Stream completed; + + /// Stream of buffering state changes. + final Stream buffering; + + /// Stream of position updates. + final Stream position; + + /// Stream of duration changes (when media is loaded). + final Stream duration; + + /// Stream of buffer position updates. + final Stream buffer; + + /// Stream of volume changes. + final Stream volume; + + /// Stream of playback rate changes. + final Stream rate; + + /// Stream of available tracks updates. + final Stream tracks; + + /// Stream of track selection changes. + final Stream track; + + /// Stream of log messages from MPV. + final Stream log; + + /// Stream of error messages. + final Stream error; + + /// Stream of audio device changes. + final Stream audioDevice; + + /// Stream of available audio devices. + final Stream> audioDevices; + + const MpvPlayerStreams({ + required this.playing, + required this.completed, + required this.buffering, + required this.position, + required this.duration, + required this.buffer, + required this.volume, + required this.rate, + required this.tracks, + required this.track, + required this.log, + required this.error, + required this.audioDevice, + required this.audioDevices, + }); +} diff --git a/lib/mpv/src/player/mpv_player_stub.dart b/lib/mpv/src/player/mpv_player_stub.dart new file mode 100644 index 00000000..397c4050 --- /dev/null +++ b/lib/mpv/src/player/mpv_player_stub.dart @@ -0,0 +1,242 @@ +import 'dart:async'; + +import '../models/mpv_audio_device.dart'; +import '../models/mpv_media.dart'; +import '../models/mpv_audio_track.dart'; +import '../models/mpv_subtitle_track.dart'; +import '../models/mpv_tracks.dart'; +import '../models/mpv_track_selection.dart'; +import '../models/mpv_log.dart'; +import 'mpv_player.dart'; +import 'mpv_player_state.dart'; +import 'mpv_player_streams.dart'; + +/// Stub implementation of [MpvPlayer]. +/// +/// This implementation creates all the necessary stream controllers and +/// state management, but throws [UnimplementedError] for all playback +/// methods. It serves as a foundation for platform-specific implementations. +class MpvPlayerStub implements MpvPlayer { + MpvPlayerState _state = const MpvPlayerState(); + + @override + MpvPlayerState get state => _state; + + late final MpvPlayerStreams _streams; + + @override + MpvPlayerStreams get streams => _streams; + + @override + int? get textureId => null; // Set by platform implementation + + // Stream controllers + final _playingController = StreamController.broadcast(); + final _completedController = StreamController.broadcast(); + final _bufferingController = StreamController.broadcast(); + final _positionController = StreamController.broadcast(); + final _durationController = StreamController.broadcast(); + final _bufferController = StreamController.broadcast(); + final _volumeController = StreamController.broadcast(); + final _rateController = StreamController.broadcast(); + final _tracksController = StreamController.broadcast(); + final _trackController = StreamController.broadcast(); + final _logController = StreamController.broadcast(); + final _errorController = StreamController.broadcast(); + final _audioDeviceController = StreamController.broadcast(); + final _audioDevicesController = + StreamController>.broadcast(); + + bool _disposed = false; + + MpvPlayerStub() { + _streams = MpvPlayerStreams( + playing: _playingController.stream, + completed: _completedController.stream, + buffering: _bufferingController.stream, + position: _positionController.stream, + duration: _durationController.stream, + buffer: _bufferController.stream, + volume: _volumeController.stream, + rate: _rateController.stream, + tracks: _tracksController.stream, + track: _trackController.stream, + log: _logController.stream, + error: _errorController.stream, + audioDevice: _audioDeviceController.stream, + audioDevices: _audioDevicesController.stream, + ); + } + + void _checkDisposed() { + if (_disposed) { + throw StateError('MpvPlayer has been disposed'); + } + } + + // ============================================ + // Playback Control + // ============================================ + + @override + Future open(MpvMedia media, {bool play = true}) async { + _checkDisposed(); + throw UnimplementedError('MpvPlayer.open() is not yet implemented'); + } + + @override + Future play() async { + _checkDisposed(); + throw UnimplementedError('MpvPlayer.play() is not yet implemented'); + } + + @override + Future pause() async { + _checkDisposed(); + throw UnimplementedError('MpvPlayer.pause() is not yet implemented'); + } + + @override + Future playOrPause() async { + _checkDisposed(); + if (_state.playing) { + await pause(); + } else { + await play(); + } + } + + @override + Future stop() async { + _checkDisposed(); + throw UnimplementedError('MpvPlayer.stop() is not yet implemented'); + } + + @override + Future seek(Duration position) async { + _checkDisposed(); + throw UnimplementedError('MpvPlayer.seek() is not yet implemented'); + } + + // ============================================ + // Track Selection + // ============================================ + + @override + Future selectAudioTrack(MpvAudioTrack track) async { + _checkDisposed(); + throw UnimplementedError( + 'MpvPlayer.selectAudioTrack() is not yet implemented'); + } + + @override + Future selectSubtitleTrack(MpvSubtitleTrack track) async { + _checkDisposed(); + throw UnimplementedError( + 'MpvPlayer.selectSubtitleTrack() is not yet implemented'); + } + + @override + Future addSubtitleTrack({ + required String uri, + String? title, + String? language, + bool select = false, + }) async { + _checkDisposed(); + throw UnimplementedError( + 'MpvPlayer.addSubtitleTrack() is not yet implemented'); + } + + // ============================================ + // Volume and Rate + // ============================================ + + @override + Future setVolume(double volume) async { + _checkDisposed(); + throw UnimplementedError('MpvPlayer.setVolume() is not yet implemented'); + } + + @override + Future setRate(double rate) async { + _checkDisposed(); + throw UnimplementedError('MpvPlayer.setRate() is not yet implemented'); + } + + @override + Future setAudioDevice(MpvAudioDevice device) async { + _checkDisposed(); + throw UnimplementedError( + 'MpvPlayer.setAudioDevice() is not yet implemented'); + } + + // ============================================ + // MPV Properties + // ============================================ + + @override + Future setProperty(String name, String value) async { + _checkDisposed(); + throw UnimplementedError('MpvPlayer.setProperty() is not yet implemented'); + } + + @override + Future getProperty(String name) async { + _checkDisposed(); + throw UnimplementedError('MpvPlayer.getProperty() is not yet implemented'); + } + + @override + Future command(List args) async { + _checkDisposed(); + throw UnimplementedError('MpvPlayer.command() is not yet implemented'); + } + + // ============================================ + // Passthrough + // ============================================ + + @override + Future setAudioPassthrough(bool enabled) async { + _checkDisposed(); + throw UnimplementedError( + 'MpvPlayer.setAudioPassthrough() is not yet implemented'); + } + + // ============================================ + // Visibility + // ============================================ + + @override + Future setVisible(bool visible) async { + _checkDisposed(); + // No-op on unsupported platforms + return false; + } + + // ============================================ + // Lifecycle + // ============================================ + + @override + Future dispose() async { + if (_disposed) return; + _disposed = true; + + await _playingController.close(); + await _completedController.close(); + await _bufferingController.close(); + await _positionController.close(); + await _durationController.close(); + await _bufferController.close(); + await _volumeController.close(); + await _rateController.close(); + await _tracksController.close(); + await _trackController.close(); + await _logController.close(); + await _errorController.close(); + await _audioDeviceController.close(); + await _audioDevicesController.close(); + } +} diff --git a/lib/mpv/src/video/mpv_video.dart b/lib/mpv/src/video/mpv_video.dart new file mode 100644 index 00000000..a8dcde60 --- /dev/null +++ b/lib/mpv/src/video/mpv_video.dart @@ -0,0 +1,106 @@ +import 'dart:io' show Platform; + +import 'package:flutter/material.dart'; + +import '../player/mpv_player.dart'; + +/// Video widget for displaying MPV player output. +/// +/// This widget displays the video output from an [MpvPlayer] instance +/// and optionally overlays custom controls. +/// +/// Example usage: +/// ```dart +/// final player = MpvPlayer(); +/// +/// MpvVideo( +/// player: player, +/// fit: BoxFit.contain, +/// controls: (context) => MyCustomControls(), +/// ) +/// ``` +class MpvVideo extends StatefulWidget { + /// The player instance. + final MpvPlayer player; + + /// How the video should be inscribed into the widget's box. + final BoxFit fit; + + /// Builder for custom video controls overlay. + final Widget Function(BuildContext context)? controls; + + /// Background color shown behind the video. + final Color backgroundColor; + + const MpvVideo({ + super.key, + required this.player, + this.fit = BoxFit.contain, + this.controls, + this.backgroundColor = Colors.black, + }); + + @override + State createState() => _MpvVideoState(); +} + +class _MpvVideoState extends State { + @override + Widget build(BuildContext context) { + // macOS uses transparent background so native Metal layer shows through + final bgColor = Platform.isMacOS ? Colors.transparent : widget.backgroundColor; + + return Container( + color: bgColor, + child: Stack( + fit: StackFit.expand, + children: [ + // Video rendering area + _buildVideoSurface(), + + // Controls overlay + if (widget.controls != null) widget.controls!(context), + ], + ), + ); + } + + Widget _buildVideoSurface() { + // macOS: Video renders behind Flutter via native Metal layer + // Just show transparent container so native layer shows through + if (Platform.isMacOS) { + return const SizedBox.expand(); + } + + // When textureId is available, use Flutter's Texture widget + final textureId = widget.player.textureId; + if (textureId != null) { + return FittedBox( + fit: widget.fit, + child: Texture(textureId: textureId), + ); + } + + // Placeholder when video is not ready + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.play_circle_outline, + size: 64, + color: Colors.white.withValues(alpha: 0.3), + ), + const SizedBox(height: 16), + Text( + 'MPV Video', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.3), + fontSize: 14, + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index f058a07e..63ab65e6 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -3,11 +3,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:media_kit/media_kit.dart'; -import 'package:media_kit_video/media_kit_video.dart'; import 'package:os_media_controls/os_media_controls.dart'; import 'package:provider/provider.dart'; +import '../mpv/mpv.dart'; + import '../client/plex_client.dart'; import '../models/plex_media_version.dart'; import '../models/plex_metadata.dart'; @@ -28,8 +28,8 @@ import '../i18n/strings.g.dart'; class VideoPlayerScreen extends StatefulWidget { final PlexMetadata metadata; - final AudioTrack? preferredAudioTrack; - final SubtitleTrack? preferredSubtitleTrack; + final MpvAudioTrack? preferredAudioTrack; + final MpvSubtitleTrack? preferredSubtitleTrack; final double? preferredPlaybackRate; final int selectedMediaIndex; @@ -48,8 +48,7 @@ class VideoPlayerScreen extends StatefulWidget { class VideoPlayerScreenState extends State with WidgetsBindingObserver { - Player? player; - VideoController? controller; + MpvPlayer? player; bool _isPlayerInitialized = false; PlexMetadata? _nextEpisode; PlexMetadata? _previousEpisode; @@ -57,13 +56,13 @@ class VideoPlayerScreenState extends State bool _showPlayNextDialog = false; bool _isPhone = false; List _availableVersions = []; - StreamSubscription? _logSubscription; + StreamSubscription? _logSubscription; StreamSubscription? _errorSubscription; StreamSubscription? _playingSubscription; StreamSubscription? _completedSubscription; StreamSubscription? _mediaControlSubscription; StreamSubscription? _bufferingSubscription; - StreamSubscription? _trackLoadingSubscription; + StreamSubscription? _trackLoadingSubscription; bool _isReplacingWithVideo = false; // Flag to skip orientation restoration during video-to-video navigation @@ -222,57 +221,44 @@ class VideoPlayerScreenState extends State .getEnableHardwareDecoding(); final debugLoggingEnabled = settingsService.getEnableDebugLogging(); - // Build MPV configuration - final config = { - 'sub-font-size': settingsService.getSubtitleFontSize().toString(), - 'sub-color': settingsService.getSubtitleTextColor(), - 'sub-border-size': settingsService.getSubtitleBorderSize().toString(), - 'sub-border-color': settingsService.getSubtitleBorderColor(), - 'sub-back-color': - '#${(settingsService.getSubtitleBackgroundOpacity() * 255 / 100).toInt().toRadixString(16).padLeft(2, '0').toUpperCase()}${settingsService.getSubtitleBackgroundColor().replaceFirst('#', '')}', - 'sub-ass-override': 'no', - }; + // Create player + player = MpvPlayer(); + // Configure player properties + await player!.setProperty('sub-ass', 'yes'); // Enable libass + await player!.setProperty('sub-fonts-dir', 'assets'); + await player!.setProperty('sub-font', 'Go Noto Current-Regular'); + await player!.setProperty('demuxer-max-bytes', bufferSizeBytes.toString()); + await player!.setProperty('msg-level', debugLoggingEnabled ? 'all=debug' : 'all=error'); + // await player!.setProperty('hwdec', enableHardwareDecoding ? 'auto' : 'no'); + + // Subtitle styling + await player!.setProperty('sub-font-size', settingsService.getSubtitleFontSize().toString()); + await player!.setProperty('sub-color', settingsService.getSubtitleTextColor()); + await player!.setProperty('sub-border-size', settingsService.getSubtitleBorderSize().toString()); + await player!.setProperty('sub-border-color', settingsService.getSubtitleBorderColor()); + final bgOpacity = (settingsService.getSubtitleBackgroundOpacity() * 255 / 100).toInt(); + final bgColor = settingsService.getSubtitleBackgroundColor().replaceFirst('#', ''); + await player!.setProperty('sub-back-color', '#${bgOpacity.toRadixString(16).padLeft(2, '0').toUpperCase()}$bgColor'); + await player!.setProperty('sub-ass-override', 'no'); + + // Platform-specific settings if (Platform.isIOS) { - config['audio-exclusive'] = 'yes'; + await player!.setProperty('audio-exclusive', 'yes'); } - // Create player with configuration - player = Player( - configuration: PlayerConfiguration( - libass: true, - libassAndroidFont: 'assets/go-noto-current-regular.ttf', - libassAndroidFontName: 'Go Noto Current-Regular', - bufferSize: bufferSizeBytes, - logLevel: debugLoggingEnabled ? MPVLogLevel.debug : MPVLogLevel.error, - mpvConfiguration: config, - ), - ); - controller = VideoController( - player!, - configuration: VideoControllerConfiguration( - enableHardwareAcceleration: enableHardwareDecoding, - ), - ); - // Apply audio sync offset final audioSyncOffset = settingsService.getAudioSyncOffset(); if (audioSyncOffset != 0) { final offsetSeconds = audioSyncOffset / 1000.0; - await (player!.platform as dynamic).setProperty( - 'audio-delay', - offsetSeconds.toString(), - ); + await player!.setProperty('audio-delay', offsetSeconds.toString()); } // Apply subtitle sync offset final subtitleSyncOffset = settingsService.getSubtitleSyncOffset(); if (subtitleSyncOffset != 0) { final offsetSeconds = subtitleSyncOffset / 1000.0; - await (player!.platform as dynamic).setProperty( - 'sub-delay', - offsetSeconds.toString(), - ); + await player!.setProperty('sub-delay', offsetSeconds.toString()); } // Apply saved volume @@ -310,23 +296,23 @@ class VideoPlayerScreenState extends State } // Listen to playback state changes - _playingSubscription = player!.stream.playing.listen( + _playingSubscription = player!.streams.playing.listen( _onPlayingStateChanged, ); // Listen to completion - _completedSubscription = player!.stream.completed.listen( + _completedSubscription = player!.streams.completed.listen( _onVideoCompleted, ); // Listen to MPV logs - _logSubscription = player!.stream.log.listen(_onPlayerLog); + _logSubscription = player!.streams.log.listen(_onPlayerLog); // Listen to MPV errors - _errorSubscription = player!.stream.error.listen(_onPlayerError); + _errorSubscription = player!.streams.error.listen(_onPlayerError); // Listen to buffering state - _bufferingSubscription = player!.stream.buffering.listen((isBuffering) { + _bufferingSubscription = player!.streams.buffering.listen((isBuffering) { _isBuffering.value = isBuffering; }); @@ -424,12 +410,12 @@ class VideoPlayerScreenState extends State ); // Listen to playing state and update media controls - player!.stream.playing.listen((isPlaying) { + player!.streams.playing.listen((isPlaying) { _updateMediaControlsPlaybackState(); }); // Listen to position updates for media controls - player!.stream.position.listen((position) { + player!.streams.position.listen((position) { _mediaControlsManager?.updatePlaybackState( isPlaying: player!.state.playing, position: position, @@ -542,26 +528,22 @@ class VideoPlayerScreenState extends State // Use server-specific client for this metadata final client = _getClientForMetadata(context); - // Capture profile settings before async gap - final profileSettings = context.profileSettings; - // Initialize playback service final playbackService = PlaybackInitializationService( - player: player!, client: client, - context: context, ); - // Start playback and get available versions - final result = await playbackService.startPlayback( + // Get playback data (video URL and available versions) + final result = await playbackService.getPlaybackData( metadata: widget.metadata, selectedMediaIndex: widget.selectedMediaIndex, - profileSettings: profileSettings, - preferredAudioTrack: widget.preferredAudioTrack, - preferredSubtitleTrack: widget.preferredSubtitleTrack, - preferredPlaybackRate: widget.preferredPlaybackRate, ); + // Open video through MpvPlayer + if (result.videoUrl != null) { + await player!.open(MpvMedia(result.videoUrl!)); + } + // Update available versions from the playback data if (mounted) { setState(() { @@ -643,11 +625,10 @@ class VideoPlayerScreenState extends State // Clear video filter and reset subtitle margins before disposing player try { if (player != null) { - final nativePlayer = player!.platform as dynamic; - nativePlayer.setProperty('vf', ''); - nativePlayer.setProperty('sub-margin-x', '0'); - nativePlayer.setProperty('sub-margin-y', '0'); - nativePlayer.setProperty('sub-scale', '1.0'); + player!.setProperty('vf', ''); + player!.setProperty('sub-margin-x', '0'); + player!.setProperty('sub-margin-y', '0'); + player!.setProperty('sub-scale', '1.0'); } } catch (e) { // Non-critical: Cleanup operations during disposal @@ -700,26 +681,26 @@ class VideoPlayerScreenState extends State } } - void _onPlayerLog(PlayerLog log) { + void _onPlayerLog(MpvLog log) { // Map MPV log levels to app logger levels - switch (log.level.toLowerCase()) { - case 'fatal': - case 'error': + switch (log.level) { + case MpvLogLevel.fatal: + case MpvLogLevel.error: appLogger.e('[MPV:${log.prefix}] ${log.text}'); break; - case 'warn': + case MpvLogLevel.warn: appLogger.w('[MPV:${log.prefix}] ${log.text}'); break; - case 'info': + case MpvLogLevel.info: appLogger.i('[MPV:${log.prefix}] ${log.text}'); break; - case 'debug': - case 'trace': - case 'v': + case MpvLogLevel.debug: + case MpvLogLevel.trace: + case MpvLogLevel.verbose: appLogger.d('[MPV:${log.prefix}] ${log.text}'); break; default: - appLogger.d('[MPV:${log.prefix}:${log.level}] ${log.text}'); + appLogger.d('[MPV:${log.prefix}:${log.level.name}] ${log.text}'); } } @@ -758,7 +739,7 @@ class VideoPlayerScreenState extends State } /// Handle audio track changes from the user - save as per-media preference if enabled - Future _onAudioTrackChanged(AudioTrack track) async { + Future _onAudioTrackChanged(MpvAudioTrack track) async { final settings = await SettingsService.getInstance(); // Only save if remember track selections is enabled @@ -800,7 +781,7 @@ class VideoPlayerScreenState extends State } /// Handle subtitle track changes from the user - save as per-media preference if enabled - Future _onSubtitleTrackChanged(SubtitleTrack track) async { + Future _onSubtitleTrackChanged(MpvSubtitleTrack track) async { final settings = await SettingsService.getInstance(); // Only save if remember track selections is enabled @@ -908,7 +889,7 @@ class VideoPlayerScreenState extends State @override Widget build(BuildContext context) { // Show loading indicator while player initializes - if (!_isPlayerInitialized || controller == null || player == null) { + if (!_isPlayerInitialized || player == null) { return const Scaffold( backgroundColor: Colors.black, body: Center(child: CircularProgressIndicator(color: Colors.white)), @@ -928,7 +909,8 @@ class VideoPlayerScreenState extends State } }, child: Scaffold( - backgroundColor: Colors.black, + // Use transparent background on macOS when native video layer is active + backgroundColor: Platform.isMacOS ? Colors.transparent : Colors.black, body: GestureDetector( behavior: HitTestBehavior .translucent, // Allow taps to pass through to controls @@ -959,44 +941,44 @@ class VideoPlayerScreenState extends State children: [ // Video player Center( - child: LayoutBuilder( - builder: (context, constraints) { - // Update player size when layout changes - final newSize = Size( - constraints.maxWidth, - constraints.maxHeight, - ); + child: LayoutBuilder( + builder: (context, constraints) { + // Update player size when layout changes + final newSize = Size( + constraints.maxWidth, + constraints.maxHeight, + ); - // Update player size in video filter manager - if (_videoFilterManager != null) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - _videoFilterManager!.updatePlayerSize(newSize); - } - }); - } + // Update player size in video filter manager + if (_videoFilterManager != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _videoFilterManager!.updatePlayerSize(newSize); + } + }); + } - return Video( - controller: controller!, - fit: _videoFilterManager?.currentBoxFit ?? BoxFit.contain, - controls: (state) => plexVideoControlsBuilder( - player!, - widget.metadata, - onNext: _nextEpisode != null ? _playNext : null, - onPrevious: _previousEpisode != null - ? _playPrevious - : null, - availableVersions: _availableVersions, - selectedMediaIndex: widget.selectedMediaIndex, - boxFitMode: _videoFilterManager?.boxFitMode ?? 0, - onCycleBoxFitMode: _cycleBoxFitMode, - onAudioTrackChanged: _onAudioTrackChanged, - onSubtitleTrackChanged: _onSubtitleTrackChanged, - ), - ); - }, + return MpvVideo( + player: player!, + fit: _videoFilterManager?.currentBoxFit ?? BoxFit.contain, + controls: (context) => plexVideoControlsBuilder( + player!, + widget.metadata, + onNext: _nextEpisode != null ? _playNext : null, + onPrevious: _previousEpisode != null + ? _playPrevious + : null, + availableVersions: _availableVersions, + selectedMediaIndex: widget.selectedMediaIndex, + boxFitMode: _videoFilterManager?.boxFitMode ?? 0, + onCycleBoxFitMode: _cycleBoxFitMode, + onAudioTrackChanged: _onAudioTrackChanged, + onSubtitleTrackChanged: _onSubtitleTrackChanged, + ), + ); + }, + ), ), - ), // Play Next Dialog if (_showPlayNextDialog && _nextEpisode != null) Positioned.fill( diff --git a/lib/services/episode_navigation_service.dart b/lib/services/episode_navigation_service.dart index 02c011c7..e81915fc 100644 --- a/lib/services/episode_navigation_service.dart +++ b/lib/services/episode_navigation_service.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:media_kit/media_kit.dart'; import 'package:provider/provider.dart'; +import '../mpv/mpv.dart'; import '../client/plex_client.dart'; import '../models/plex_metadata.dart'; import '../providers/playback_state_provider.dart'; @@ -77,14 +77,14 @@ class EpisodeNavigationService { Future navigateToEpisode({ required BuildContext context, required PlexMetadata episode, - required Player? player, + required MpvPlayer? player, bool usePushReplacement = true, }) async { if (!context.mounted) return; // Capture current player state before navigation - AudioTrack? currentAudioTrack; - SubtitleTrack? currentSubtitleTrack; + MpvAudioTrack? currentAudioTrack; + MpvSubtitleTrack? currentSubtitleTrack; double? currentPlaybackRate; if (player != null) { @@ -93,7 +93,7 @@ class EpisodeNavigationService { currentPlaybackRate = player.state.rate; appLogger.d( - 'Navigating to episode with preserved settings - Audio: ${currentAudioTrack.id}, Subtitle: ${currentSubtitleTrack.id}, Rate: ${currentPlaybackRate}x', + 'Navigating to episode with preserved settings - Audio: ${currentAudioTrack?.id}, Subtitle: ${currentSubtitleTrack?.id}, Rate: ${currentPlaybackRate}x', ); } diff --git a/lib/services/keyboard_shortcuts_service.dart b/lib/services/keyboard_shortcuts_service.dart index e67fd60c..22fa7298 100644 --- a/lib/services/keyboard_shortcuts_service.dart +++ b/lib/services/keyboard_shortcuts_service.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:media_kit/media_kit.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; +import '../mpv/mpv.dart'; import 'settings_service.dart'; import '../utils/keyboard_utils.dart'; import '../utils/player_utils.dart'; @@ -148,7 +148,7 @@ class KeyboardShortcutsService { // Handle keyboard input for video player KeyEventResult handleVideoPlayerKeyEvent( KeyEvent event, - Player player, + MpvPlayer player, VoidCallback? onToggleFullscreen, VoidCallback? onToggleSubtitles, VoidCallback? onNextAudioTrack, @@ -241,7 +241,7 @@ class KeyboardShortcutsService { void _executeAction( String action, - Player player, + MpvPlayer player, VoidCallback? onToggleFullscreen, VoidCallback? onToggleSubtitles, VoidCallback? onNextAudioTrack, diff --git a/lib/services/playback_initialization_service.dart b/lib/services/playback_initialization_service.dart index dc57abba..69afccdb 100644 --- a/lib/services/playback_initialization_service.dart +++ b/lib/services/playback_initialization_service.dart @@ -1,45 +1,21 @@ -import 'package:flutter/material.dart'; -import 'package:media_kit/media_kit.dart'; - import '../client/plex_client.dart'; -import '../models/plex_media_info.dart'; import '../models/plex_metadata.dart'; -import '../models/plex_user_profile.dart'; -import '../utils/app_logger.dart'; import '../i18n/strings.g.dart'; -import 'track_selection_service.dart'; -/// Service responsible for initializing video playback -/// -/// Handles the complex process of: -/// 1. Fetching video playback data from the Plex server -/// 2. Building external subtitle tracks -/// 3. Opening media in the player -/// 4. Adding external subtitles to the player -/// 5. Selecting and applying audio/subtitle tracks -/// 6. Seeking to resume position -/// 7. Starting playback +/// Service responsible for fetching video playback data from the Plex server class PlaybackInitializationService { - final Player player; final PlexClient client; - final BuildContext context; PlaybackInitializationService({ - required this.player, required this.client, - required this.context, }); - /// Start playback for the given metadata + /// Fetch playback data for the given metadata /// - /// Returns a PlaybackInitializationResult with available versions and other data - Future startPlayback({ + /// Returns a PlaybackInitializationResult with video URL and available versions + Future getPlaybackData({ required PlexMetadata metadata, required int selectedMediaIndex, - required PlexUserProfile? profileSettings, - AudioTrack? preferredAudioTrack, - SubtitleTrack? preferredSubtitleTrack, - double? preferredPlaybackRate, }) async { try { // Get consolidated playback data (URL, media info, and versions) in a single API call @@ -52,50 +28,10 @@ class PlaybackInitializationService { throw PlaybackException(t.messages.fileInfoNotAvailable); } - final videoUrl = playbackData.videoUrl!; - final mediaInfo = playbackData.mediaInfo; - - // Build list of external subtitle tracks for media_kit - final externalSubtitles = _buildExternalSubtitles(mediaInfo); - - // Open video (without external subtitles in Media constructor) - await player.open(Media(videoUrl), play: false); - - // Wait for media to be ready (duration > 0) - await _waitForMediaReady(); - - // Add external subtitle tracks without auto-selecting them - if (externalSubtitles.isNotEmpty) { - await _addExternalSubtitles(externalSubtitles); - } - - // Select and apply tracks BEFORE seeking to resume position - // This prevents track changes from resetting the playback position on Android - final trackSelectionService = TrackSelectionService( - player: player, - profileSettings: profileSettings, - metadata: metadata, - ); - - await trackSelectionService.selectAndApplyTracks( - preferredAudioTrack: preferredAudioTrack, - preferredSubtitleTrack: preferredSubtitleTrack, - preferredPlaybackRate: preferredPlaybackRate, - ); - - // Set up playback position if resuming (AFTER track selection) - if (metadata.viewOffset != null && metadata.viewOffset! > 0) { - appLogger.d('Resuming playback at ${metadata.viewOffset} ms'); - final resumePosition = Duration(milliseconds: metadata.viewOffset!); - await player.seek(resumePosition); - } - - // Start playback after seeking - await player.play(); - - // Return result with available versions for UI updates + // Return result with available versions and video URL return PlaybackInitializationResult( availableVersions: playbackData.availableVersions, + videoUrl: playbackData.videoUrl!, ); } catch (e) { if (e is PlaybackException) { @@ -104,103 +40,17 @@ class PlaybackInitializationService { throw PlaybackException(t.messages.errorLoading(error: e.toString())); } } - - /// Build list of external subtitle tracks from media info - List _buildExternalSubtitles(PlexMediaInfo? mediaInfo) { - final externalSubtitles = []; - - if (mediaInfo == null) { - return externalSubtitles; - } - - final externalTracks = mediaInfo.subtitleTracks - .where((PlexSubtitleTrack track) => track.isExternal) - .toList(); - - if (externalTracks.isNotEmpty) { - appLogger.d('Found ${externalTracks.length} external subtitle track(s)'); - } - - for (final plexTrack in externalTracks) { - try { - // Skip if no auth token is available - final token = client.config.token; - if (token == null) { - appLogger.w('No auth token available for external subtitles'); - continue; - } - - final url = plexTrack.getSubtitleUrl(client.config.baseUrl, token); - - // Skip if URL couldn't be constructed - if (url == null) continue; - - externalSubtitles.add( - SubtitleTrack.uri( - url, - title: - plexTrack.displayTitle ?? - plexTrack.language ?? - 'Track ${plexTrack.id}', - language: plexTrack.languageCode, - ), - ); - } catch (e) { - // Silent fallback - log error but continue with other subtitles - appLogger.w( - 'Failed to add external subtitle track ${plexTrack.id}', - error: e, - ); - } - } - - return externalSubtitles; - } - - /// Wait for media to be ready (duration > 0) - Future _waitForMediaReady() async { - int attempts = 0; - while (player.state.duration.inMilliseconds == 0 && attempts < 100) { - await Future.delayed(const Duration(milliseconds: 100)); - attempts++; - } - } - - /// Add external subtitle tracks to the player without auto-selecting them - Future _addExternalSubtitles( - List externalSubtitles, - ) async { - appLogger.d( - 'Adding ${externalSubtitles.length} external subtitle(s) to player', - ); - - final nativePlayer = player.platform as dynamic; - - for (final subtitleTrack in externalSubtitles) { - try { - // Use mpv's sub-add with 'auto' flag to avoid auto-selection - await nativePlayer.command([ - 'sub-add', - subtitleTrack.id, - 'auto', - subtitleTrack.title ?? 'external', - subtitleTrack.language ?? 'auto', - ]); - } catch (e) { - appLogger.w( - 'Failed to add external subtitle: ${subtitleTrack.title}', - error: e, - ); - } - } - } } /// Result of playback initialization class PlaybackInitializationResult { final List availableVersions; + final String? videoUrl; - PlaybackInitializationResult({required this.availableVersions}); + PlaybackInitializationResult({ + required this.availableVersions, + this.videoUrl, + }); } /// Exception thrown when playback initialization fails diff --git a/lib/services/playback_progress_tracker.dart b/lib/services/playback_progress_tracker.dart index 129b1454..ce4a0bd1 100644 --- a/lib/services/playback_progress_tracker.dart +++ b/lib/services/playback_progress_tracker.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:media_kit/media_kit.dart'; +import '../mpv/mpv.dart'; import '../client/plex_client.dart'; import '../models/plex_metadata.dart'; @@ -15,7 +15,7 @@ import '../utils/app_logger.dart'; class PlaybackProgressTracker { final PlexClient client; final PlexMetadata metadata; - final Player player; + final MpvPlayer player; /// Timer for periodic progress updates Timer? _progressTimer; diff --git a/lib/services/track_selection_service.dart b/lib/services/track_selection_service.dart index 8000b532..093b39d9 100644 --- a/lib/services/track_selection_service.dart +++ b/lib/services/track_selection_service.dart @@ -1,4 +1,4 @@ -import 'package:media_kit/media_kit.dart'; +import '../mpv/mpv.dart'; import '../models/plex_metadata.dart'; import '../models/plex_user_profile.dart'; @@ -8,7 +8,7 @@ import '../utils/language_codes.dart'; /// Service for selecting and applying audio and subtitle tracks based on /// preferences, user profiles, and per-media settings. class TrackSelectionService { - final Player player; + final MpvPlayer player; final PlexUserProfile? profileSettings; final PlexMetadata metadata; @@ -69,11 +69,11 @@ class TrackSelectionService { return null; } - AudioTrack? findBestAudioMatch( - List availableTracks, - AudioTrack preferred, + MpvAudioTrack? findBestAudioMatch( + List availableTracks, + MpvAudioTrack preferred, ) { - return findBestTrackMatch( + return findBestTrackMatch( availableTracks, preferred, (t) => t.id, @@ -82,8 +82,8 @@ class TrackSelectionService { ); } - AudioTrack? findAudioTrackByProfile( - List availableTracks, + MpvAudioTrack? findAudioTrackByProfile( + List availableTracks, PlexUserProfile profile, ) { appLogger.d('Audio track selection using user profile'); @@ -123,7 +123,7 @@ class TrackSelectionService { 'Checking language variations for "$preferredLanguage": ${languageVariations.join(", ")}', ); - final match = _findTrackByLanguageVariations( + final match = _findTrackByLanguageVariations( availableTracks, preferredLanguage, languageVariations, @@ -140,16 +140,16 @@ class TrackSelectionService { return null; } - SubtitleTrack? findBestSubtitleMatch( - List availableTracks, - SubtitleTrack preferred, + MpvSubtitleTrack? findBestSubtitleMatch( + List availableTracks, + MpvSubtitleTrack preferred, ) { // Handle special "no subtitles" case if (preferred.id == 'no') { - return SubtitleTrack.no(); + return MpvSubtitleTrack.off; } - return findBestTrackMatch( + return findBestTrackMatch( availableTracks, preferred, (t) => t.id, @@ -158,10 +158,10 @@ class TrackSelectionService { ); } - SubtitleTrack? findSubtitleTrackByProfile( - List availableTracks, + MpvSubtitleTrack? findSubtitleTrackByProfile( + List availableTracks, PlexUserProfile profile, { - AudioTrack? selectedAudioTrack, + MpvAudioTrack? selectedAudioTrack, }) { appLogger.d('Subtitle track selection using user profile'); appLogger.d( @@ -178,7 +178,7 @@ class TrackSelectionService { appLogger.d( 'Profile specifies manual mode (autoSelectSubtitle=0) - Subtitles OFF', ); - return SubtitleTrack.no(); + return MpvSubtitleTrack.off; } // Mode 1: Shown with foreign audio @@ -201,7 +201,7 @@ class TrackSelectionService { // If audio matches preferred language, no subtitles needed if (audioLang != null && languageVariations.contains(audioLang)) { appLogger.d('Audio matches preferred language - Subtitles OFF'); - return SubtitleTrack.no(); + return MpvSubtitleTrack.off; } appLogger.d('Foreign audio detected - enabling subtitles'); } @@ -258,7 +258,7 @@ class TrackSelectionService { 'Checking language variations for "$preferredLanguage": ${languageVariations.join(", ")}', ); - final match = _findTrackByLanguageVariations( + final match = _findTrackByLanguageVariations( candidateTracks, preferredLanguage, languageVariations, @@ -282,8 +282,8 @@ class TrackSelectionService { /// - 1: Prefer SDH subtitles /// - 2: Only show SDH subtitles /// - 3: Only show non-SDH subtitles - List filterSubtitlesBySDH( - List tracks, + List filterSubtitlesBySDH( + List tracks, int preference, ) { if (preference == 0 || preference == 1) { @@ -319,8 +319,8 @@ class TrackSelectionService { /// - 1: Prefer forced subtitles /// - 2: Only show forced subtitles /// - 3: Only show non-forced subtitles - List filterSubtitlesByForced( - List tracks, + List filterSubtitlesByForced( + List tracks, int preference, ) { if (preference == 0 || preference == 1) { @@ -353,8 +353,8 @@ class TrackSelectionService { /// Checks if a subtitle track is SDH (Subtitles for Deaf or Hard-of-Hearing) /// - /// Since media_kit may not expose this directly, we infer from the title - bool isSDH(SubtitleTrack track) { + /// Since mpv may not expose this directly, we infer from the title + bool isSDH(MpvSubtitleTrack track) { final title = track.title?.toLowerCase() ?? ''; // Look for common SDH indicators @@ -365,7 +365,7 @@ class TrackSelectionService { } /// Checks if a subtitle track is forced - bool isForced(SubtitleTrack track) { + bool isForced(MpvSubtitleTrack track) { final title = track.title?.toLowerCase() ?? ''; return title.contains('forced'); } @@ -423,8 +423,8 @@ class TrackSelectionService { /// Log available tracks for debugging void logAvailableTracks( - List audioTracks, - List subtitleTracks, + List audioTracks, + List subtitleTracks, ) { appLogger.d('Available audio tracks: ${audioTracks.length}'); for (var track in audioTracks) { @@ -445,13 +445,13 @@ class TrackSelectionService { /// Priority 2: Per-media language preference /// Priority 3: User profile preferences /// Priority 4: Default or first track - AudioTrack? selectAudioTrack( - List availableTracks, - AudioTrack? preferredAudioTrack, + MpvAudioTrack? selectAudioTrack( + List availableTracks, + MpvAudioTrack? preferredAudioTrack, ) { if (availableTracks.isEmpty) return null; - AudioTrack? trackToSelect; + MpvAudioTrack? trackToSelect; // Priority 1: Try to match preferred track from navigation if (preferredAudioTrack != null) { @@ -522,19 +522,19 @@ class TrackSelectionService { /// Priority 3: User profile preferences /// Priority 4: Default track /// Priority 5: Off - SubtitleTrack selectSubtitleTrack( - List availableTracks, - SubtitleTrack? preferredSubtitleTrack, - AudioTrack? selectedAudioTrack, + MpvSubtitleTrack selectSubtitleTrack( + List availableTracks, + MpvSubtitleTrack? preferredSubtitleTrack, + MpvAudioTrack? selectedAudioTrack, ) { - SubtitleTrack? subtitleToSelect; + MpvSubtitleTrack? subtitleToSelect; // Priority 1: Try preferred track from navigation (always wins) if (preferredSubtitleTrack != null) { appLogger.d('Priority 1: Checking preferred track from navigation'); if (preferredSubtitleTrack.id == 'no') { appLogger.d(' Preferred: OFF'); - return SubtitleTrack.no(); + return MpvSubtitleTrack.off; } else if (availableTracks.isNotEmpty) { appLogger.d( ' Preferred: ${preferredSubtitleTrack.title ?? "Track ${preferredSubtitleTrack.id}"} (${preferredSubtitleTrack.language ?? "unknown"})', @@ -566,7 +566,7 @@ class TrackSelectionService { if (metadata.subtitleLanguage == 'none' || metadata.subtitleLanguage!.isEmpty) { appLogger.d(' Per-media preference: Subtitles OFF'); - return SubtitleTrack.no(); + return MpvSubtitleTrack.off; } else if (availableTracks.isNotEmpty) { final matchedTrack = availableTracks.firstWhere( (track) => languageMatches(track.language, metadata.subtitleLanguage), @@ -615,16 +615,16 @@ class TrackSelectionService { // Priority 5: If still no subtitle selected, turn off appLogger.d('Priority 5: No subtitle selected - Subtitles OFF'); - return SubtitleTrack.no(); + return MpvSubtitleTrack.off; } /// Select and apply audio and subtitle tracks based on preferences Future selectAndApplyTracks({ - AudioTrack? preferredAudioTrack, - SubtitleTrack? preferredSubtitleTrack, + MpvAudioTrack? preferredAudioTrack, + MpvSubtitleTrack? preferredSubtitleTrack, double? preferredPlaybackRate, - Function(AudioTrack)? onAudioTrackChanged, - Function(SubtitleTrack)? onSubtitleTrackChanged, + Function(MpvAudioTrack)? onAudioTrackChanged, + Function(MpvSubtitleTrack)? onSubtitleTrackChanged, }) async { // Wait for tracks to be loaded int attempts = 0; @@ -658,7 +658,7 @@ class TrackSelectionService { appLogger.i( 'Final audio selection: ${selectedAudioTrack.title ?? "Track ${selectedAudioTrack.id}"} (${selectedAudioTrack.language ?? "unknown"})', ); - player.setAudioTrack(selectedAudioTrack); + player.selectAudioTrack(selectedAudioTrack); } else { appLogger.d('No audio tracks available'); } @@ -674,7 +674,7 @@ class TrackSelectionService { ? 'OFF' : '${selectedSubtitleTrack.title ?? "Track ${selectedSubtitleTrack.id}"} (${selectedSubtitleTrack.language ?? "unknown"})'; appLogger.i('Final subtitle selection: $finalSubtitle'); - player.setSubtitleTrack(selectedSubtitleTrack); + player.selectSubtitleTrack(selectedSubtitleTrack); // Set playback rate if preferred rate was provided if (preferredPlaybackRate != null) { diff --git a/lib/services/video_filter_manager.dart b/lib/services/video_filter_manager.dart index d9b147d7..b8b5a90f 100644 --- a/lib/services/video_filter_manager.dart +++ b/lib/services/video_filter_manager.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:media_kit/media_kit.dart'; import 'package:rate_limiter/rate_limiter.dart'; +import '../mpv/mpv.dart'; + import '../models/plex_media_version.dart'; import '../utils/app_logger.dart'; @@ -13,7 +14,7 @@ import '../utils/app_logger.dart'; /// - Subtitle positioning adjustments based on crop parameters /// - Debounced video filter updates on resize events class VideoFilterManager { - final Player player; + final MpvPlayer player; final List availableVersions; final int selectedMediaIndex; @@ -203,8 +204,6 @@ class VideoFilterManager { /// Update the video filter based on current crop mode void updateVideoFilter() async { try { - final nativePlayer = player.platform as dynamic; - if (_boxFitMode == 1) { // Fill screen mode - apply crop filter _videoSize = _getCurrentVideoSize(); @@ -218,7 +217,7 @@ class VideoFilterManager { ); // Apply crop filter - await nativePlayer.setProperty('vf', cropFilter); + await player.setProperty('vf', cropFilter); // Apply subtitle margins and scaling to compensate for crop zoom final subMarginX = cropParams['subMarginX']!; @@ -229,28 +228,28 @@ class VideoFilterManager { 'Applying subtitle properties - margins: x=$subMarginX, y=$subMarginY, scale=$subScale', ); - await nativePlayer.setProperty('sub-margin-x', subMarginX.toString()); - await nativePlayer.setProperty('sub-margin-y', subMarginY.toString()); - await nativePlayer.setProperty('sub-scale', subScale.toString()); + await player.setProperty('sub-margin-x', subMarginX.toString()); + await player.setProperty('sub-margin-y', subMarginY.toString()); + await player.setProperty('sub-scale', subScale.toString()); } else { // Clear filter but apply base margins if no cropping needed appLogger.d( 'Clearing video filter - aspect ratios similar, applying base margins (player: $_playerSize, video: $_videoSize)', ); - await nativePlayer.setProperty('vf', ''); - await nativePlayer.setProperty('sub-margin-x', '20'); // Base margin - await nativePlayer.setProperty('sub-margin-y', '40'); // Base margin - await nativePlayer.setProperty('sub-scale', '1.0'); // Reset scale + await player.setProperty('vf', ''); + await player.setProperty('sub-margin-x', '20'); // Base margin + await player.setProperty('sub-margin-y', '40'); // Base margin + await player.setProperty('sub-scale', '1.0'); // Reset scale } } else { // Other modes - clear video filter but apply base margins appLogger.d( 'Clearing video filter, applying base margins - BoxFit mode $_boxFitMode', ); - await nativePlayer.setProperty('vf', ''); - await nativePlayer.setProperty('sub-margin-x', '20'); // Base margin - await nativePlayer.setProperty('sub-margin-y', '40'); // Base margin - await nativePlayer.setProperty('sub-scale', '1.0'); // Reset scale + await player.setProperty('vf', ''); + await player.setProperty('sub-margin-x', '20'); // Base margin + await player.setProperty('sub-margin-y', '40'); // Base margin + await player.setProperty('sub-scale', '1.0'); // Reset scale } } catch (e) { appLogger.w('Failed to update video filter', error: e); diff --git a/lib/utils/player_utils.dart b/lib/utils/player_utils.dart index 4f1de3e1..98f8e9ff 100644 --- a/lib/utils/player_utils.dart +++ b/lib/utils/player_utils.dart @@ -1,8 +1,8 @@ -import 'package:media_kit/media_kit.dart'; +import '../mpv/mpv.dart'; /// Seeks by the given offset (can be positive or negative) while clamping /// the result between 0 and the video duration -void seekWithClamping(Player player, Duration offset) { +void seekWithClamping(MpvPlayer player, Duration offset) { final currentPosition = player.state.position; final duration = player.state.duration; final newPosition = currentPosition + offset; diff --git a/lib/utils/video_player_navigation.dart b/lib/utils/video_player_navigation.dart index d0996f21..41032d41 100644 --- a/lib/utils/video_player_navigation.dart +++ b/lib/utils/video_player_navigation.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:media_kit/media_kit.dart'; + +import '../mpv/mpv.dart'; import '../models/plex_metadata.dart'; import '../screens/video_player_screen.dart'; import '../services/settings_service.dart'; @@ -26,8 +27,8 @@ import '../services/settings_service.dart'; Future navigateToVideoPlayer( BuildContext context, { required PlexMetadata metadata, - AudioTrack? preferredAudioTrack, - SubtitleTrack? preferredSubtitleTrack, + MpvAudioTrack? preferredAudioTrack, + MpvSubtitleTrack? preferredSubtitleTrack, double? preferredPlaybackRate, int? selectedMediaIndex, bool usePushReplacement = false, diff --git a/lib/widgets/video_controls/desktop_video_controls.dart b/lib/widgets/video_controls/desktop_video_controls.dart index 7e2fcf93..ea1f1fad 100644 --- a/lib/widgets/video_controls/desktop_video_controls.dart +++ b/lib/widgets/video_controls/desktop_video_controls.dart @@ -1,8 +1,8 @@ import 'dart:io' show Platform; import 'package:flutter/material.dart'; -import 'package:media_kit/media_kit.dart'; +import '../../mpv/mpv.dart'; import '../../models/plex_media_info.dart'; import '../../models/plex_metadata.dart'; import '../../services/fullscreen_state_manager.dart'; @@ -14,7 +14,7 @@ import 'widgets/timeline_slider.dart'; /// Desktop-specific video controls layout with top bar and bottom controls class DesktopVideoControls extends StatelessWidget { - final Player player; + final MpvPlayer player; final PlexMetadata metadata; final VoidCallback? onNext; final VoidCallback? onPrevious; @@ -158,11 +158,11 @@ class DesktopVideoControls extends StatelessWidget { children: [ // Row 1: Timeline with time indicators StreamBuilder( - stream: player.stream.position, + stream: player.streams.position, initialData: player.state.position, builder: (context, positionSnapshot) { return StreamBuilder( - stream: player.stream.duration, + stream: player.streams.duration, initialData: player.state.duration, builder: (context, durationSnapshot) { final position = positionSnapshot.data ?? Duration.zero; @@ -238,7 +238,7 @@ class DesktopVideoControls extends StatelessWidget { ), // Play/Pause StreamBuilder( - stream: player.stream.playing, + stream: player.streams.playing, initialData: player.state.playing, builder: (context, snapshot) { final isPlaying = snapshot.data ?? false; diff --git a/lib/widgets/video_controls/mobile_video_controls.dart b/lib/widgets/video_controls/mobile_video_controls.dart index 598f46b2..551e76dc 100644 --- a/lib/widgets/video_controls/mobile_video_controls.dart +++ b/lib/widgets/video_controls/mobile_video_controls.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:media_kit/media_kit.dart'; +import '../../mpv/mpv.dart'; import '../../models/plex_media_info.dart'; import '../../models/plex_metadata.dart'; import '../../utils/desktop_window_padding.dart'; @@ -18,7 +18,7 @@ import 'widgets/timeline_slider.dart'; /// - Center: Large playback controls (seek backward, play/pause, seek forward) /// - Bottom bar: Timeline slider with chapter markers and timestamps class MobileVideoControls extends StatelessWidget { - final Player player; + final MpvPlayer player; final PlexMetadata metadata; final List chapters; final bool chaptersLoaded; @@ -114,7 +114,7 @@ class MobileVideoControls extends StatelessWidget { Widget _buildPlaybackControls(BuildContext context) { return StreamBuilder( - stream: player.stream.playing, + stream: player.streams.playing, initialData: player.state.playing, builder: (context, snapshot) { final isPlaying = snapshot.data ?? false; @@ -214,11 +214,11 @@ class MobileVideoControls extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(16), child: StreamBuilder( - stream: player.stream.position, + stream: player.streams.position, initialData: player.state.position, builder: (context, positionSnapshot) { return StreamBuilder( - stream: player.stream.duration, + stream: player.streams.duration, initialData: player.state.duration, builder: (context, durationSnapshot) { final position = positionSnapshot.data ?? Duration.zero; diff --git a/lib/widgets/video_controls/sheets/audio_track_sheet.dart b/lib/widgets/video_controls/sheets/audio_track_sheet.dart index 76973f54..65897683 100644 --- a/lib/widgets/video_controls/sheets/audio_track_sheet.dart +++ b/lib/widgets/video_controls/sheets/audio_track_sheet.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:media_kit/media_kit.dart'; + +import '../../../mpv/mpv.dart'; import '../../../i18n/strings.g.dart'; import 'track_selection_sheet.dart'; @@ -7,10 +8,10 @@ import 'track_selection_sheet.dart'; class AudioTrackSheet { static void show( BuildContext context, - Player player, { - Function(AudioTrack)? onTrackChanged, + MpvPlayer player, { + Function(MpvAudioTrack)? onTrackChanged, }) { - TrackSelectionSheet.show( + TrackSelectionSheet.show( context: context, player: player, title: t.videoControls.audioLabel, @@ -28,12 +29,12 @@ class AudioTrackSheet { if (audioTrack.codec != null && audioTrack.codec!.isNotEmpty) { parts.add(audioTrack.codec!.toUpperCase()); } - if (audioTrack.channelscount != null) { - parts.add('${audioTrack.channelscount}ch'); + if (audioTrack.channelsCount != null) { + parts.add('${audioTrack.channelsCount}ch'); } return parts.isEmpty ? 'Audio Track ${index + 1}' : parts.join(' · '); }, - setTrack: (track) => player.setAudioTrack(track), + setTrack: (track) => player.selectAudioTrack(track), onTrackChanged: onTrackChanged, ); } diff --git a/lib/widgets/video_controls/sheets/chapter_sheet.dart b/lib/widgets/video_controls/sheets/chapter_sheet.dart index 65ced99d..65d34179 100644 --- a/lib/widgets/video_controls/sheets/chapter_sheet.dart +++ b/lib/widgets/video_controls/sheets/chapter_sheet.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:media_kit/media_kit.dart'; + +import '../../../mpv/mpv.dart'; import '../../../client/plex_client.dart'; import '../../../models/plex_media_info.dart'; import '../../../utils/duration_formatter.dart'; @@ -8,7 +9,7 @@ import 'base_video_control_sheet.dart'; /// Bottom sheet for selecting chapters class ChapterSheet extends StatelessWidget { - final Player player; + final MpvPlayer player; final List chapters; final bool chaptersLoaded; final String? serverId; // Server ID for the metadata these chapters belong to @@ -23,7 +24,7 @@ class ChapterSheet extends StatelessWidget { static void show( BuildContext context, - Player player, + MpvPlayer player, List chapters, bool chaptersLoaded, { String? serverId, @@ -47,7 +48,7 @@ class ChapterSheet extends StatelessWidget { @override Widget build(BuildContext context) { return StreamBuilder( - stream: player.stream.position, + stream: player.streams.position, initialData: player.state.position, builder: (context, positionSnapshot) { final currentPosition = positionSnapshot.data ?? Duration.zero; diff --git a/lib/widgets/video_controls/sheets/subtitle_track_sheet.dart b/lib/widgets/video_controls/sheets/subtitle_track_sheet.dart index a60c7865..4630116f 100644 --- a/lib/widgets/video_controls/sheets/subtitle_track_sheet.dart +++ b/lib/widgets/video_controls/sheets/subtitle_track_sheet.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:media_kit/media_kit.dart'; + +import '../../../mpv/mpv.dart'; import '../../../i18n/strings.g.dart'; import 'track_selection_sheet.dart'; @@ -7,10 +8,10 @@ import 'track_selection_sheet.dart'; class SubtitleTrackSheet { static void show( BuildContext context, - Player player, { - Function(SubtitleTrack)? onTrackChanged, + MpvPlayer player, { + Function(MpvSubtitleTrack)? onTrackChanged, }) { - TrackSelectionSheet.show( + TrackSelectionSheet.show( context: context, player: player, title: t.videoControls.subtitlesLabel, @@ -41,10 +42,10 @@ class SubtitleTrackSheet { } return parts.isEmpty ? 'Track ${index + 1}' : parts.join(' · '); }, - setTrack: (track) => player.setSubtitleTrack(track), + setTrack: (track) => player.selectSubtitleTrack(track), onTrackChanged: onTrackChanged, showOffOption: true, - createOffTrack: () => SubtitleTrack.no(), + createOffTrack: () => MpvSubtitleTrack.off, isOffTrack: (track) => track.id == 'no', ); } diff --git a/lib/widgets/video_controls/sheets/track_selection_sheet.dart b/lib/widgets/video_controls/sheets/track_selection_sheet.dart index 1439333b..b7bae665 100644 --- a/lib/widgets/video_controls/sheets/track_selection_sheet.dart +++ b/lib/widgets/video_controls/sheets/track_selection_sheet.dart @@ -1,16 +1,17 @@ import 'package:flutter/material.dart'; -import 'package:media_kit/media_kit.dart'; + +import '../../../mpv/mpv.dart'; import 'base_video_control_sheet.dart'; /// Generic track selection sheet for audio and subtitle tracks /// -/// Type parameter [T] should be either [AudioTrack] or [SubtitleTrack] +/// Type parameter [T] should be either [MpvAudioTrack] or [MpvSubtitleTrack] class TrackSelectionSheet extends StatelessWidget { - final Player player; + final MpvPlayer player; final String title; final IconData icon; - final List Function(Tracks?) extractTracks; - final T Function(Track) getCurrentTrack; + final List Function(MpvTracks?) extractTracks; + final T? Function(MpvTrack) getCurrentTrack; final String Function(T track, int index) buildLabel; final void Function(T track) setTrack; final Function(T)? onTrackChanged; @@ -35,11 +36,11 @@ class TrackSelectionSheet extends StatelessWidget { static void show({ required BuildContext context, - required Player player, + required MpvPlayer player, required String title, required IconData icon, - required List Function(Tracks?) extractTracks, - required T Function(Track) getCurrentTrack, + required List Function(MpvTracks?) extractTracks, + required T? Function(MpvTrack) getCurrentTrack, required String Function(T track, int index) buildLabel, required void Function(T track) setTrack, Function(T)? onTrackChanged, @@ -66,9 +67,9 @@ class TrackSelectionSheet extends StatelessWidget { } String _getEmptyMessage() { - if (T == SubtitleTrack) { + if (T == MpvSubtitleTrack) { return 'No subtitles available'; - } else if (T == AudioTrack) { + } else if (T == MpvAudioTrack) { return 'No audio tracks available'; } return 'No tracks available'; @@ -76,16 +77,16 @@ class TrackSelectionSheet extends StatelessWidget { @override Widget build(BuildContext context) { - return StreamBuilder( - stream: player.stream.tracks, + return StreamBuilder( + stream: player.streams.tracks, initialData: player.state.tracks, builder: (context, snapshot) { final tracks = snapshot.data; final availableTracks = extractTracks(tracks).where((track) { // Filter out 'auto' and 'no' tracks from the list - if (track is AudioTrack) { + if (track is MpvAudioTrack) { return track.id != 'auto' && track.id != 'no'; - } else if (track is SubtitleTrack) { + } else if (track is MpvSubtitleTrack) { return track.id != 'auto' && track.id != 'no'; } return true; @@ -101,17 +102,17 @@ class TrackSelectionSheet extends StatelessWidget { style: const TextStyle(color: Colors.white70), ), ) - : StreamBuilder( - stream: player.stream.track, + : StreamBuilder( + stream: player.streams.track, initialData: player.state.track, builder: (context, selectedSnapshot) { final currentTrack = selectedSnapshot.data ?? player.state.track; final selectedTrack = getCurrentTrack(currentTrack); - // Determine if "Off" is selected - final isOffSelected = - isOffTrack?.call(selectedTrack) ?? false; + // Determine if "Off" is selected (null or explicit off) + final isOffSelected = selectedTrack == null || + (isOffTrack?.call(selectedTrack) ?? false); final itemCount = availableTracks.length + (showOffOption ? 1 : 0); @@ -150,18 +151,20 @@ class TrackSelectionSheet extends StatelessWidget { // Check if this track is selected String trackId; - if (track is AudioTrack) { + if (track is MpvAudioTrack) { trackId = track.id; - } else if (track is SubtitleTrack) { + } else if (track is MpvSubtitleTrack) { trackId = track.id; } else { trackId = ''; } String selectedId; - if (selectedTrack is AudioTrack) { + if (selectedTrack == null) { + selectedId = ''; + } else if (selectedTrack is MpvAudioTrack) { selectedId = selectedTrack.id; - } else if (selectedTrack is SubtitleTrack) { + } else if (selectedTrack is MpvSubtitleTrack) { selectedId = selectedTrack.id; } else { selectedId = ''; diff --git a/lib/widgets/video_controls/sheets/video_settings_sheet.dart b/lib/widgets/video_controls/sheets/video_settings_sheet.dart index 37a6402b..47fd1493 100644 --- a/lib/widgets/video_controls/sheets/video_settings_sheet.dart +++ b/lib/widgets/video_controls/sheets/video_settings_sheet.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:media_kit/media_kit.dart'; + +import '../../../mpv/mpv.dart'; import '../../../services/settings_service.dart'; import '../../../services/sleep_timer_service.dart'; import '../../../utils/platform_detector.dart'; @@ -58,7 +59,7 @@ class _SettingsMenuItem extends StatelessWidget { /// Unified settings sheet for playback adjustments with in-sheet navigation class VideoSettingsSheet extends StatefulWidget { - final Player player; + final MpvPlayer player; final int audioSyncOffset; final int subtitleSyncOffset; @@ -71,7 +72,7 @@ class VideoSettingsSheet extends StatefulWidget { static Future show( BuildContext context, - Player player, + MpvPlayer player, int audioSyncOffset, int subtitleSyncOffset, ) { @@ -200,7 +201,7 @@ class _VideoSettingsSheetState extends State { children: [ // Playback Speed StreamBuilder( - stream: widget.player.stream.rate, + stream: widget.player.streams.rate, initialData: widget.player.state.rate, builder: (context, snapshot) { final currentRate = snapshot.data ?? 1.0; @@ -248,8 +249,8 @@ class _VideoSettingsSheetState extends State { // Audio Output Device (Desktop only) if (isDesktop) - StreamBuilder( - stream: widget.player.stream.audioDevice, + StreamBuilder( + stream: widget.player.streams.audioDevice, initialData: widget.player.state.audioDevice, builder: (context, snapshot) { final currentDevice = @@ -273,7 +274,7 @@ class _VideoSettingsSheetState extends State { Widget _buildSpeedView() { return StreamBuilder( - stream: widget.player.stream.rate, + stream: widget.player.streams.rate, initialData: widget.player.state.rate, builder: (context, snapshot) { final currentRate = snapshot.data ?? 1.0; @@ -352,14 +353,14 @@ class _VideoSettingsSheetState extends State { } Widget _buildAudioDeviceView() { - return StreamBuilder>( - stream: widget.player.stream.audioDevices, + return StreamBuilder>( + stream: widget.player.streams.audioDevices, initialData: widget.player.state.audioDevices, builder: (context, snapshot) { final devices = snapshot.data ?? []; - return StreamBuilder( - stream: widget.player.stream.audioDevice, + return StreamBuilder( + stream: widget.player.streams.audioDevice, initialData: widget.player.state.audioDevice, builder: (context, selectedSnapshot) { final currentDevice = diff --git a/lib/widgets/video_controls/video_controls.dart b/lib/widgets/video_controls/video_controls.dart index b8216faa..e26c2aa7 100644 --- a/lib/widgets/video_controls/video_controls.dart +++ b/lib/widgets/video_controls/video_controls.dart @@ -5,9 +5,10 @@ import 'package:flutter/material.dart'; import 'package:rate_limiter/rate_limiter.dart'; import 'package:flutter/services.dart' show SystemChrome, DeviceOrientation; import 'package:macos_window_utils/macos_window_utils.dart'; -import 'package:media_kit/media_kit.dart'; import 'package:window_manager/window_manager.dart'; +import '../../mpv/mpv.dart'; + import '../../client/plex_client.dart'; import '../../models/plex_media_info.dart'; import '../../models/plex_media_version.dart'; @@ -27,7 +28,7 @@ import 'desktop_video_controls.dart'; /// Custom video controls builder for Plex with chapter, audio, and subtitle support Widget plexVideoControlsBuilder( - Player player, + MpvPlayer player, PlexMetadata metadata, { VoidCallback? onNext, VoidCallback? onPrevious, @@ -35,8 +36,8 @@ Widget plexVideoControlsBuilder( int? selectedMediaIndex, int boxFitMode = 0, VoidCallback? onCycleBoxFitMode, - Function(AudioTrack)? onAudioTrackChanged, - Function(SubtitleTrack)? onSubtitleTrackChanged, + Function(MpvAudioTrack)? onAudioTrackChanged, + Function(MpvSubtitleTrack)? onSubtitleTrackChanged, }) { return PlexVideoControls( player: player, @@ -53,7 +54,7 @@ Widget plexVideoControlsBuilder( } class PlexVideoControls extends StatefulWidget { - final Player player; + final MpvPlayer player; final PlexMetadata metadata; final VoidCallback? onNext; final VoidCallback? onPrevious; @@ -61,8 +62,8 @@ class PlexVideoControls extends StatefulWidget { final int selectedMediaIndex; final int boxFitMode; final VoidCallback? onCycleBoxFitMode; - final Function(AudioTrack)? onAudioTrackChanged; - final Function(SubtitleTrack)? onSubtitleTrackChanged; + final Function(MpvAudioTrack)? onAudioTrackChanged; + final Function(MpvSubtitleTrack)? onSubtitleTrackChanged; const PlexVideoControls({ super.key, @@ -114,6 +115,9 @@ class _PlexVideoControlsState extends State bool _markersLoaded = false; // Playback state subscription for auto-hide timer StreamSubscription? _playingSubscription; + // Window resize pause state + Timer? _resizeDebounceTimer; + bool _wasPlayingBeforeResize = false; @override void initState() { @@ -145,7 +149,7 @@ class _PlexVideoControlsState extends State } void _listenToPosition() { - widget.player.stream.position.listen((position) { + widget.player.streams.position.listen((position) { if (_markers.isEmpty || !_markersLoaded) { return; } @@ -170,7 +174,7 @@ class _PlexVideoControlsState extends State /// Listen to playback state changes to manage auto-hide timer on iOS/mobile void _listenToPlayingState() { - _playingSubscription = widget.player.stream.playing.listen((isPlaying) { + _playingSubscription = widget.player.streams.playing.listen((isPlaying) { if (isPlaying && _showControls) { _startHideTimer(); } else if (!isPlaying) { @@ -240,6 +244,7 @@ class _PlexVideoControlsState extends State void dispose() { _hideTimer?.cancel(); _feedbackTimer?.cancel(); + _resizeDebounceTimer?.cancel(); _seekThrottle.cancel(); _playingSubscription?.cancel(); _focusNode.dispose(); @@ -298,6 +303,25 @@ class _PlexVideoControlsState extends State } } + @override + void onWindowResize() { + // Pause video while resizing to prevent lag + if (_resizeDebounceTimer == null && widget.player.state.playing) { + _wasPlayingBeforeResize = true; + widget.player.pause(); + } + + // Reset debounce timer - resume when resizing stops + _resizeDebounceTimer?.cancel(); + _resizeDebounceTimer = Timer(const Duration(milliseconds: 300), () { + if (_wasPlayingBeforeResize && mounted) { + widget.player.play(); + } + _wasPlayingBeforeResize = false; + _resizeDebounceTimer = null; + }); + } + void _startHideTimer() { _hideTimer?.cancel(); // Only auto-hide if playing diff --git a/lib/widgets/video_controls/widgets/sleep_timer_content.dart b/lib/widgets/video_controls/widgets/sleep_timer_content.dart index c23611d0..e1521777 100644 --- a/lib/widgets/video_controls/widgets/sleep_timer_content.dart +++ b/lib/widgets/video_controls/widgets/sleep_timer_content.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:media_kit/media_kit.dart'; + +import '../../../mpv/mpv.dart'; import '../../../services/sleep_timer_service.dart'; import 'sleep_timer_active_status.dart'; import 'sleep_timer_duration_list.dart'; /// Shared UI for sleep timer selection and active status. class SleepTimerContent extends StatelessWidget { - final Player player; + final MpvPlayer player; final SleepTimerService sleepTimer; final int? defaultDuration; final VoidCallback? onCancel; diff --git a/lib/widgets/video_controls/widgets/sleep_timer_duration_list.dart b/lib/widgets/video_controls/widgets/sleep_timer_duration_list.dart index 5c8a6037..d2b601d6 100644 --- a/lib/widgets/video_controls/widgets/sleep_timer_duration_list.dart +++ b/lib/widgets/video_controls/widgets/sleep_timer_duration_list.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:media_kit/media_kit.dart'; + +import '../../../mpv/mpv.dart'; import '../../../services/sleep_timer_service.dart'; import '../../../utils/duration_formatter.dart'; import '../../../i18n/strings.g.dart'; /// Widget displaying list of sleep timer durations for selection class SleepTimerDurationList extends StatelessWidget { - final Player player; + final MpvPlayer player; final SleepTimerService sleepTimer; final int? defaultDuration; diff --git a/lib/widgets/video_controls/widgets/sync_offset_control.dart b/lib/widgets/video_controls/widgets/sync_offset_control.dart index 37f4d989..40a94646 100644 --- a/lib/widgets/video_controls/widgets/sync_offset_control.dart +++ b/lib/widgets/video_controls/widgets/sync_offset_control.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:media_kit/media_kit.dart'; + +import '../../../mpv/mpv.dart'; import '../../../i18n/strings.g.dart'; import '../../../utils/duration_formatter.dart'; /// Reusable widget for adjusting sync offsets (audio or subtitle) class SyncOffsetControl extends StatefulWidget { - final Player player; + final MpvPlayer player; final String propertyName; // 'audio-delay' or 'sub-delay' final int initialOffset; final String labelText; // 'Audio' or 'Subtitles' @@ -44,11 +45,11 @@ class _SyncOffsetControlState extends State { } Future _applyOffset(double offsetMs) async { - // Convert milliseconds to seconds for media_kit + // Convert milliseconds to seconds for mpv final offsetSeconds = offsetMs / 1000.0; // Apply to player using setProperty - await (widget.player.platform as dynamic).setProperty( + await widget.player.setProperty( widget.propertyName, offsetSeconds.toString(), ); diff --git a/lib/widgets/video_controls/widgets/track_chapter_controls.dart b/lib/widgets/video_controls/widgets/track_chapter_controls.dart index ca2a8f6a..07cc2ee9 100644 --- a/lib/widgets/video_controls/widgets/track_chapter_controls.dart +++ b/lib/widgets/video_controls/widgets/track_chapter_controls.dart @@ -1,8 +1,8 @@ import 'dart:io' show Platform; import 'package:flutter/material.dart'; -import 'package:media_kit/media_kit.dart'; +import '../../../mpv/mpv.dart'; import '../../../models/plex_media_info.dart'; import '../../../models/plex_media_version.dart'; import '../../../services/sleep_timer_service.dart'; @@ -17,7 +17,7 @@ import '../video_control_button.dart'; /// Row of track and chapter control buttons for the video player class TrackChapterControls extends StatelessWidget { - final Player player; + final MpvPlayer player; final List chapters; final bool chaptersLoaded; final List availableVersions; @@ -31,8 +31,8 @@ class TrackChapterControls extends StatelessWidget { final VoidCallback? onToggleRotationLock; final VoidCallback? onToggleFullscreen; final Function(int)? onSwitchVersion; - final Function(AudioTrack)? onAudioTrackChanged; - final Function(SubtitleTrack)? onSubtitleTrackChanged; + final Function(MpvAudioTrack)? onAudioTrackChanged; + final Function(MpvSubtitleTrack)? onSubtitleTrackChanged; final VoidCallback? onLoadSeekTimes; final String serverId; @@ -60,8 +60,8 @@ class TrackChapterControls extends StatelessWidget { @override Widget build(BuildContext context) { - return StreamBuilder( - stream: player.stream.tracks, + return StreamBuilder( + stream: player.streams.tracks, initialData: player.state.tracks, builder: (context, snapshot) { final tracks = snapshot.data; @@ -175,7 +175,7 @@ class TrackChapterControls extends StatelessWidget { ); } - bool _hasMultipleAudioTracks(Tracks? tracks) { + bool _hasMultipleAudioTracks(MpvTracks? tracks) { if (tracks == null) return false; final audioTracks = tracks.audio .where((track) => track.id != 'auto' && track.id != 'no') @@ -183,7 +183,7 @@ class TrackChapterControls extends StatelessWidget { return audioTracks.length > 1; } - bool _hasSubtitles(Tracks? tracks) { + bool _hasSubtitles(MpvTracks? tracks) { if (tracks == null) return false; final subtitles = tracks.subtitle .where((track) => track.id != 'auto' && track.id != 'no') diff --git a/lib/widgets/video_controls/widgets/volume_control.dart b/lib/widgets/video_controls/widgets/volume_control.dart index 95157af8..0d481903 100644 --- a/lib/widgets/video_controls/widgets/volume_control.dart +++ b/lib/widgets/video_controls/widgets/volume_control.dart @@ -1,22 +1,22 @@ import 'package:flutter/material.dart'; -import 'package:media_kit/media_kit.dart'; +import '../../../mpv/mpv.dart'; import '../../../services/settings_service.dart'; import '../../../i18n/strings.g.dart'; /// A volume control widget that displays a mute/unmute button and volume slider. /// -/// This widget integrates with [Player] to control volume and persists +/// This widget integrates with [MpvPlayer] to control volume and persists /// the volume setting using [SettingsService]. class VolumeControl extends StatelessWidget { - final Player player; + final MpvPlayer player; const VolumeControl({super.key, required this.player}); @override Widget build(BuildContext context) { return StreamBuilder( - stream: player.stream.volume, + stream: player.streams.volume, initialData: player.state.volume, builder: (context, snapshot) { final volume = snapshot.data ?? 100.0; diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 1882a226..3b730d7a 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,24 +7,15 @@ #include "generated_plugin_registrant.h" #include -#include -#include #include #include #include -#include #include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) hotkey_manager_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerLinuxPlugin"); hotkey_manager_linux_plugin_register_with_registrar(hotkey_manager_linux_registrar); - g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); - media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); - g_autoptr(FlPluginRegistrar) media_kit_video_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin"); - media_kit_video_plugin_register_with_registrar(media_kit_video_registrar); g_autoptr(FlPluginRegistrar) os_media_controls_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "OsMediaControlsPlugin"); os_media_controls_plugin_register_with_registrar(os_media_controls_registrar); @@ -34,9 +25,6 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); - g_autoptr(FlPluginRegistrar) volume_controller_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "VolumeControllerPlugin"); - volume_controller_plugin_register_with_registrar(volume_controller_registrar); g_autoptr(FlPluginRegistrar) window_manager_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); window_manager_plugin_register_with_registrar(window_manager_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 29593036..0cb2d895 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,12 +4,9 @@ list(APPEND FLUTTER_PLUGIN_LIST hotkey_manager_linux - media_kit_libs_linux - media_kit_video os_media_controls screen_retriever_linux url_launcher_linux - volume_controller window_manager ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index a6e9b89c..6987bc40 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,8 +8,6 @@ import Foundation import connectivity_plus 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 @@ -17,16 +15,12 @@ import screen_retriever_macos import shared_preferences_foundation import sqflite_darwin import url_launcher_macos -import volume_controller -import wakelock_plus import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) 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")) @@ -34,7 +28,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) - VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin")) - WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/macos/Podfile b/macos/Podfile index ff5ddb3b..0d122438 100644 --- a/macos/Podfile +++ b/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.15' +platform :osx, '14.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 0cfbe468..967f7d9f 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -8,10 +8,6 @@ PODS: - HotKey - macos_window_utils (1.0.0): - FlutterMacOS - - media_kit_libs_macos_video (1.0.4): - - FlutterMacOS - - media_kit_video (0.0.1): - - FlutterMacOS - os_media_controls (0.0.1): - FlutterMacOS - package_info_plus (0.0.1): @@ -29,10 +25,6 @@ PODS: - FlutterMacOS - url_launcher_macos (0.0.1): - FlutterMacOS - - volume_controller (0.0.1): - - FlutterMacOS - - wakelock_plus (0.0.1): - - FlutterMacOS - window_manager (0.2.0): - FlutterMacOS @@ -41,8 +33,6 @@ DEPENDENCIES: - 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`) @@ -50,8 +40,6 @@ DEPENDENCIES: - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - - volume_controller (from `Flutter/ephemeral/.symlinks/plugins/volume_controller/macos`) - - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) SPEC REPOS: @@ -67,10 +55,6 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos macos_window_utils: :path: Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos - media_kit_libs_macos_video: - :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: @@ -85,10 +69,6 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos - volume_controller: - :path: Flutter/ephemeral/.symlinks/plugins/volume_controller/macos - wakelock_plus: - :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos window_manager: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos @@ -98,8 +78,6 @@ SPEC CHECKSUMS: 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 @@ -107,10 +85,8 @@ SPEC CHECKSUMS: shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd - volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd - wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c -PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 +PODFILE CHECKSUM: d16f5e5d196d1ca9863d7a71d5885cad7ffa7d2d COCOAPODS: 1.16.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 08f104b2..5cfdfbe0 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -26,7 +26,10 @@ 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 6AB6B0952EDB27C100EAC8DB /* MPVKit-GPL in Frameworks */ = {isa = PBXBuildFile; productRef = 6AB6B0942EDB27C100EAC8DB /* MPVKit-GPL */; }; 6AC86ED72EA70B4C0067BC66 /* plezy.icon in Resources */ = {isa = PBXBuildFile; fileRef = 6AC86ED62EA70B4C0067BC66 /* plezy.icon */; }; + 6AD8B1612ED7B50000E9E1B4 /* MpvPlayerCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD8B1632ED7B50000E9E1B4 /* MpvPlayerCore.swift */; }; + 6AD8B1622ED7B50000E9E1B4 /* MpvPlayerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD8B1642ED7B50000E9E1B4 /* MpvPlayerPlugin.swift */; }; 9AECA605E2E1BECE3CA5BD01 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 78F9F23331B540A25C4A70C7 /* Pods_Runner.framework */; }; FE52EE1D2D489FA88507D14E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84260356652A3270E664FED4 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ @@ -81,6 +84,8 @@ 4E4BFEE5F31FF1CC308F1693 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 5EBEC2B546108DF45BEB0B00 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 6AC86ED62EA70B4C0067BC66 /* plezy.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = plezy.icon; sourceTree = ""; }; + 6AD8B1632ED7B50000E9E1B4 /* MpvPlayerCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPlayerCore.swift; sourceTree = ""; }; + 6AD8B1642ED7B50000E9E1B4 /* MpvPlayerPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPlayerPlugin.swift; sourceTree = ""; }; 78F9F23331B540A25C4A70C7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 84260356652A3270E664FED4 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -104,6 +109,7 @@ buildActionMask = 2147483647; files = ( 9AECA605E2E1BECE3CA5BD01 /* Pods_Runner.framework in Frameworks */, + 6AB6B0952EDB27C100EAC8DB /* MPVKit-GPL in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -190,6 +196,7 @@ children = ( 33CC10F02044A3C60003C045 /* AppDelegate.swift */, 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 6AD8B1652ED7B50000E9E1B4 /* MpvPlayer */, 33E51913231747F40026EE4D /* DebugProfile.entitlements */, 33E51914231749380026EE4D /* Release.entitlements */, 33CC11242044D66E0003C045 /* Resources */, @@ -207,6 +214,15 @@ name = Frameworks; sourceTree = ""; }; + 6AD8B1652ED7B50000E9E1B4 /* MpvPlayer */ = { + isa = PBXGroup; + children = ( + 6AD8B1632ED7B50000E9E1B4 /* MpvPlayerCore.swift */, + 6AD8B1642ED7B50000E9E1B4 /* MpvPlayerPlugin.swift */, + ); + path = MpvPlayer; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -291,6 +307,9 @@ Base, ); mainGroup = 33CC10E42044A3C60003C045; + packageReferences = ( + 6AB6B0932EDB27C100EAC8DB /* XCRemoteSwiftPackageReference "MPVKit" */, + ); productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -439,6 +458,8 @@ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + 6AD8B1612ED7B50000E9E1B4 /* MpvPlayerCore.swift in Sources */, + 6AD8B1622ED7B50000E9E1B4 /* MpvPlayerPlugin.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -556,7 +577,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -638,7 +659,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -688,7 +709,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -795,6 +816,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 6AB6B0932EDB27C100EAC8DB /* XCRemoteSwiftPackageReference "MPVKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/edde746/MPVKit"; + requirement = { + kind = revision; + revision = a8ae0d2f08df6188c662b7670bddb97131db8c5a; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 6AB6B0942EDB27C100EAC8DB /* MPVKit-GPL */ = { + isa = XCSwiftPackageProductDependency; + package = 6AB6B0932EDB27C100EAC8DB /* XCRemoteSwiftPackageReference "MPVKit" */; + productName = "MPVKit-GPL"; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..be7df7e5 --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,14 @@ +{ + "originHash" : "c535d3cdb62bd4e11e67f3c9e68290fa7ee9306634c5f56d2b0396eb75ed41ee", + "pins" : [ + { + "identity" : "mpvkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/edde746/MPVKit", + "state" : { + "revision" : "a8ae0d2f08df6188c662b7670bddb97131db8c5a" + } + } + ], + "version" : 3 +} diff --git a/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..be7df7e5 --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,14 @@ +{ + "originHash" : "c535d3cdb62bd4e11e67f3c9e68290fa7ee9306634c5f56d2b0396eb75ed41ee", + "pins" : [ + { + "identity" : "mpvkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/edde746/MPVKit", + "state" : { + "revision" : "a8ae0d2f08df6188c662b7670bddb97131db8c5a" + } + } + ], + "version" : 3 +} diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift index 3cc05eb2..b77c4b2a 100644 --- a/macos/Runner/MainFlutterWindow.swift +++ b/macos/Runner/MainFlutterWindow.swift @@ -4,10 +4,18 @@ import FlutterMacOS class MainFlutterWindow: NSWindow { override func awakeFromNib() { let flutterViewController = FlutterViewController() + + // Enable transparency for Metal layer behind Flutter + self.backgroundColor = NSColor.clear + flutterViewController.backgroundColor = NSColor.clear + let windowFrame = self.frame self.contentViewController = flutterViewController self.setFrame(windowFrame, display: true) + // Register MPV player plugin for video playback + MpvPlayerPlugin.register(with: flutterViewController.registrar(forPlugin: "MpvPlayerPlugin")) + RegisterGeneratedPlugins(registry: flutterViewController) super.awakeFromNib() diff --git a/macos/Runner/MpvPlayer/MpvPlayerCore.swift b/macos/Runner/MpvPlayer/MpvPlayerCore.swift new file mode 100644 index 00000000..9d70c930 --- /dev/null +++ b/macos/Runner/MpvPlayer/MpvPlayerCore.swift @@ -0,0 +1,382 @@ +import Cocoa +import Libmpv + +/// Protocol for receiving player events +protocol MpvPlayerDelegate: AnyObject { + func onPropertyChange(name: String, value: Any?) + func onEvent(name: String, data: [String: Any]?) +} + +// Workaround for MoltenVK problems that cause flicker +// https://github.com/mpv-player/mpv/pull/13651 +private class MetalLayer: CAMetalLayer { + override var drawableSize: CGSize { + get { return super.drawableSize } + set { + if Int(newValue.width) > 1 && Int(newValue.height) > 1 { + super.drawableSize = newValue + } + } + } + + // Fix for target-colorspace-hint - needs main thread for EDR + override var wantsExtendedDynamicRangeContent: Bool { + get { return super.wantsExtendedDynamicRangeContent } + set { + if Thread.isMainThread { + super.wantsExtendedDynamicRangeContent = newValue + } else { + DispatchQueue.main.sync { + super.wantsExtendedDynamicRangeContent = newValue + } + } + } + } +} + +/// Core MPV player using Metal rendering +class MpvPlayerCore: NSObject { + + // MARK: - Properties + + private var metalLayer: MetalLayer? + private var mpv: OpaquePointer? + private weak var window: NSWindow? + private lazy var queue = DispatchQueue(label: "mpv", qos: .userInitiated) + + weak var delegate: MpvPlayerDelegate? + + private(set) var isInitialized = false + + // MARK: - Initialization + + func initialize(in window: NSWindow) -> Bool { + guard !isInitialized else { + print("[MpvPlayerCore] Already initialized") + return true + } + + guard let contentView = window.contentView else { + print("[MpvPlayerCore] No content view") + return false + } + + self.window = window + + // Create Metal layer for video rendering + let layer = MetalLayer() + layer.frame = contentView.bounds + if let screen = window.screen ?? NSScreen.main { + layer.contentsScale = screen.backingScaleFactor + } + layer.framebufferOnly = true + layer.autoresizingMask = [.layerWidthSizable, .layerHeightSizable] + + metalLayer = layer + + // Ensure contentView has a layer and add our Metal layer + contentView.wantsLayer = true + contentView.layer?.addSublayer(layer) + + print("[MpvPlayerCore] Metal layer added, frame: \(layer.frame)") + + // Initialize MPV with this Metal layer + guard setupMpv() else { + print("[MpvPlayerCore] Failed to setup MPV") + layer.removeFromSuperlayer() + metalLayer = nil + return false + } + + isInitialized = true + print("[MpvPlayerCore] Initialized successfully with MPV") + return true + } + + private func setupMpv() -> Bool { + guard let metalLayer = metalLayer else { return false } + + mpv = mpv_create() + guard mpv != nil else { + print("[MpvPlayerCore] Failed to create MPV context") + return false + } + + // Logging + #if DEBUG + checkError(mpv_request_log_messages(mpv, "info")) + #else + checkError(mpv_request_log_messages(mpv, "warn")) + #endif + + // Set the Metal layer as the render target (must use local var for &) + var layer = metalLayer + checkError(mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &layer)) + + // Video output settings for Metal/Vulkan + checkError(mpv_set_option_string(mpv, "vo", "gpu-next")) + checkError(mpv_set_option_string(mpv, "gpu-api", "vulkan")) + checkError(mpv_set_option_string(mpv, "gpu-context", "moltenvk")) + checkError(mpv_set_option_string(mpv, "hwdec", "videotoolbox")) + + // Initialize MPV + let initResult = mpv_initialize(mpv) + if initResult < 0 { + print("[MpvPlayerCore] mpv_initialize failed: \(String(cString: mpv_error_string(initResult)))") + mpv_terminate_destroy(mpv) + mpv = nil + return false + } + + // Set up wakeup callback for event handling + mpv_set_wakeup_callback(mpv, { ctx in + let core = Unmanaged.fromOpaque(ctx!).takeUnretainedValue() + core.readEvents() + }, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())) + + print("[MpvPlayerCore] MPV initialized successfully") + return true + } + + // MARK: - MPV Properties and Commands + + func setProperty(_ name: String, value: String) { + guard mpv != nil else { return } + mpv_set_property_string(mpv, name, value) + } + + func getProperty(_ name: String) -> String? { + guard mpv != nil else { return nil } + let cstr = mpv_get_property_string(mpv, name) + defer { mpv_free(cstr) } + return cstr.map { String(cString: $0) } + } + + func observeProperty(_ name: String, format: String) { + guard mpv != nil else { return } + + let mpvFormat: mpv_format + switch format { + case "double": mpvFormat = MPV_FORMAT_DOUBLE + case "flag": mpvFormat = MPV_FORMAT_FLAG + case "node": mpvFormat = MPV_FORMAT_NODE + case "string": mpvFormat = MPV_FORMAT_STRING + default: return + } + + mpv_observe_property(mpv, 0, name, mpvFormat) + } + + func command(_ args: [String]) { + guard mpv != nil, !args.isEmpty else { return } + command(args[0], args: Array(args.dropFirst())) + } + + // MARK: - Visibility + + func setVisible(_ visible: Bool) { + guard let layer = metalLayer else { return } + + if visible { + // Re-insert after background layer but before Flutter control views + layer.removeFromSuperlayer() + if let superlayer = window?.contentView?.layer { + superlayer.insertSublayer(layer, at: 1) + } + } + + layer.isHidden = !visible + print("[MpvPlayerCore] setVisible(\(visible))") + } + + func updateFrame(_ frame: CGRect? = nil) { + guard let metalLayer = metalLayer else { return } + + if let frame = frame { + metalLayer.frame = frame + } else if let contentView = window?.contentView { + metalLayer.frame = contentView.bounds + } + + // Update drawable size for proper scaling + if let screen = window?.screen ?? NSScreen.main { + let scale = screen.backingScaleFactor + metalLayer.drawableSize = CGSize( + width: metalLayer.frame.width * scale, + height: metalLayer.frame.height * scale + ) + } + + print("[MpvPlayerCore] updateFrame: \(metalLayer.frame)") + } + + // MARK: - Private Helpers + + private func command(_ cmd: String, args: [String] = []) { + guard mpv != nil else { return } + + // Build array of C strings for mpv_command + var cargs: [UnsafeMutablePointer?] = ([cmd] + args).map { strdup($0) } + cargs.append(nil) // null-terminate + defer { + for ptr in cargs { + free(ptr) + } + } + + // mpv_command expects UnsafePointer, use withUnsafeBufferPointer for the conversion + cargs.withUnsafeBufferPointer { buffer in + var constPtrs = buffer.map { UnsafePointer($0) } + _ = mpv_command(mpv, &constPtrs) + } + } + + private func readEvents() { + queue.async { [weak self] in + guard let self = self, let mpv = self.mpv else { return } + + while true { + let event = mpv_wait_event(mpv, 0) + guard let eventPtr = event else { break } + + if eventPtr.pointee.event_id == MPV_EVENT_NONE { + break + } + + self.handleEvent(eventPtr.pointee) + } + } + } + + private func handleEvent(_ event: mpv_event) { + switch event.event_id { + case MPV_EVENT_PROPERTY_CHANGE: + guard let data = event.data else { break } + let property = data.assumingMemoryBound(to: mpv_event_property.self).pointee + let name = String(cString: property.name) + handlePropertyChange(name: name, property: property) + + case MPV_EVENT_FILE_LOADED: + DispatchQueue.main.async { + self.delegate?.onEvent(name: "file-loaded", data: nil) + } + + case MPV_EVENT_END_FILE: + DispatchQueue.main.async { + self.delegate?.onEvent(name: "end-file", data: nil) + } + + case MPV_EVENT_SHUTDOWN: + print("[MpvPlayerCore] MPV shutdown event") + + case MPV_EVENT_LOG_MESSAGE: + if let msgPtr = event.data?.assumingMemoryBound(to: mpv_event_log_message.self) { + let msg = msgPtr.pointee + if let prefix = msg.prefix, let level = msg.level, let text = msg.text { + print("[MPV:\(String(cString: prefix))] \(String(cString: level)): \(String(cString: text))", terminator: "") + } + } + + default: + break + } + } + + private func handlePropertyChange(name: String, property: mpv_event_property) { + var value: Any? + + switch property.format { + case MPV_FORMAT_DOUBLE: + if let ptr = property.data { + value = ptr.assumingMemoryBound(to: Double.self).pointee + } + + case MPV_FORMAT_FLAG: + if let ptr = property.data { + value = ptr.assumingMemoryBound(to: Int32.self).pointee != 0 + } + + case MPV_FORMAT_NODE: + if let ptr = property.data { + let node = ptr.assumingMemoryBound(to: mpv_node.self).pointee + value = convertNode(node) + } + + case MPV_FORMAT_STRING: + if let ptr = property.data { + let cstr = ptr.assumingMemoryBound(to: UnsafePointer?.self).pointee + value = cstr.map { String(cString: $0) } + } + + default: + break + } + + DispatchQueue.main.async { + self.delegate?.onPropertyChange(name: name, value: value) + } + } + + private func convertNode(_ node: mpv_node) -> Any? { + switch node.format { + case MPV_FORMAT_STRING: + return node.u.string.map { String(cString: $0) } + + case MPV_FORMAT_FLAG: + return node.u.flag != 0 + + case MPV_FORMAT_INT64: + return node.u.int64 + + case MPV_FORMAT_DOUBLE: + return node.u.double_ + + case MPV_FORMAT_NODE_ARRAY: + guard let list = node.u.list?.pointee else { return nil } + var array = [Any]() + for i in 0.. FlutterError? { + self.eventSink = events + print("[MpvPlayerPlugin] Event stream connected") + return nil + } + + func onCancel(withArguments arguments: Any?) -> FlutterError? { + self.eventSink = nil + print("[MpvPlayerPlugin] Event stream disconnected") + return nil + } + + // MARK: - FlutterPlugin Method Handler + + func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "initialize": + handleInitialize(result: result) + + case "dispose": + handleDispose(result: result) + + case "setProperty": + handleSetProperty(call: call, result: result) + + case "getProperty": + handleGetProperty(call: call, result: result) + + case "observeProperty": + handleObserveProperty(call: call, result: result) + + case "command": + handleCommand(call: call, result: result) + + case "setVisible": + handleSetVisible(call: call, result: result) + + case "isInitialized": + result(playerCore?.isInitialized ?? false) + + default: + result(FlutterMethodNotImplemented) + } + } + + // MARK: - Method Handlers + + private func handleInitialize(result: @escaping FlutterResult) { + DispatchQueue.main.async { [weak self] in + guard let self = self else { + result(FlutterError(code: "ERROR", message: "Plugin deallocated", details: nil)) + return + } + + // Check if already initialized + if self.playerCore?.isInitialized == true { + print("[MpvPlayerPlugin] Already initialized") + result(true) + return + } + + // Find the Flutter window + guard let (window, _, _) = self.findFlutterWindow() else { + print("[MpvPlayerPlugin] Failed to find Flutter window") + result(FlutterError(code: "NO_WINDOW", message: "Could not find Flutter window", details: nil)) + return + } + + // Create and initialize player core + let core = MpvPlayerCore() + core.delegate = self + + guard core.initialize(in: window) else { + print("[MpvPlayerPlugin] Failed to initialize MPV") + result(FlutterError(code: "MPV_INIT_FAILED", message: "Failed to initialize MPV", details: nil)) + return + } + + self.playerCore = core + + // Start hidden + core.setVisible(false) + + print("[MpvPlayerPlugin] Initialized successfully") + result(true) + } + } + + private func handleDispose(result: @escaping FlutterResult) { + DispatchQueue.main.async { [weak self] in + self?.playerCore?.dispose() + self?.playerCore = nil + print("[MpvPlayerPlugin] Disposed") + result(nil) + } + } + + private func handleSetProperty(call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: Any], + let name = args["name"] as? String, + let value = args["value"] as? String else { + result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name' or 'value' argument", details: nil)) + return + } + + playerCore?.setProperty(name, value: value) + result(nil) + } + + private func handleGetProperty(call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: Any], + let name = args["name"] as? String else { + result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name' argument", details: nil)) + return + } + + let value = playerCore?.getProperty(name) + result(value) + } + + private func handleObserveProperty(call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: Any], + let name = args["name"] as? String, + let format = args["format"] as? String else { + result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name' or 'format' argument", details: nil)) + return + } + + playerCore?.observeProperty(name, format: format) + result(nil) + } + + private func handleCommand(call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: Any], + let commandArgs = args["args"] as? [String] else { + result(FlutterError(code: "INVALID_ARGS", message: "Missing 'args' argument", details: nil)) + return + } + + playerCore?.command(commandArgs) + result(nil) + } + + private func handleSetVisible(call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: Any], + let visible = args["visible"] as? Bool else { + result(FlutterError(code: "INVALID_ARGS", message: "Missing 'visible' argument", details: nil)) + return + } + + DispatchQueue.main.async { [weak self] in + self?.playerCore?.setVisible(visible) + + // Update frame when becoming visible + if visible { + self?.playerCore?.updateFrame() + } + + result(nil) + } + } + + // MARK: - MpvPlayerDelegate + + func onPropertyChange(name: String, value: Any?) { + guard let eventSink = eventSink else { return } + + var event: [String: Any] = ["type": "property", "name": name] + + if let value = value { + event["value"] = value + } + + eventSink(event) + } + + func onEvent(name: String, data: [String: Any]?) { + guard let eventSink = eventSink else { return } + + var event: [String: Any] = ["type": "event", "name": name] + if let data = data { + event["data"] = data + } + + eventSink(event) + } + + // MARK: - Helpers + + private func findFlutterWindow() -> (NSWindow, NSView, NSView)? { + for window in NSApplication.shared.windows { + if window is MainFlutterWindow, + let contentView = window.contentView, + let contentVC = window.contentViewController { + let flutterView = contentVC.view + return (window, contentView, flutterView) + } + } + + // Fallback + for window in NSApplication.shared.windows { + if let contentView = window.contentView, + let contentVC = window.contentViewController { + let flutterView = contentVC.view + return (window, contentView, flutterView) + } + } + + return nil + } +} diff --git a/pubspec.lock b/pubspec.lock index 79f07bc2..90654492 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -600,78 +600,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.11.1" - media_kit: - dependency: "direct main" - description: - path: media_kit - ref: dce2963936424ffc80309ef4bca7e95041dfbfef - resolved-ref: dce2963936424ffc80309ef4bca7e95041dfbfef - url: "https://github.com/edde746/media-kit" - source: git - version: "1.2.1" - media_kit_libs_android_video: - dependency: "direct overridden" - description: - path: "libs/android/media_kit_libs_android_video" - ref: dce2963936424ffc80309ef4bca7e95041dfbfef - resolved-ref: dce2963936424ffc80309ef4bca7e95041dfbfef - url: "https://github.com/edde746/media-kit" - source: git - version: "1.3.9" - media_kit_libs_ios_video: - dependency: "direct overridden" - description: - path: "libs/ios/media_kit_libs_ios_video" - ref: dce2963936424ffc80309ef4bca7e95041dfbfef - resolved-ref: dce2963936424ffc80309ef4bca7e95041dfbfef - url: "https://github.com/edde746/media-kit" - source: git - version: "1.1.4" - media_kit_libs_linux: - dependency: transitive - description: - path: "libs/linux/media_kit_libs_linux" - ref: dce2963936424ffc80309ef4bca7e95041dfbfef - resolved-ref: dce2963936424ffc80309ef4bca7e95041dfbfef - url: "https://github.com/edde746/media-kit" - source: git - version: "1.2.1" - media_kit_libs_macos_video: - dependency: "direct overridden" - description: - path: "libs/macos/media_kit_libs_macos_video" - ref: dce2963936424ffc80309ef4bca7e95041dfbfef - resolved-ref: dce2963936424ffc80309ef4bca7e95041dfbfef - url: "https://github.com/edde746/media-kit" - source: git - version: "1.1.5" - media_kit_libs_video: - dependency: "direct main" - description: - path: "libs/universal/media_kit_libs_video" - ref: dce2963936424ffc80309ef4bca7e95041dfbfef - resolved-ref: dce2963936424ffc80309ef4bca7e95041dfbfef - url: "https://github.com/edde746/media-kit" - source: git - version: "1.0.7" - media_kit_libs_windows_video: - dependency: transitive - description: - path: "libs/windows/media_kit_libs_windows_video" - ref: dce2963936424ffc80309ef4bca7e95041dfbfef - resolved-ref: dce2963936424ffc80309ef4bca7e95041dfbfef - url: "https://github.com/edde746/media-kit" - source: git - version: "1.0.12" - media_kit_video: - dependency: "direct main" - description: - path: media_kit_video - ref: dce2963936424ffc80309ef4bca7e95041dfbfef - resolved-ref: dce2963936424ffc80309ef4bca7e95041dfbfef - url: "https://github.com/edde746/media-kit" - source: git - version: "1.3.1" meta: dependency: transitive description: @@ -913,30 +841,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" - safe_local_storage: - dependency: transitive - description: - name: safe_local_storage - sha256: e9a21b6fec7a8aa62cc2585ff4c1b127df42f3185adbd2aca66b47abe2e80236 - url: "https://pub.dev" - source: hosted - version: "2.0.1" - screen_brightness_android: - dependency: transitive - description: - name: screen_brightness_android - sha256: d34f5321abd03bc3474f4c381f53d189117eba0b039eac1916aa92cca5fd0a96 - url: "https://pub.dev" - source: hosted - version: "2.1.3" - screen_brightness_platform_interface: - dependency: transitive - description: - name: screen_brightness_platform_interface - sha256: "737bd47b57746bc4291cab1b8a5843ee881af499514881b0247ec77447ee769c" - url: "https://pub.dev" - source: hosted - version: "2.1.0" screen_retriever: dependency: transitive description: @@ -1222,22 +1126,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.3" - universal_platform: - dependency: transitive - description: - name: universal_platform - sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - uri_parser: - dependency: transitive - description: - name: uri_parser - sha256: ff4d2c720aca3f4f7d5445e23b11b2d15ef8af5ddce5164643f38ff962dcb270 - url: "https://pub.dev" - source: hosted - version: "3.0.0" url_launcher: dependency: "direct main" description: @@ -1326,30 +1214,6 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.2" - volume_controller: - dependency: transitive - description: - name: volume_controller - sha256: d75039e69c0d90e7810bfd47e3eedf29ff8543ea7a10392792e81f9bded7edf5 - url: "https://pub.dev" - source: hosted - version: "3.4.0" - wakelock_plus: - dependency: transitive - description: - name: wakelock_plus - sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228" - url: "https://pub.dev" - source: hosted - version: "1.4.0" - wakelock_plus_platform_interface: - dependency: transitive - description: - name: wakelock_plus_platform_interface - sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2" - url: "https://pub.dev" - source: hosted - version: "1.3.0" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7d21a8b0..c4f2fa51 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,9 +13,6 @@ dependencies: json_annotation: ^4.8.1 shared_preferences: ^2.2.2 cached_network_image: ^3.4.1 - media_kit: ^1.1.10+1 - media_kit_video: ^1.2.4 - media_kit_libs_video: ^1.0.4 macos_window_utils: ^1.9.0 url_launcher: ^6.3.0 uuid: ^4.4.0 @@ -36,38 +33,6 @@ dependencies: ref: 75dc5642cd148c54c03fe96976702b8d31c48599 rate_limiter: ^1.0.0 -dependency_overrides: - media_kit: - git: - url: https://github.com/edde746/media-kit - ref: dce2963936424ffc80309ef4bca7e95041dfbfef - path: media_kit - media_kit_video: - git: - url: https://github.com/edde746/media-kit - ref: dce2963936424ffc80309ef4bca7e95041dfbfef - path: media_kit_video - media_kit_libs_video: - git: - url: https://github.com/edde746/media-kit - ref: dce2963936424ffc80309ef4bca7e95041dfbfef - path: libs/universal/media_kit_libs_video - media_kit_libs_macos_video: - git: - url: https://github.com/edde746/media-kit - ref: dce2963936424ffc80309ef4bca7e95041dfbfef - path: libs/macos/media_kit_libs_macos_video - media_kit_libs_ios_video: - git: - url: https://github.com/edde746/media-kit - ref: dce2963936424ffc80309ef4bca7e95041dfbfef - path: libs/ios/media_kit_libs_ios_video - media_kit_libs_android_video: - git: - url: https://github.com/edde746/media-kit - ref: dce2963936424ffc80309ef4bca7e95041dfbfef - path: libs/android/media_kit_libs_android_video - dev_dependencies: flutter_test: sdk: flutter diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index e59aa9c2..c474c09e 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,12 +8,9 @@ #include #include -#include -#include #include #include #include -#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -21,18 +18,12 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); HotkeyManagerWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi")); - MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi")); - MediaKitVideoPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi")); OsMediaControlsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("OsMediaControlsPluginCApi")); ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); - VolumeControllerPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("VolumeControllerPluginCApi")); WindowManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("WindowManagerPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 0c647b45..ef9d7c28 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,12 +5,9 @@ list(APPEND FLUTTER_PLUGIN_LIST connectivity_plus hotkey_manager_windows - media_kit_libs_windows_video - media_kit_video os_media_controls screen_retriever_windows url_launcher_windows - volume_controller window_manager )