This commit is contained in:
edde746
2025-12-02 15:46:19 +01:00
parent 01bb1126ef
commit d9cdde66d8
53 changed files with 2645 additions and 685 deletions
-4
View File
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dart:io' show Platform; import 'dart:io' show Platform;
import 'package:media_kit/media_kit.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -49,9 +48,6 @@ void main() async {
// Configure macOS window with custom titlebar // Configure macOS window with custom titlebar
await MacOSTitlebarService.setupCustomTitlebar(); await MacOSTitlebarService.setupCustomTitlebar();
// Initialize MediaKit
MediaKit.ensureInitialized();
// Note: Orientation will be set dynamically based on device type in MainApp // Note: Orientation will be set dynamically based on device type in MainApp
await StorageService.getInstance(); await StorageService.getInstance();
+69
View File
@@ -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;
+29
View File
@@ -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;
}
+70
View File
@@ -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;
}
+22
View File
@@ -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';
}
+26
View File
@@ -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,
}
+31
View File
@@ -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<String, String>? 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;
}
@@ -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;
}
@@ -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)';
}
+31
View File
@@ -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<MpvAudioTrack> audio;
/// Available subtitle tracks.
final List<MpvSubtitleTrack> subtitle;
const MpvTracks({
this.audio = const [],
this.subtitle = const [],
});
/// Creates a copy with the given fields replaced.
MpvTracks copyWith({
List<MpvAudioTrack>? audio,
List<MpvSubtitleTrack>? subtitle,
}) {
return MpvTracks(
audio: audio ?? this.audio,
subtitle: subtitle ?? this.subtitle,
);
}
@override
String toString() =>
'MpvTracks(audio: ${audio.length}, subtitle: ${subtitle.length})';
}
+198
View File
@@ -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<void> open(MpvMedia media, {bool play = true});
/// Start or resume playback.
Future<void> play();
/// Pause playback.
Future<void> pause();
/// Toggle between play and pause.
Future<void> playOrPause();
/// Stop playback and reset position.
Future<void> stop();
/// Seek to a specific position.
Future<void> seek(Duration position);
// ============================================
// Track Selection
// ============================================
/// Select an audio track.
Future<void> selectAudioTrack(MpvAudioTrack track);
/// Select a subtitle track.
///
/// Pass [MpvSubtitleTrack.off] to disable subtitles.
Future<void> 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<void> 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<void> setVolume(double volume);
/// Set the playback rate/speed.
///
/// [rate] - Playback rate from 0.25 to 4.0 (1.0 = normal speed).
Future<void> setRate(double rate);
/// Set the audio output device.
///
/// [device] - The audio device to use.
Future<void> 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<void> setProperty(String name, String value);
/// Get an MPV property value by name.
Future<String?> getProperty(String name);
/// Execute a raw MPV command.
///
/// [args] - Command and arguments as a list of strings.
Future<void> command(List<String> 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<void> 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<bool> setVisible(bool visible);
// ============================================
// Lifecycle
// ============================================
/// Dispose of the player and release resources.
///
/// After calling this, the player instance should not be used.
Future<void> 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();
}
}
+506
View File
@@ -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<bool>.broadcast();
final _completedController = StreamController<bool>.broadcast();
final _bufferingController = StreamController<bool>.broadcast();
final _positionController = StreamController<Duration>.broadcast();
final _durationController = StreamController<Duration>.broadcast();
final _bufferController = StreamController<Duration>.broadcast();
final _volumeController = StreamController<double>.broadcast();
final _rateController = StreamController<double>.broadcast();
final _tracksController = StreamController<MpvTracks>.broadcast();
final _trackController = StreamController<MpvTrackSelection>.broadcast();
final _logController = StreamController<MpvLog>.broadcast();
final _errorController = StreamController<String>.broadcast();
final _audioDeviceController = StreamController<MpvAudioDevice>.broadcast();
final _audioDevicesController =
StreamController<List<MpvAudioDevice>>.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 = <MpvAudioTrack>[];
final subtitleTracks = <MpvSubtitleTrack>[];
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<MpvAudioTrack?>().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<MpvSubtitleTrack?>().firstWhere(
(t) => t?.id == id,
orElse: () => null,
);
}
_state = _state.copyWith(
track: _state.track.copyWith(subtitle: selectedTrack),
);
_trackController.add(_state.track);
}
Future<void> _ensureInitialized() async {
if (_initialized) return;
try {
final result = await _methodChannel.invokeMethod<bool>('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<void> _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<void> 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<void> play() async {
_checkDisposed();
await setProperty('pause', 'no');
}
@override
Future<void> pause() async {
_checkDisposed();
await setProperty('pause', 'yes');
}
@override
Future<void> playOrPause() async {
_checkDisposed();
if (_state.playing) {
await pause();
} else {
await play();
}
}
@override
Future<void> stop() async {
_checkDisposed();
await command(['stop']);
await _methodChannel.invokeMethod('setVisible', {'visible': false});
_isVisible = false;
}
@override
Future<void> seek(Duration position) async {
_checkDisposed();
await command([
'seek',
(position.inMilliseconds / 1000.0).toString(),
'absolute',
]);
}
// ============================================
// Track Selection
// ============================================
@override
Future<void> selectAudioTrack(MpvAudioTrack track) async {
_checkDisposed();
await setProperty('aid', track.id);
}
@override
Future<void> selectSubtitleTrack(MpvSubtitleTrack track) async {
_checkDisposed();
await setProperty('sid', track.id);
}
@override
Future<void> 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<void> setVolume(double volume) async {
_checkDisposed();
await setProperty('volume', volume.toString());
}
@override
Future<void> setRate(double rate) async {
_checkDisposed();
await setProperty('speed', rate.toString());
}
@override
Future<void> setAudioDevice(MpvAudioDevice device) async {
_checkDisposed();
await setProperty('audio-device', device.name);
}
// ============================================
// MPV Properties
// ============================================
@override
Future<void> setProperty(String name, String value) async {
_checkDisposed();
await _methodChannel.invokeMethod('setProperty', {
'name': name,
'value': value,
});
}
@override
Future<String?> getProperty(String name) async {
_checkDisposed();
return await _methodChannel.invokeMethod<String>('getProperty', {
'name': name,
});
}
@override
Future<void> command(List<String> args) async {
_checkDisposed();
await _methodChannel.invokeMethod('command', {'args': args});
}
// ============================================
// Passthrough
// ============================================
@override
Future<void> 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<bool> 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<void> 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();
}
}
+113
View File
@@ -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<MpvAudioDevice> 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<MpvAudioDevice>? 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)';
}
@@ -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<bool> playing;
/// Stream of completion state changes.
final Stream<bool> completed;
/// Stream of buffering state changes.
final Stream<bool> buffering;
/// Stream of position updates.
final Stream<Duration> position;
/// Stream of duration changes (when media is loaded).
final Stream<Duration> duration;
/// Stream of buffer position updates.
final Stream<Duration> buffer;
/// Stream of volume changes.
final Stream<double> volume;
/// Stream of playback rate changes.
final Stream<double> rate;
/// Stream of available tracks updates.
final Stream<MpvTracks> tracks;
/// Stream of track selection changes.
final Stream<MpvTrackSelection> track;
/// Stream of log messages from MPV.
final Stream<MpvLog> log;
/// Stream of error messages.
final Stream<String> error;
/// Stream of audio device changes.
final Stream<MpvAudioDevice> audioDevice;
/// Stream of available audio devices.
final Stream<List<MpvAudioDevice>> 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,
});
}
+242
View File
@@ -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<bool>.broadcast();
final _completedController = StreamController<bool>.broadcast();
final _bufferingController = StreamController<bool>.broadcast();
final _positionController = StreamController<Duration>.broadcast();
final _durationController = StreamController<Duration>.broadcast();
final _bufferController = StreamController<Duration>.broadcast();
final _volumeController = StreamController<double>.broadcast();
final _rateController = StreamController<double>.broadcast();
final _tracksController = StreamController<MpvTracks>.broadcast();
final _trackController = StreamController<MpvTrackSelection>.broadcast();
final _logController = StreamController<MpvLog>.broadcast();
final _errorController = StreamController<String>.broadcast();
final _audioDeviceController = StreamController<MpvAudioDevice>.broadcast();
final _audioDevicesController =
StreamController<List<MpvAudioDevice>>.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<void> open(MpvMedia media, {bool play = true}) async {
_checkDisposed();
throw UnimplementedError('MpvPlayer.open() is not yet implemented');
}
@override
Future<void> play() async {
_checkDisposed();
throw UnimplementedError('MpvPlayer.play() is not yet implemented');
}
@override
Future<void> pause() async {
_checkDisposed();
throw UnimplementedError('MpvPlayer.pause() is not yet implemented');
}
@override
Future<void> playOrPause() async {
_checkDisposed();
if (_state.playing) {
await pause();
} else {
await play();
}
}
@override
Future<void> stop() async {
_checkDisposed();
throw UnimplementedError('MpvPlayer.stop() is not yet implemented');
}
@override
Future<void> seek(Duration position) async {
_checkDisposed();
throw UnimplementedError('MpvPlayer.seek() is not yet implemented');
}
// ============================================
// Track Selection
// ============================================
@override
Future<void> selectAudioTrack(MpvAudioTrack track) async {
_checkDisposed();
throw UnimplementedError(
'MpvPlayer.selectAudioTrack() is not yet implemented');
}
@override
Future<void> selectSubtitleTrack(MpvSubtitleTrack track) async {
_checkDisposed();
throw UnimplementedError(
'MpvPlayer.selectSubtitleTrack() is not yet implemented');
}
@override
Future<void> 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<void> setVolume(double volume) async {
_checkDisposed();
throw UnimplementedError('MpvPlayer.setVolume() is not yet implemented');
}
@override
Future<void> setRate(double rate) async {
_checkDisposed();
throw UnimplementedError('MpvPlayer.setRate() is not yet implemented');
}
@override
Future<void> setAudioDevice(MpvAudioDevice device) async {
_checkDisposed();
throw UnimplementedError(
'MpvPlayer.setAudioDevice() is not yet implemented');
}
// ============================================
// MPV Properties
// ============================================
@override
Future<void> setProperty(String name, String value) async {
_checkDisposed();
throw UnimplementedError('MpvPlayer.setProperty() is not yet implemented');
}
@override
Future<String?> getProperty(String name) async {
_checkDisposed();
throw UnimplementedError('MpvPlayer.getProperty() is not yet implemented');
}
@override
Future<void> command(List<String> args) async {
_checkDisposed();
throw UnimplementedError('MpvPlayer.command() is not yet implemented');
}
// ============================================
// Passthrough
// ============================================
@override
Future<void> setAudioPassthrough(bool enabled) async {
_checkDisposed();
throw UnimplementedError(
'MpvPlayer.setAudioPassthrough() is not yet implemented');
}
// ============================================
// Visibility
// ============================================
@override
Future<bool> setVisible(bool visible) async {
_checkDisposed();
// No-op on unsupported platforms
return false;
}
// ============================================
// Lifecycle
// ============================================
@override
Future<void> 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();
}
}
+106
View File
@@ -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<MpvVideo> createState() => _MpvVideoState();
}
class _MpvVideoState extends State<MpvVideo> {
@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,
),
),
],
),
);
}
}
+99 -117
View File
@@ -3,11 +3,11 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.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:os_media_controls/os_media_controls.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../mpv/mpv.dart';
import '../client/plex_client.dart'; import '../client/plex_client.dart';
import '../models/plex_media_version.dart'; import '../models/plex_media_version.dart';
import '../models/plex_metadata.dart'; import '../models/plex_metadata.dart';
@@ -28,8 +28,8 @@ import '../i18n/strings.g.dart';
class VideoPlayerScreen extends StatefulWidget { class VideoPlayerScreen extends StatefulWidget {
final PlexMetadata metadata; final PlexMetadata metadata;
final AudioTrack? preferredAudioTrack; final MpvAudioTrack? preferredAudioTrack;
final SubtitleTrack? preferredSubtitleTrack; final MpvSubtitleTrack? preferredSubtitleTrack;
final double? preferredPlaybackRate; final double? preferredPlaybackRate;
final int selectedMediaIndex; final int selectedMediaIndex;
@@ -48,8 +48,7 @@ class VideoPlayerScreen extends StatefulWidget {
class VideoPlayerScreenState extends State<VideoPlayerScreen> class VideoPlayerScreenState extends State<VideoPlayerScreen>
with WidgetsBindingObserver { with WidgetsBindingObserver {
Player? player; MpvPlayer? player;
VideoController? controller;
bool _isPlayerInitialized = false; bool _isPlayerInitialized = false;
PlexMetadata? _nextEpisode; PlexMetadata? _nextEpisode;
PlexMetadata? _previousEpisode; PlexMetadata? _previousEpisode;
@@ -57,13 +56,13 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
bool _showPlayNextDialog = false; bool _showPlayNextDialog = false;
bool _isPhone = false; bool _isPhone = false;
List<PlexMediaVersion> _availableVersions = []; List<PlexMediaVersion> _availableVersions = [];
StreamSubscription<PlayerLog>? _logSubscription; StreamSubscription<MpvLog>? _logSubscription;
StreamSubscription<String>? _errorSubscription; StreamSubscription<String>? _errorSubscription;
StreamSubscription<bool>? _playingSubscription; StreamSubscription<bool>? _playingSubscription;
StreamSubscription<bool>? _completedSubscription; StreamSubscription<bool>? _completedSubscription;
StreamSubscription<dynamic>? _mediaControlSubscription; StreamSubscription<dynamic>? _mediaControlSubscription;
StreamSubscription<bool>? _bufferingSubscription; StreamSubscription<bool>? _bufferingSubscription;
StreamSubscription<Tracks>? _trackLoadingSubscription; StreamSubscription<MpvTracks>? _trackLoadingSubscription;
bool _isReplacingWithVideo = bool _isReplacingWithVideo =
false; // Flag to skip orientation restoration during video-to-video navigation false; // Flag to skip orientation restoration during video-to-video navigation
@@ -222,57 +221,44 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
.getEnableHardwareDecoding(); .getEnableHardwareDecoding();
final debugLoggingEnabled = settingsService.getEnableDebugLogging(); final debugLoggingEnabled = settingsService.getEnableDebugLogging();
// Build MPV configuration // Create player
final config = <String, String>{ player = MpvPlayer();
'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',
};
// 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) { 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 // Apply audio sync offset
final audioSyncOffset = settingsService.getAudioSyncOffset(); final audioSyncOffset = settingsService.getAudioSyncOffset();
if (audioSyncOffset != 0) { if (audioSyncOffset != 0) {
final offsetSeconds = audioSyncOffset / 1000.0; final offsetSeconds = audioSyncOffset / 1000.0;
await (player!.platform as dynamic).setProperty( await player!.setProperty('audio-delay', offsetSeconds.toString());
'audio-delay',
offsetSeconds.toString(),
);
} }
// Apply subtitle sync offset // Apply subtitle sync offset
final subtitleSyncOffset = settingsService.getSubtitleSyncOffset(); final subtitleSyncOffset = settingsService.getSubtitleSyncOffset();
if (subtitleSyncOffset != 0) { if (subtitleSyncOffset != 0) {
final offsetSeconds = subtitleSyncOffset / 1000.0; final offsetSeconds = subtitleSyncOffset / 1000.0;
await (player!.platform as dynamic).setProperty( await player!.setProperty('sub-delay', offsetSeconds.toString());
'sub-delay',
offsetSeconds.toString(),
);
} }
// Apply saved volume // Apply saved volume
@@ -310,23 +296,23 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
} }
// Listen to playback state changes // Listen to playback state changes
_playingSubscription = player!.stream.playing.listen( _playingSubscription = player!.streams.playing.listen(
_onPlayingStateChanged, _onPlayingStateChanged,
); );
// Listen to completion // Listen to completion
_completedSubscription = player!.stream.completed.listen( _completedSubscription = player!.streams.completed.listen(
_onVideoCompleted, _onVideoCompleted,
); );
// Listen to MPV logs // Listen to MPV logs
_logSubscription = player!.stream.log.listen(_onPlayerLog); _logSubscription = player!.streams.log.listen(_onPlayerLog);
// Listen to MPV errors // Listen to MPV errors
_errorSubscription = player!.stream.error.listen(_onPlayerError); _errorSubscription = player!.streams.error.listen(_onPlayerError);
// Listen to buffering state // Listen to buffering state
_bufferingSubscription = player!.stream.buffering.listen((isBuffering) { _bufferingSubscription = player!.streams.buffering.listen((isBuffering) {
_isBuffering.value = isBuffering; _isBuffering.value = isBuffering;
}); });
@@ -424,12 +410,12 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
); );
// Listen to playing state and update media controls // Listen to playing state and update media controls
player!.stream.playing.listen((isPlaying) { player!.streams.playing.listen((isPlaying) {
_updateMediaControlsPlaybackState(); _updateMediaControlsPlaybackState();
}); });
// Listen to position updates for media controls // Listen to position updates for media controls
player!.stream.position.listen((position) { player!.streams.position.listen((position) {
_mediaControlsManager?.updatePlaybackState( _mediaControlsManager?.updatePlaybackState(
isPlaying: player!.state.playing, isPlaying: player!.state.playing,
position: position, position: position,
@@ -542,26 +528,22 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
// Use server-specific client for this metadata // Use server-specific client for this metadata
final client = _getClientForMetadata(context); final client = _getClientForMetadata(context);
// Capture profile settings before async gap
final profileSettings = context.profileSettings;
// Initialize playback service // Initialize playback service
final playbackService = PlaybackInitializationService( final playbackService = PlaybackInitializationService(
player: player!,
client: client, client: client,
context: context,
); );
// Start playback and get available versions // Get playback data (video URL and available versions)
final result = await playbackService.startPlayback( final result = await playbackService.getPlaybackData(
metadata: widget.metadata, metadata: widget.metadata,
selectedMediaIndex: widget.selectedMediaIndex, 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 // Update available versions from the playback data
if (mounted) { if (mounted) {
setState(() { setState(() {
@@ -643,11 +625,10 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
// Clear video filter and reset subtitle margins before disposing player // Clear video filter and reset subtitle margins before disposing player
try { try {
if (player != null) { if (player != null) {
final nativePlayer = player!.platform as dynamic; player!.setProperty('vf', '');
nativePlayer.setProperty('vf', ''); player!.setProperty('sub-margin-x', '0');
nativePlayer.setProperty('sub-margin-x', '0'); player!.setProperty('sub-margin-y', '0');
nativePlayer.setProperty('sub-margin-y', '0'); player!.setProperty('sub-scale', '1.0');
nativePlayer.setProperty('sub-scale', '1.0');
} }
} catch (e) { } catch (e) {
// Non-critical: Cleanup operations during disposal // Non-critical: Cleanup operations during disposal
@@ -700,26 +681,26 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
} }
} }
void _onPlayerLog(PlayerLog log) { void _onPlayerLog(MpvLog log) {
// Map MPV log levels to app logger levels // Map MPV log levels to app logger levels
switch (log.level.toLowerCase()) { switch (log.level) {
case 'fatal': case MpvLogLevel.fatal:
case 'error': case MpvLogLevel.error:
appLogger.e('[MPV:${log.prefix}] ${log.text}'); appLogger.e('[MPV:${log.prefix}] ${log.text}');
break; break;
case 'warn': case MpvLogLevel.warn:
appLogger.w('[MPV:${log.prefix}] ${log.text}'); appLogger.w('[MPV:${log.prefix}] ${log.text}');
break; break;
case 'info': case MpvLogLevel.info:
appLogger.i('[MPV:${log.prefix}] ${log.text}'); appLogger.i('[MPV:${log.prefix}] ${log.text}');
break; break;
case 'debug': case MpvLogLevel.debug:
case 'trace': case MpvLogLevel.trace:
case 'v': case MpvLogLevel.verbose:
appLogger.d('[MPV:${log.prefix}] ${log.text}'); appLogger.d('[MPV:${log.prefix}] ${log.text}');
break; break;
default: 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<VideoPlayerScreen>
} }
/// Handle audio track changes from the user - save as per-media preference if enabled /// Handle audio track changes from the user - save as per-media preference if enabled
Future<void> _onAudioTrackChanged(AudioTrack track) async { Future<void> _onAudioTrackChanged(MpvAudioTrack track) async {
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
// Only save if remember track selections is enabled // Only save if remember track selections is enabled
@@ -800,7 +781,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
} }
/// Handle subtitle track changes from the user - save as per-media preference if enabled /// Handle subtitle track changes from the user - save as per-media preference if enabled
Future<void> _onSubtitleTrackChanged(SubtitleTrack track) async { Future<void> _onSubtitleTrackChanged(MpvSubtitleTrack track) async {
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
// Only save if remember track selections is enabled // Only save if remember track selections is enabled
@@ -908,7 +889,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Show loading indicator while player initializes // Show loading indicator while player initializes
if (!_isPlayerInitialized || controller == null || player == null) { if (!_isPlayerInitialized || player == null) {
return const Scaffold( return const Scaffold(
backgroundColor: Colors.black, backgroundColor: Colors.black,
body: Center(child: CircularProgressIndicator(color: Colors.white)), body: Center(child: CircularProgressIndicator(color: Colors.white)),
@@ -928,7 +909,8 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
} }
}, },
child: Scaffold( 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( body: GestureDetector(
behavior: HitTestBehavior behavior: HitTestBehavior
.translucent, // Allow taps to pass through to controls .translucent, // Allow taps to pass through to controls
@@ -959,44 +941,44 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
children: [ children: [
// Video player // Video player
Center( Center(
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
// Update player size when layout changes // Update player size when layout changes
final newSize = Size( final newSize = Size(
constraints.maxWidth, constraints.maxWidth,
constraints.maxHeight, constraints.maxHeight,
); );
// Update player size in video filter manager // Update player size in video filter manager
if (_videoFilterManager != null) { if (_videoFilterManager != null) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) { if (mounted) {
_videoFilterManager!.updatePlayerSize(newSize); _videoFilterManager!.updatePlayerSize(newSize);
} }
}); });
} }
return Video( return MpvVideo(
controller: controller!, player: player!,
fit: _videoFilterManager?.currentBoxFit ?? BoxFit.contain, fit: _videoFilterManager?.currentBoxFit ?? BoxFit.contain,
controls: (state) => plexVideoControlsBuilder( controls: (context) => plexVideoControlsBuilder(
player!, player!,
widget.metadata, widget.metadata,
onNext: _nextEpisode != null ? _playNext : null, onNext: _nextEpisode != null ? _playNext : null,
onPrevious: _previousEpisode != null onPrevious: _previousEpisode != null
? _playPrevious ? _playPrevious
: null, : null,
availableVersions: _availableVersions, availableVersions: _availableVersions,
selectedMediaIndex: widget.selectedMediaIndex, selectedMediaIndex: widget.selectedMediaIndex,
boxFitMode: _videoFilterManager?.boxFitMode ?? 0, boxFitMode: _videoFilterManager?.boxFitMode ?? 0,
onCycleBoxFitMode: _cycleBoxFitMode, onCycleBoxFitMode: _cycleBoxFitMode,
onAudioTrackChanged: _onAudioTrackChanged, onAudioTrackChanged: _onAudioTrackChanged,
onSubtitleTrackChanged: _onSubtitleTrackChanged, onSubtitleTrackChanged: _onSubtitleTrackChanged,
), ),
); );
}, },
),
), ),
),
// Play Next Dialog // Play Next Dialog
if (_showPlayNextDialog && _nextEpisode != null) if (_showPlayNextDialog && _nextEpisode != null)
Positioned.fill( Positioned.fill(
+5 -5
View File
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../mpv/mpv.dart';
import '../client/plex_client.dart'; import '../client/plex_client.dart';
import '../models/plex_metadata.dart'; import '../models/plex_metadata.dart';
import '../providers/playback_state_provider.dart'; import '../providers/playback_state_provider.dart';
@@ -77,14 +77,14 @@ class EpisodeNavigationService {
Future<void> navigateToEpisode({ Future<void> navigateToEpisode({
required BuildContext context, required BuildContext context,
required PlexMetadata episode, required PlexMetadata episode,
required Player? player, required MpvPlayer? player,
bool usePushReplacement = true, bool usePushReplacement = true,
}) async { }) async {
if (!context.mounted) return; if (!context.mounted) return;
// Capture current player state before navigation // Capture current player state before navigation
AudioTrack? currentAudioTrack; MpvAudioTrack? currentAudioTrack;
SubtitleTrack? currentSubtitleTrack; MpvSubtitleTrack? currentSubtitleTrack;
double? currentPlaybackRate; double? currentPlaybackRate;
if (player != null) { if (player != null) {
@@ -93,7 +93,7 @@ class EpisodeNavigationService {
currentPlaybackRate = player.state.rate; currentPlaybackRate = player.state.rate;
appLogger.d( 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',
); );
} }
+3 -3
View File
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:media_kit/media_kit.dart';
import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:hotkey_manager/hotkey_manager.dart';
import '../mpv/mpv.dart';
import 'settings_service.dart'; import 'settings_service.dart';
import '../utils/keyboard_utils.dart'; import '../utils/keyboard_utils.dart';
import '../utils/player_utils.dart'; import '../utils/player_utils.dart';
@@ -148,7 +148,7 @@ class KeyboardShortcutsService {
// Handle keyboard input for video player // Handle keyboard input for video player
KeyEventResult handleVideoPlayerKeyEvent( KeyEventResult handleVideoPlayerKeyEvent(
KeyEvent event, KeyEvent event,
Player player, MpvPlayer player,
VoidCallback? onToggleFullscreen, VoidCallback? onToggleFullscreen,
VoidCallback? onToggleSubtitles, VoidCallback? onToggleSubtitles,
VoidCallback? onNextAudioTrack, VoidCallback? onNextAudioTrack,
@@ -241,7 +241,7 @@ class KeyboardShortcutsService {
void _executeAction( void _executeAction(
String action, String action,
Player player, MpvPlayer player,
VoidCallback? onToggleFullscreen, VoidCallback? onToggleFullscreen,
VoidCallback? onToggleSubtitles, VoidCallback? onToggleSubtitles,
VoidCallback? onNextAudioTrack, VoidCallback? onNextAudioTrack,
+11 -161
View File
@@ -1,45 +1,21 @@
import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
import '../client/plex_client.dart'; import '../client/plex_client.dart';
import '../models/plex_media_info.dart';
import '../models/plex_metadata.dart'; import '../models/plex_metadata.dart';
import '../models/plex_user_profile.dart';
import '../utils/app_logger.dart';
import '../i18n/strings.g.dart'; import '../i18n/strings.g.dart';
import 'track_selection_service.dart';
/// Service responsible for initializing video playback /// Service responsible for fetching video playback data from the Plex server
///
/// 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
class PlaybackInitializationService { class PlaybackInitializationService {
final Player player;
final PlexClient client; final PlexClient client;
final BuildContext context;
PlaybackInitializationService({ PlaybackInitializationService({
required this.player,
required this.client, 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 /// Returns a PlaybackInitializationResult with video URL and available versions
Future<PlaybackInitializationResult> startPlayback({ Future<PlaybackInitializationResult> getPlaybackData({
required PlexMetadata metadata, required PlexMetadata metadata,
required int selectedMediaIndex, required int selectedMediaIndex,
required PlexUserProfile? profileSettings,
AudioTrack? preferredAudioTrack,
SubtitleTrack? preferredSubtitleTrack,
double? preferredPlaybackRate,
}) async { }) async {
try { try {
// Get consolidated playback data (URL, media info, and versions) in a single API call // 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); throw PlaybackException(t.messages.fileInfoNotAvailable);
} }
final videoUrl = playbackData.videoUrl!; // Return result with available versions and video URL
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 PlaybackInitializationResult( return PlaybackInitializationResult(
availableVersions: playbackData.availableVersions, availableVersions: playbackData.availableVersions,
videoUrl: playbackData.videoUrl!,
); );
} catch (e) { } catch (e) {
if (e is PlaybackException) { if (e is PlaybackException) {
@@ -104,103 +40,17 @@ class PlaybackInitializationService {
throw PlaybackException(t.messages.errorLoading(error: e.toString())); throw PlaybackException(t.messages.errorLoading(error: e.toString()));
} }
} }
/// Build list of external subtitle tracks from media info
List<SubtitleTrack> _buildExternalSubtitles(PlexMediaInfo? mediaInfo) {
final externalSubtitles = <SubtitleTrack>[];
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<void> _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<void> _addExternalSubtitles(
List<SubtitleTrack> 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 /// Result of playback initialization
class PlaybackInitializationResult { class PlaybackInitializationResult {
final List<dynamic> availableVersions; final List<dynamic> availableVersions;
final String? videoUrl;
PlaybackInitializationResult({required this.availableVersions}); PlaybackInitializationResult({
required this.availableVersions,
this.videoUrl,
});
} }
/// Exception thrown when playback initialization fails /// Exception thrown when playback initialization fails
+2 -2
View File
@@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:media_kit/media_kit.dart'; import '../mpv/mpv.dart';
import '../client/plex_client.dart'; import '../client/plex_client.dart';
import '../models/plex_metadata.dart'; import '../models/plex_metadata.dart';
@@ -15,7 +15,7 @@ import '../utils/app_logger.dart';
class PlaybackProgressTracker { class PlaybackProgressTracker {
final PlexClient client; final PlexClient client;
final PlexMetadata metadata; final PlexMetadata metadata;
final Player player; final MpvPlayer player;
/// Timer for periodic progress updates /// Timer for periodic progress updates
Timer? _progressTimer; Timer? _progressTimer;
+47 -47
View File
@@ -1,4 +1,4 @@
import 'package:media_kit/media_kit.dart'; import '../mpv/mpv.dart';
import '../models/plex_metadata.dart'; import '../models/plex_metadata.dart';
import '../models/plex_user_profile.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 /// Service for selecting and applying audio and subtitle tracks based on
/// preferences, user profiles, and per-media settings. /// preferences, user profiles, and per-media settings.
class TrackSelectionService { class TrackSelectionService {
final Player player; final MpvPlayer player;
final PlexUserProfile? profileSettings; final PlexUserProfile? profileSettings;
final PlexMetadata metadata; final PlexMetadata metadata;
@@ -69,11 +69,11 @@ class TrackSelectionService {
return null; return null;
} }
AudioTrack? findBestAudioMatch( MpvAudioTrack? findBestAudioMatch(
List<AudioTrack> availableTracks, List<MpvAudioTrack> availableTracks,
AudioTrack preferred, MpvAudioTrack preferred,
) { ) {
return findBestTrackMatch<AudioTrack>( return findBestTrackMatch<MpvAudioTrack>(
availableTracks, availableTracks,
preferred, preferred,
(t) => t.id, (t) => t.id,
@@ -82,8 +82,8 @@ class TrackSelectionService {
); );
} }
AudioTrack? findAudioTrackByProfile( MpvAudioTrack? findAudioTrackByProfile(
List<AudioTrack> availableTracks, List<MpvAudioTrack> availableTracks,
PlexUserProfile profile, PlexUserProfile profile,
) { ) {
appLogger.d('Audio track selection using user profile'); appLogger.d('Audio track selection using user profile');
@@ -123,7 +123,7 @@ class TrackSelectionService {
'Checking language variations for "$preferredLanguage": ${languageVariations.join(", ")}', 'Checking language variations for "$preferredLanguage": ${languageVariations.join(", ")}',
); );
final match = _findTrackByLanguageVariations<AudioTrack>( final match = _findTrackByLanguageVariations<MpvAudioTrack>(
availableTracks, availableTracks,
preferredLanguage, preferredLanguage,
languageVariations, languageVariations,
@@ -140,16 +140,16 @@ class TrackSelectionService {
return null; return null;
} }
SubtitleTrack? findBestSubtitleMatch( MpvSubtitleTrack? findBestSubtitleMatch(
List<SubtitleTrack> availableTracks, List<MpvSubtitleTrack> availableTracks,
SubtitleTrack preferred, MpvSubtitleTrack preferred,
) { ) {
// Handle special "no subtitles" case // Handle special "no subtitles" case
if (preferred.id == 'no') { if (preferred.id == 'no') {
return SubtitleTrack.no(); return MpvSubtitleTrack.off;
} }
return findBestTrackMatch<SubtitleTrack>( return findBestTrackMatch<MpvSubtitleTrack>(
availableTracks, availableTracks,
preferred, preferred,
(t) => t.id, (t) => t.id,
@@ -158,10 +158,10 @@ class TrackSelectionService {
); );
} }
SubtitleTrack? findSubtitleTrackByProfile( MpvSubtitleTrack? findSubtitleTrackByProfile(
List<SubtitleTrack> availableTracks, List<MpvSubtitleTrack> availableTracks,
PlexUserProfile profile, { PlexUserProfile profile, {
AudioTrack? selectedAudioTrack, MpvAudioTrack? selectedAudioTrack,
}) { }) {
appLogger.d('Subtitle track selection using user profile'); appLogger.d('Subtitle track selection using user profile');
appLogger.d( appLogger.d(
@@ -178,7 +178,7 @@ class TrackSelectionService {
appLogger.d( appLogger.d(
'Profile specifies manual mode (autoSelectSubtitle=0) - Subtitles OFF', 'Profile specifies manual mode (autoSelectSubtitle=0) - Subtitles OFF',
); );
return SubtitleTrack.no(); return MpvSubtitleTrack.off;
} }
// Mode 1: Shown with foreign audio // Mode 1: Shown with foreign audio
@@ -201,7 +201,7 @@ class TrackSelectionService {
// If audio matches preferred language, no subtitles needed // If audio matches preferred language, no subtitles needed
if (audioLang != null && languageVariations.contains(audioLang)) { if (audioLang != null && languageVariations.contains(audioLang)) {
appLogger.d('Audio matches preferred language - Subtitles OFF'); appLogger.d('Audio matches preferred language - Subtitles OFF');
return SubtitleTrack.no(); return MpvSubtitleTrack.off;
} }
appLogger.d('Foreign audio detected - enabling subtitles'); appLogger.d('Foreign audio detected - enabling subtitles');
} }
@@ -258,7 +258,7 @@ class TrackSelectionService {
'Checking language variations for "$preferredLanguage": ${languageVariations.join(", ")}', 'Checking language variations for "$preferredLanguage": ${languageVariations.join(", ")}',
); );
final match = _findTrackByLanguageVariations<SubtitleTrack>( final match = _findTrackByLanguageVariations<MpvSubtitleTrack>(
candidateTracks, candidateTracks,
preferredLanguage, preferredLanguage,
languageVariations, languageVariations,
@@ -282,8 +282,8 @@ class TrackSelectionService {
/// - 1: Prefer SDH subtitles /// - 1: Prefer SDH subtitles
/// - 2: Only show SDH subtitles /// - 2: Only show SDH subtitles
/// - 3: Only show non-SDH subtitles /// - 3: Only show non-SDH subtitles
List<SubtitleTrack> filterSubtitlesBySDH( List<MpvSubtitleTrack> filterSubtitlesBySDH(
List<SubtitleTrack> tracks, List<MpvSubtitleTrack> tracks,
int preference, int preference,
) { ) {
if (preference == 0 || preference == 1) { if (preference == 0 || preference == 1) {
@@ -319,8 +319,8 @@ class TrackSelectionService {
/// - 1: Prefer forced subtitles /// - 1: Prefer forced subtitles
/// - 2: Only show forced subtitles /// - 2: Only show forced subtitles
/// - 3: Only show non-forced subtitles /// - 3: Only show non-forced subtitles
List<SubtitleTrack> filterSubtitlesByForced( List<MpvSubtitleTrack> filterSubtitlesByForced(
List<SubtitleTrack> tracks, List<MpvSubtitleTrack> tracks,
int preference, int preference,
) { ) {
if (preference == 0 || preference == 1) { 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) /// 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 /// Since mpv may not expose this directly, we infer from the title
bool isSDH(SubtitleTrack track) { bool isSDH(MpvSubtitleTrack track) {
final title = track.title?.toLowerCase() ?? ''; final title = track.title?.toLowerCase() ?? '';
// Look for common SDH indicators // Look for common SDH indicators
@@ -365,7 +365,7 @@ class TrackSelectionService {
} }
/// Checks if a subtitle track is forced /// Checks if a subtitle track is forced
bool isForced(SubtitleTrack track) { bool isForced(MpvSubtitleTrack track) {
final title = track.title?.toLowerCase() ?? ''; final title = track.title?.toLowerCase() ?? '';
return title.contains('forced'); return title.contains('forced');
} }
@@ -423,8 +423,8 @@ class TrackSelectionService {
/// Log available tracks for debugging /// Log available tracks for debugging
void logAvailableTracks( void logAvailableTracks(
List<AudioTrack> audioTracks, List<MpvAudioTrack> audioTracks,
List<SubtitleTrack> subtitleTracks, List<MpvSubtitleTrack> subtitleTracks,
) { ) {
appLogger.d('Available audio tracks: ${audioTracks.length}'); appLogger.d('Available audio tracks: ${audioTracks.length}');
for (var track in audioTracks) { for (var track in audioTracks) {
@@ -445,13 +445,13 @@ class TrackSelectionService {
/// Priority 2: Per-media language preference /// Priority 2: Per-media language preference
/// Priority 3: User profile preferences /// Priority 3: User profile preferences
/// Priority 4: Default or first track /// Priority 4: Default or first track
AudioTrack? selectAudioTrack( MpvAudioTrack? selectAudioTrack(
List<AudioTrack> availableTracks, List<MpvAudioTrack> availableTracks,
AudioTrack? preferredAudioTrack, MpvAudioTrack? preferredAudioTrack,
) { ) {
if (availableTracks.isEmpty) return null; if (availableTracks.isEmpty) return null;
AudioTrack? trackToSelect; MpvAudioTrack? trackToSelect;
// Priority 1: Try to match preferred track from navigation // Priority 1: Try to match preferred track from navigation
if (preferredAudioTrack != null) { if (preferredAudioTrack != null) {
@@ -522,19 +522,19 @@ class TrackSelectionService {
/// Priority 3: User profile preferences /// Priority 3: User profile preferences
/// Priority 4: Default track /// Priority 4: Default track
/// Priority 5: Off /// Priority 5: Off
SubtitleTrack selectSubtitleTrack( MpvSubtitleTrack selectSubtitleTrack(
List<SubtitleTrack> availableTracks, List<MpvSubtitleTrack> availableTracks,
SubtitleTrack? preferredSubtitleTrack, MpvSubtitleTrack? preferredSubtitleTrack,
AudioTrack? selectedAudioTrack, MpvAudioTrack? selectedAudioTrack,
) { ) {
SubtitleTrack? subtitleToSelect; MpvSubtitleTrack? subtitleToSelect;
// Priority 1: Try preferred track from navigation (always wins) // Priority 1: Try preferred track from navigation (always wins)
if (preferredSubtitleTrack != null) { if (preferredSubtitleTrack != null) {
appLogger.d('Priority 1: Checking preferred track from navigation'); appLogger.d('Priority 1: Checking preferred track from navigation');
if (preferredSubtitleTrack.id == 'no') { if (preferredSubtitleTrack.id == 'no') {
appLogger.d(' Preferred: OFF'); appLogger.d(' Preferred: OFF');
return SubtitleTrack.no(); return MpvSubtitleTrack.off;
} else if (availableTracks.isNotEmpty) { } else if (availableTracks.isNotEmpty) {
appLogger.d( appLogger.d(
' Preferred: ${preferredSubtitleTrack.title ?? "Track ${preferredSubtitleTrack.id}"} (${preferredSubtitleTrack.language ?? "unknown"})', ' Preferred: ${preferredSubtitleTrack.title ?? "Track ${preferredSubtitleTrack.id}"} (${preferredSubtitleTrack.language ?? "unknown"})',
@@ -566,7 +566,7 @@ class TrackSelectionService {
if (metadata.subtitleLanguage == 'none' || if (metadata.subtitleLanguage == 'none' ||
metadata.subtitleLanguage!.isEmpty) { metadata.subtitleLanguage!.isEmpty) {
appLogger.d(' Per-media preference: Subtitles OFF'); appLogger.d(' Per-media preference: Subtitles OFF');
return SubtitleTrack.no(); return MpvSubtitleTrack.off;
} else if (availableTracks.isNotEmpty) { } else if (availableTracks.isNotEmpty) {
final matchedTrack = availableTracks.firstWhere( final matchedTrack = availableTracks.firstWhere(
(track) => languageMatches(track.language, metadata.subtitleLanguage), (track) => languageMatches(track.language, metadata.subtitleLanguage),
@@ -615,16 +615,16 @@ class TrackSelectionService {
// Priority 5: If still no subtitle selected, turn off // Priority 5: If still no subtitle selected, turn off
appLogger.d('Priority 5: No subtitle selected - Subtitles 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 /// Select and apply audio and subtitle tracks based on preferences
Future<void> selectAndApplyTracks({ Future<void> selectAndApplyTracks({
AudioTrack? preferredAudioTrack, MpvAudioTrack? preferredAudioTrack,
SubtitleTrack? preferredSubtitleTrack, MpvSubtitleTrack? preferredSubtitleTrack,
double? preferredPlaybackRate, double? preferredPlaybackRate,
Function(AudioTrack)? onAudioTrackChanged, Function(MpvAudioTrack)? onAudioTrackChanged,
Function(SubtitleTrack)? onSubtitleTrackChanged, Function(MpvSubtitleTrack)? onSubtitleTrackChanged,
}) async { }) async {
// Wait for tracks to be loaded // Wait for tracks to be loaded
int attempts = 0; int attempts = 0;
@@ -658,7 +658,7 @@ class TrackSelectionService {
appLogger.i( appLogger.i(
'Final audio selection: ${selectedAudioTrack.title ?? "Track ${selectedAudioTrack.id}"} (${selectedAudioTrack.language ?? "unknown"})', 'Final audio selection: ${selectedAudioTrack.title ?? "Track ${selectedAudioTrack.id}"} (${selectedAudioTrack.language ?? "unknown"})',
); );
player.setAudioTrack(selectedAudioTrack); player.selectAudioTrack(selectedAudioTrack);
} else { } else {
appLogger.d('No audio tracks available'); appLogger.d('No audio tracks available');
} }
@@ -674,7 +674,7 @@ class TrackSelectionService {
? 'OFF' ? 'OFF'
: '${selectedSubtitleTrack.title ?? "Track ${selectedSubtitleTrack.id}"} (${selectedSubtitleTrack.language ?? "unknown"})'; : '${selectedSubtitleTrack.title ?? "Track ${selectedSubtitleTrack.id}"} (${selectedSubtitleTrack.language ?? "unknown"})';
appLogger.i('Final subtitle selection: $finalSubtitle'); appLogger.i('Final subtitle selection: $finalSubtitle');
player.setSubtitleTrack(selectedSubtitleTrack); player.selectSubtitleTrack(selectedSubtitleTrack);
// Set playback rate if preferred rate was provided // Set playback rate if preferred rate was provided
if (preferredPlaybackRate != null) { if (preferredPlaybackRate != null) {
+15 -16
View File
@@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
import 'package:rate_limiter/rate_limiter.dart'; import 'package:rate_limiter/rate_limiter.dart';
import '../mpv/mpv.dart';
import '../models/plex_media_version.dart'; import '../models/plex_media_version.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
@@ -13,7 +14,7 @@ import '../utils/app_logger.dart';
/// - Subtitle positioning adjustments based on crop parameters /// - Subtitle positioning adjustments based on crop parameters
/// - Debounced video filter updates on resize events /// - Debounced video filter updates on resize events
class VideoFilterManager { class VideoFilterManager {
final Player player; final MpvPlayer player;
final List<PlexMediaVersion> availableVersions; final List<PlexMediaVersion> availableVersions;
final int selectedMediaIndex; final int selectedMediaIndex;
@@ -203,8 +204,6 @@ class VideoFilterManager {
/// Update the video filter based on current crop mode /// Update the video filter based on current crop mode
void updateVideoFilter() async { void updateVideoFilter() async {
try { try {
final nativePlayer = player.platform as dynamic;
if (_boxFitMode == 1) { if (_boxFitMode == 1) {
// Fill screen mode - apply crop filter // Fill screen mode - apply crop filter
_videoSize = _getCurrentVideoSize(); _videoSize = _getCurrentVideoSize();
@@ -218,7 +217,7 @@ class VideoFilterManager {
); );
// Apply crop filter // Apply crop filter
await nativePlayer.setProperty('vf', cropFilter); await player.setProperty('vf', cropFilter);
// Apply subtitle margins and scaling to compensate for crop zoom // Apply subtitle margins and scaling to compensate for crop zoom
final subMarginX = cropParams['subMarginX']!; final subMarginX = cropParams['subMarginX']!;
@@ -229,28 +228,28 @@ class VideoFilterManager {
'Applying subtitle properties - margins: x=$subMarginX, y=$subMarginY, scale=$subScale', 'Applying subtitle properties - margins: x=$subMarginX, y=$subMarginY, scale=$subScale',
); );
await nativePlayer.setProperty('sub-margin-x', subMarginX.toString()); await player.setProperty('sub-margin-x', subMarginX.toString());
await nativePlayer.setProperty('sub-margin-y', subMarginY.toString()); await player.setProperty('sub-margin-y', subMarginY.toString());
await nativePlayer.setProperty('sub-scale', subScale.toString()); await player.setProperty('sub-scale', subScale.toString());
} else { } else {
// Clear filter but apply base margins if no cropping needed // Clear filter but apply base margins if no cropping needed
appLogger.d( appLogger.d(
'Clearing video filter - aspect ratios similar, applying base margins (player: $_playerSize, video: $_videoSize)', 'Clearing video filter - aspect ratios similar, applying base margins (player: $_playerSize, video: $_videoSize)',
); );
await nativePlayer.setProperty('vf', ''); await player.setProperty('vf', '');
await nativePlayer.setProperty('sub-margin-x', '20'); // Base margin await player.setProperty('sub-margin-x', '20'); // Base margin
await nativePlayer.setProperty('sub-margin-y', '40'); // Base margin await player.setProperty('sub-margin-y', '40'); // Base margin
await nativePlayer.setProperty('sub-scale', '1.0'); // Reset scale await player.setProperty('sub-scale', '1.0'); // Reset scale
} }
} else { } else {
// Other modes - clear video filter but apply base margins // Other modes - clear video filter but apply base margins
appLogger.d( appLogger.d(
'Clearing video filter, applying base margins - BoxFit mode $_boxFitMode', 'Clearing video filter, applying base margins - BoxFit mode $_boxFitMode',
); );
await nativePlayer.setProperty('vf', ''); await player.setProperty('vf', '');
await nativePlayer.setProperty('sub-margin-x', '20'); // Base margin await player.setProperty('sub-margin-x', '20'); // Base margin
await nativePlayer.setProperty('sub-margin-y', '40'); // Base margin await player.setProperty('sub-margin-y', '40'); // Base margin
await nativePlayer.setProperty('sub-scale', '1.0'); // Reset scale await player.setProperty('sub-scale', '1.0'); // Reset scale
} }
} catch (e) { } catch (e) {
appLogger.w('Failed to update video filter', error: e); appLogger.w('Failed to update video filter', error: e);
+2 -2
View File
@@ -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 /// Seeks by the given offset (can be positive or negative) while clamping
/// the result between 0 and the video duration /// 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 currentPosition = player.state.position;
final duration = player.state.duration; final duration = player.state.duration;
final newPosition = currentPosition + offset; final newPosition = currentPosition + offset;
+4 -3
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
import '../mpv/mpv.dart';
import '../models/plex_metadata.dart'; import '../models/plex_metadata.dart';
import '../screens/video_player_screen.dart'; import '../screens/video_player_screen.dart';
import '../services/settings_service.dart'; import '../services/settings_service.dart';
@@ -26,8 +27,8 @@ import '../services/settings_service.dart';
Future<bool?> navigateToVideoPlayer( Future<bool?> navigateToVideoPlayer(
BuildContext context, { BuildContext context, {
required PlexMetadata metadata, required PlexMetadata metadata,
AudioTrack? preferredAudioTrack, MpvAudioTrack? preferredAudioTrack,
SubtitleTrack? preferredSubtitleTrack, MpvSubtitleTrack? preferredSubtitleTrack,
double? preferredPlaybackRate, double? preferredPlaybackRate,
int? selectedMediaIndex, int? selectedMediaIndex,
bool usePushReplacement = false, bool usePushReplacement = false,
@@ -1,8 +1,8 @@
import 'dart:io' show Platform; import 'dart:io' show Platform;
import 'package:flutter/material.dart'; 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_info.dart';
import '../../models/plex_metadata.dart'; import '../../models/plex_metadata.dart';
import '../../services/fullscreen_state_manager.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 /// Desktop-specific video controls layout with top bar and bottom controls
class DesktopVideoControls extends StatelessWidget { class DesktopVideoControls extends StatelessWidget {
final Player player; final MpvPlayer player;
final PlexMetadata metadata; final PlexMetadata metadata;
final VoidCallback? onNext; final VoidCallback? onNext;
final VoidCallback? onPrevious; final VoidCallback? onPrevious;
@@ -158,11 +158,11 @@ class DesktopVideoControls extends StatelessWidget {
children: [ children: [
// Row 1: Timeline with time indicators // Row 1: Timeline with time indicators
StreamBuilder<Duration>( StreamBuilder<Duration>(
stream: player.stream.position, stream: player.streams.position,
initialData: player.state.position, initialData: player.state.position,
builder: (context, positionSnapshot) { builder: (context, positionSnapshot) {
return StreamBuilder<Duration>( return StreamBuilder<Duration>(
stream: player.stream.duration, stream: player.streams.duration,
initialData: player.state.duration, initialData: player.state.duration,
builder: (context, durationSnapshot) { builder: (context, durationSnapshot) {
final position = positionSnapshot.data ?? Duration.zero; final position = positionSnapshot.data ?? Duration.zero;
@@ -238,7 +238,7 @@ class DesktopVideoControls extends StatelessWidget {
), ),
// Play/Pause // Play/Pause
StreamBuilder<bool>( StreamBuilder<bool>(
stream: player.stream.playing, stream: player.streams.playing,
initialData: player.state.playing, initialData: player.state.playing,
builder: (context, snapshot) { builder: (context, snapshot) {
final isPlaying = snapshot.data ?? false; final isPlaying = snapshot.data ?? false;
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; 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_info.dart';
import '../../models/plex_metadata.dart'; import '../../models/plex_metadata.dart';
import '../../utils/desktop_window_padding.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) /// - Center: Large playback controls (seek backward, play/pause, seek forward)
/// - Bottom bar: Timeline slider with chapter markers and timestamps /// - Bottom bar: Timeline slider with chapter markers and timestamps
class MobileVideoControls extends StatelessWidget { class MobileVideoControls extends StatelessWidget {
final Player player; final MpvPlayer player;
final PlexMetadata metadata; final PlexMetadata metadata;
final List<PlexChapter> chapters; final List<PlexChapter> chapters;
final bool chaptersLoaded; final bool chaptersLoaded;
@@ -114,7 +114,7 @@ class MobileVideoControls extends StatelessWidget {
Widget _buildPlaybackControls(BuildContext context) { Widget _buildPlaybackControls(BuildContext context) {
return StreamBuilder<bool>( return StreamBuilder<bool>(
stream: player.stream.playing, stream: player.streams.playing,
initialData: player.state.playing, initialData: player.state.playing,
builder: (context, snapshot) { builder: (context, snapshot) {
final isPlaying = snapshot.data ?? false; final isPlaying = snapshot.data ?? false;
@@ -214,11 +214,11 @@ class MobileVideoControls extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: StreamBuilder<Duration>( child: StreamBuilder<Duration>(
stream: player.stream.position, stream: player.streams.position,
initialData: player.state.position, initialData: player.state.position,
builder: (context, positionSnapshot) { builder: (context, positionSnapshot) {
return StreamBuilder<Duration>( return StreamBuilder<Duration>(
stream: player.stream.duration, stream: player.streams.duration,
initialData: player.state.duration, initialData: player.state.duration,
builder: (context, durationSnapshot) { builder: (context, durationSnapshot) {
final position = positionSnapshot.data ?? Duration.zero; final position = positionSnapshot.data ?? Duration.zero;
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
import '../../../mpv/mpv.dart';
import '../../../i18n/strings.g.dart'; import '../../../i18n/strings.g.dart';
import 'track_selection_sheet.dart'; import 'track_selection_sheet.dart';
@@ -7,10 +8,10 @@ import 'track_selection_sheet.dart';
class AudioTrackSheet { class AudioTrackSheet {
static void show( static void show(
BuildContext context, BuildContext context,
Player player, { MpvPlayer player, {
Function(AudioTrack)? onTrackChanged, Function(MpvAudioTrack)? onTrackChanged,
}) { }) {
TrackSelectionSheet.show<AudioTrack>( TrackSelectionSheet.show<MpvAudioTrack>(
context: context, context: context,
player: player, player: player,
title: t.videoControls.audioLabel, title: t.videoControls.audioLabel,
@@ -28,12 +29,12 @@ class AudioTrackSheet {
if (audioTrack.codec != null && audioTrack.codec!.isNotEmpty) { if (audioTrack.codec != null && audioTrack.codec!.isNotEmpty) {
parts.add(audioTrack.codec!.toUpperCase()); parts.add(audioTrack.codec!.toUpperCase());
} }
if (audioTrack.channelscount != null) { if (audioTrack.channelsCount != null) {
parts.add('${audioTrack.channelscount}ch'); parts.add('${audioTrack.channelsCount}ch');
} }
return parts.isEmpty ? 'Audio Track ${index + 1}' : parts.join(' · '); return parts.isEmpty ? 'Audio Track ${index + 1}' : parts.join(' · ');
}, },
setTrack: (track) => player.setAudioTrack(track), setTrack: (track) => player.selectAudioTrack(track),
onTrackChanged: onTrackChanged, onTrackChanged: onTrackChanged,
); );
} }
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
import '../../../mpv/mpv.dart';
import '../../../client/plex_client.dart'; import '../../../client/plex_client.dart';
import '../../../models/plex_media_info.dart'; import '../../../models/plex_media_info.dart';
import '../../../utils/duration_formatter.dart'; import '../../../utils/duration_formatter.dart';
@@ -8,7 +9,7 @@ import 'base_video_control_sheet.dart';
/// Bottom sheet for selecting chapters /// Bottom sheet for selecting chapters
class ChapterSheet extends StatelessWidget { class ChapterSheet extends StatelessWidget {
final Player player; final MpvPlayer player;
final List<PlexChapter> chapters; final List<PlexChapter> chapters;
final bool chaptersLoaded; final bool chaptersLoaded;
final String? serverId; // Server ID for the metadata these chapters belong to final String? serverId; // Server ID for the metadata these chapters belong to
@@ -23,7 +24,7 @@ class ChapterSheet extends StatelessWidget {
static void show( static void show(
BuildContext context, BuildContext context,
Player player, MpvPlayer player,
List<PlexChapter> chapters, List<PlexChapter> chapters,
bool chaptersLoaded, { bool chaptersLoaded, {
String? serverId, String? serverId,
@@ -47,7 +48,7 @@ class ChapterSheet extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StreamBuilder<Duration>( return StreamBuilder<Duration>(
stream: player.stream.position, stream: player.streams.position,
initialData: player.state.position, initialData: player.state.position,
builder: (context, positionSnapshot) { builder: (context, positionSnapshot) {
final currentPosition = positionSnapshot.data ?? Duration.zero; final currentPosition = positionSnapshot.data ?? Duration.zero;
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
import '../../../mpv/mpv.dart';
import '../../../i18n/strings.g.dart'; import '../../../i18n/strings.g.dart';
import 'track_selection_sheet.dart'; import 'track_selection_sheet.dart';
@@ -7,10 +8,10 @@ import 'track_selection_sheet.dart';
class SubtitleTrackSheet { class SubtitleTrackSheet {
static void show( static void show(
BuildContext context, BuildContext context,
Player player, { MpvPlayer player, {
Function(SubtitleTrack)? onTrackChanged, Function(MpvSubtitleTrack)? onTrackChanged,
}) { }) {
TrackSelectionSheet.show<SubtitleTrack>( TrackSelectionSheet.show<MpvSubtitleTrack>(
context: context, context: context,
player: player, player: player,
title: t.videoControls.subtitlesLabel, title: t.videoControls.subtitlesLabel,
@@ -41,10 +42,10 @@ class SubtitleTrackSheet {
} }
return parts.isEmpty ? 'Track ${index + 1}' : parts.join(' · '); return parts.isEmpty ? 'Track ${index + 1}' : parts.join(' · ');
}, },
setTrack: (track) => player.setSubtitleTrack(track), setTrack: (track) => player.selectSubtitleTrack(track),
onTrackChanged: onTrackChanged, onTrackChanged: onTrackChanged,
showOffOption: true, showOffOption: true,
createOffTrack: () => SubtitleTrack.no(), createOffTrack: () => MpvSubtitleTrack.off,
isOffTrack: (track) => track.id == 'no', isOffTrack: (track) => track.id == 'no',
); );
} }
@@ -1,16 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
import '../../../mpv/mpv.dart';
import 'base_video_control_sheet.dart'; import 'base_video_control_sheet.dart';
/// Generic track selection sheet for audio and subtitle tracks /// 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<T> extends StatelessWidget { class TrackSelectionSheet<T> extends StatelessWidget {
final Player player; final MpvPlayer player;
final String title; final String title;
final IconData icon; final IconData icon;
final List<T> Function(Tracks?) extractTracks; final List<T> Function(MpvTracks?) extractTracks;
final T Function(Track) getCurrentTrack; final T? Function(MpvTrack) getCurrentTrack;
final String Function(T track, int index) buildLabel; final String Function(T track, int index) buildLabel;
final void Function(T track) setTrack; final void Function(T track) setTrack;
final Function(T)? onTrackChanged; final Function(T)? onTrackChanged;
@@ -35,11 +36,11 @@ class TrackSelectionSheet<T> extends StatelessWidget {
static void show<T>({ static void show<T>({
required BuildContext context, required BuildContext context,
required Player player, required MpvPlayer player,
required String title, required String title,
required IconData icon, required IconData icon,
required List<T> Function(Tracks?) extractTracks, required List<T> Function(MpvTracks?) extractTracks,
required T Function(Track) getCurrentTrack, required T? Function(MpvTrack) getCurrentTrack,
required String Function(T track, int index) buildLabel, required String Function(T track, int index) buildLabel,
required void Function(T track) setTrack, required void Function(T track) setTrack,
Function(T)? onTrackChanged, Function(T)? onTrackChanged,
@@ -66,9 +67,9 @@ class TrackSelectionSheet<T> extends StatelessWidget {
} }
String _getEmptyMessage() { String _getEmptyMessage() {
if (T == SubtitleTrack) { if (T == MpvSubtitleTrack) {
return 'No subtitles available'; return 'No subtitles available';
} else if (T == AudioTrack) { } else if (T == MpvAudioTrack) {
return 'No audio tracks available'; return 'No audio tracks available';
} }
return 'No tracks available'; return 'No tracks available';
@@ -76,16 +77,16 @@ class TrackSelectionSheet<T> extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StreamBuilder<Tracks>( return StreamBuilder<MpvTracks>(
stream: player.stream.tracks, stream: player.streams.tracks,
initialData: player.state.tracks, initialData: player.state.tracks,
builder: (context, snapshot) { builder: (context, snapshot) {
final tracks = snapshot.data; final tracks = snapshot.data;
final availableTracks = extractTracks(tracks).where((track) { final availableTracks = extractTracks(tracks).where((track) {
// Filter out 'auto' and 'no' tracks from the list // Filter out 'auto' and 'no' tracks from the list
if (track is AudioTrack) { if (track is MpvAudioTrack) {
return track.id != 'auto' && track.id != 'no'; 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 track.id != 'auto' && track.id != 'no';
} }
return true; return true;
@@ -101,17 +102,17 @@ class TrackSelectionSheet<T> extends StatelessWidget {
style: const TextStyle(color: Colors.white70), style: const TextStyle(color: Colors.white70),
), ),
) )
: StreamBuilder<Track>( : StreamBuilder<MpvTrack>(
stream: player.stream.track, stream: player.streams.track,
initialData: player.state.track, initialData: player.state.track,
builder: (context, selectedSnapshot) { builder: (context, selectedSnapshot) {
final currentTrack = final currentTrack =
selectedSnapshot.data ?? player.state.track; selectedSnapshot.data ?? player.state.track;
final selectedTrack = getCurrentTrack(currentTrack); final selectedTrack = getCurrentTrack(currentTrack);
// Determine if "Off" is selected // Determine if "Off" is selected (null or explicit off)
final isOffSelected = final isOffSelected = selectedTrack == null ||
isOffTrack?.call(selectedTrack) ?? false; (isOffTrack?.call(selectedTrack) ?? false);
final itemCount = final itemCount =
availableTracks.length + (showOffOption ? 1 : 0); availableTracks.length + (showOffOption ? 1 : 0);
@@ -150,18 +151,20 @@ class TrackSelectionSheet<T> extends StatelessWidget {
// Check if this track is selected // Check if this track is selected
String trackId; String trackId;
if (track is AudioTrack) { if (track is MpvAudioTrack) {
trackId = track.id; trackId = track.id;
} else if (track is SubtitleTrack) { } else if (track is MpvSubtitleTrack) {
trackId = track.id; trackId = track.id;
} else { } else {
trackId = ''; trackId = '';
} }
String selectedId; String selectedId;
if (selectedTrack is AudioTrack) { if (selectedTrack == null) {
selectedId = '';
} else if (selectedTrack is MpvAudioTrack) {
selectedId = selectedTrack.id; selectedId = selectedTrack.id;
} else if (selectedTrack is SubtitleTrack) { } else if (selectedTrack is MpvSubtitleTrack) {
selectedId = selectedTrack.id; selectedId = selectedTrack.id;
} else { } else {
selectedId = ''; selectedId = '';
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
import '../../../mpv/mpv.dart';
import '../../../services/settings_service.dart'; import '../../../services/settings_service.dart';
import '../../../services/sleep_timer_service.dart'; import '../../../services/sleep_timer_service.dart';
import '../../../utils/platform_detector.dart'; import '../../../utils/platform_detector.dart';
@@ -58,7 +59,7 @@ class _SettingsMenuItem extends StatelessWidget {
/// Unified settings sheet for playback adjustments with in-sheet navigation /// Unified settings sheet for playback adjustments with in-sheet navigation
class VideoSettingsSheet extends StatefulWidget { class VideoSettingsSheet extends StatefulWidget {
final Player player; final MpvPlayer player;
final int audioSyncOffset; final int audioSyncOffset;
final int subtitleSyncOffset; final int subtitleSyncOffset;
@@ -71,7 +72,7 @@ class VideoSettingsSheet extends StatefulWidget {
static Future<void> show( static Future<void> show(
BuildContext context, BuildContext context,
Player player, MpvPlayer player,
int audioSyncOffset, int audioSyncOffset,
int subtitleSyncOffset, int subtitleSyncOffset,
) { ) {
@@ -200,7 +201,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
children: [ children: [
// Playback Speed // Playback Speed
StreamBuilder<double>( StreamBuilder<double>(
stream: widget.player.stream.rate, stream: widget.player.streams.rate,
initialData: widget.player.state.rate, initialData: widget.player.state.rate,
builder: (context, snapshot) { builder: (context, snapshot) {
final currentRate = snapshot.data ?? 1.0; final currentRate = snapshot.data ?? 1.0;
@@ -248,8 +249,8 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
// Audio Output Device (Desktop only) // Audio Output Device (Desktop only)
if (isDesktop) if (isDesktop)
StreamBuilder<AudioDevice>( StreamBuilder<MpvAudioDevice>(
stream: widget.player.stream.audioDevice, stream: widget.player.streams.audioDevice,
initialData: widget.player.state.audioDevice, initialData: widget.player.state.audioDevice,
builder: (context, snapshot) { builder: (context, snapshot) {
final currentDevice = final currentDevice =
@@ -273,7 +274,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
Widget _buildSpeedView() { Widget _buildSpeedView() {
return StreamBuilder<double>( return StreamBuilder<double>(
stream: widget.player.stream.rate, stream: widget.player.streams.rate,
initialData: widget.player.state.rate, initialData: widget.player.state.rate,
builder: (context, snapshot) { builder: (context, snapshot) {
final currentRate = snapshot.data ?? 1.0; final currentRate = snapshot.data ?? 1.0;
@@ -352,14 +353,14 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
} }
Widget _buildAudioDeviceView() { Widget _buildAudioDeviceView() {
return StreamBuilder<List<AudioDevice>>( return StreamBuilder<List<MpvAudioDevice>>(
stream: widget.player.stream.audioDevices, stream: widget.player.streams.audioDevices,
initialData: widget.player.state.audioDevices, initialData: widget.player.state.audioDevices,
builder: (context, snapshot) { builder: (context, snapshot) {
final devices = snapshot.data ?? []; final devices = snapshot.data ?? [];
return StreamBuilder<AudioDevice>( return StreamBuilder<MpvAudioDevice>(
stream: widget.player.stream.audioDevice, stream: widget.player.streams.audioDevice,
initialData: widget.player.state.audioDevice, initialData: widget.player.state.audioDevice,
builder: (context, selectedSnapshot) { builder: (context, selectedSnapshot) {
final currentDevice = final currentDevice =
+33 -9
View File
@@ -5,9 +5,10 @@ import 'package:flutter/material.dart';
import 'package:rate_limiter/rate_limiter.dart'; import 'package:rate_limiter/rate_limiter.dart';
import 'package:flutter/services.dart' show SystemChrome, DeviceOrientation; import 'package:flutter/services.dart' show SystemChrome, DeviceOrientation;
import 'package:macos_window_utils/macos_window_utils.dart'; import 'package:macos_window_utils/macos_window_utils.dart';
import 'package:media_kit/media_kit.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import '../../mpv/mpv.dart';
import '../../client/plex_client.dart'; import '../../client/plex_client.dart';
import '../../models/plex_media_info.dart'; import '../../models/plex_media_info.dart';
import '../../models/plex_media_version.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 /// Custom video controls builder for Plex with chapter, audio, and subtitle support
Widget plexVideoControlsBuilder( Widget plexVideoControlsBuilder(
Player player, MpvPlayer player,
PlexMetadata metadata, { PlexMetadata metadata, {
VoidCallback? onNext, VoidCallback? onNext,
VoidCallback? onPrevious, VoidCallback? onPrevious,
@@ -35,8 +36,8 @@ Widget plexVideoControlsBuilder(
int? selectedMediaIndex, int? selectedMediaIndex,
int boxFitMode = 0, int boxFitMode = 0,
VoidCallback? onCycleBoxFitMode, VoidCallback? onCycleBoxFitMode,
Function(AudioTrack)? onAudioTrackChanged, Function(MpvAudioTrack)? onAudioTrackChanged,
Function(SubtitleTrack)? onSubtitleTrackChanged, Function(MpvSubtitleTrack)? onSubtitleTrackChanged,
}) { }) {
return PlexVideoControls( return PlexVideoControls(
player: player, player: player,
@@ -53,7 +54,7 @@ Widget plexVideoControlsBuilder(
} }
class PlexVideoControls extends StatefulWidget { class PlexVideoControls extends StatefulWidget {
final Player player; final MpvPlayer player;
final PlexMetadata metadata; final PlexMetadata metadata;
final VoidCallback? onNext; final VoidCallback? onNext;
final VoidCallback? onPrevious; final VoidCallback? onPrevious;
@@ -61,8 +62,8 @@ class PlexVideoControls extends StatefulWidget {
final int selectedMediaIndex; final int selectedMediaIndex;
final int boxFitMode; final int boxFitMode;
final VoidCallback? onCycleBoxFitMode; final VoidCallback? onCycleBoxFitMode;
final Function(AudioTrack)? onAudioTrackChanged; final Function(MpvAudioTrack)? onAudioTrackChanged;
final Function(SubtitleTrack)? onSubtitleTrackChanged; final Function(MpvSubtitleTrack)? onSubtitleTrackChanged;
const PlexVideoControls({ const PlexVideoControls({
super.key, super.key,
@@ -114,6 +115,9 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
bool _markersLoaded = false; bool _markersLoaded = false;
// Playback state subscription for auto-hide timer // Playback state subscription for auto-hide timer
StreamSubscription<bool>? _playingSubscription; StreamSubscription<bool>? _playingSubscription;
// Window resize pause state
Timer? _resizeDebounceTimer;
bool _wasPlayingBeforeResize = false;
@override @override
void initState() { void initState() {
@@ -145,7 +149,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
} }
void _listenToPosition() { void _listenToPosition() {
widget.player.stream.position.listen((position) { widget.player.streams.position.listen((position) {
if (_markers.isEmpty || !_markersLoaded) { if (_markers.isEmpty || !_markersLoaded) {
return; return;
} }
@@ -170,7 +174,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
/// Listen to playback state changes to manage auto-hide timer on iOS/mobile /// Listen to playback state changes to manage auto-hide timer on iOS/mobile
void _listenToPlayingState() { void _listenToPlayingState() {
_playingSubscription = widget.player.stream.playing.listen((isPlaying) { _playingSubscription = widget.player.streams.playing.listen((isPlaying) {
if (isPlaying && _showControls) { if (isPlaying && _showControls) {
_startHideTimer(); _startHideTimer();
} else if (!isPlaying) { } else if (!isPlaying) {
@@ -240,6 +244,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
void dispose() { void dispose() {
_hideTimer?.cancel(); _hideTimer?.cancel();
_feedbackTimer?.cancel(); _feedbackTimer?.cancel();
_resizeDebounceTimer?.cancel();
_seekThrottle.cancel(); _seekThrottle.cancel();
_playingSubscription?.cancel(); _playingSubscription?.cancel();
_focusNode.dispose(); _focusNode.dispose();
@@ -298,6 +303,25 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
} }
} }
@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() { void _startHideTimer() {
_hideTimer?.cancel(); _hideTimer?.cancel();
// Only auto-hide if playing // Only auto-hide if playing
@@ -1,12 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
import '../../../mpv/mpv.dart';
import '../../../services/sleep_timer_service.dart'; import '../../../services/sleep_timer_service.dart';
import 'sleep_timer_active_status.dart'; import 'sleep_timer_active_status.dart';
import 'sleep_timer_duration_list.dart'; import 'sleep_timer_duration_list.dart';
/// Shared UI for sleep timer selection and active status. /// Shared UI for sleep timer selection and active status.
class SleepTimerContent extends StatelessWidget { class SleepTimerContent extends StatelessWidget {
final Player player; final MpvPlayer player;
final SleepTimerService sleepTimer; final SleepTimerService sleepTimer;
final int? defaultDuration; final int? defaultDuration;
final VoidCallback? onCancel; final VoidCallback? onCancel;
@@ -1,12 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
import '../../../mpv/mpv.dart';
import '../../../services/sleep_timer_service.dart'; import '../../../services/sleep_timer_service.dart';
import '../../../utils/duration_formatter.dart'; import '../../../utils/duration_formatter.dart';
import '../../../i18n/strings.g.dart'; import '../../../i18n/strings.g.dart';
/// Widget displaying list of sleep timer durations for selection /// Widget displaying list of sleep timer durations for selection
class SleepTimerDurationList extends StatelessWidget { class SleepTimerDurationList extends StatelessWidget {
final Player player; final MpvPlayer player;
final SleepTimerService sleepTimer; final SleepTimerService sleepTimer;
final int? defaultDuration; final int? defaultDuration;
@@ -1,11 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
import '../../../mpv/mpv.dart';
import '../../../i18n/strings.g.dart'; import '../../../i18n/strings.g.dart';
import '../../../utils/duration_formatter.dart'; import '../../../utils/duration_formatter.dart';
/// Reusable widget for adjusting sync offsets (audio or subtitle) /// Reusable widget for adjusting sync offsets (audio or subtitle)
class SyncOffsetControl extends StatefulWidget { class SyncOffsetControl extends StatefulWidget {
final Player player; final MpvPlayer player;
final String propertyName; // 'audio-delay' or 'sub-delay' final String propertyName; // 'audio-delay' or 'sub-delay'
final int initialOffset; final int initialOffset;
final String labelText; // 'Audio' or 'Subtitles' final String labelText; // 'Audio' or 'Subtitles'
@@ -44,11 +45,11 @@ class _SyncOffsetControlState extends State<SyncOffsetControl> {
} }
Future<void> _applyOffset(double offsetMs) async { Future<void> _applyOffset(double offsetMs) async {
// Convert milliseconds to seconds for media_kit // Convert milliseconds to seconds for mpv
final offsetSeconds = offsetMs / 1000.0; final offsetSeconds = offsetMs / 1000.0;
// Apply to player using setProperty // Apply to player using setProperty
await (widget.player.platform as dynamic).setProperty( await widget.player.setProperty(
widget.propertyName, widget.propertyName,
offsetSeconds.toString(), offsetSeconds.toString(),
); );
@@ -1,8 +1,8 @@
import 'dart:io' show Platform; import 'dart:io' show Platform;
import 'package:flutter/material.dart'; 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_info.dart';
import '../../../models/plex_media_version.dart'; import '../../../models/plex_media_version.dart';
import '../../../services/sleep_timer_service.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 /// Row of track and chapter control buttons for the video player
class TrackChapterControls extends StatelessWidget { class TrackChapterControls extends StatelessWidget {
final Player player; final MpvPlayer player;
final List<PlexChapter> chapters; final List<PlexChapter> chapters;
final bool chaptersLoaded; final bool chaptersLoaded;
final List<PlexMediaVersion> availableVersions; final List<PlexMediaVersion> availableVersions;
@@ -31,8 +31,8 @@ class TrackChapterControls extends StatelessWidget {
final VoidCallback? onToggleRotationLock; final VoidCallback? onToggleRotationLock;
final VoidCallback? onToggleFullscreen; final VoidCallback? onToggleFullscreen;
final Function(int)? onSwitchVersion; final Function(int)? onSwitchVersion;
final Function(AudioTrack)? onAudioTrackChanged; final Function(MpvAudioTrack)? onAudioTrackChanged;
final Function(SubtitleTrack)? onSubtitleTrackChanged; final Function(MpvSubtitleTrack)? onSubtitleTrackChanged;
final VoidCallback? onLoadSeekTimes; final VoidCallback? onLoadSeekTimes;
final String serverId; final String serverId;
@@ -60,8 +60,8 @@ class TrackChapterControls extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StreamBuilder<Tracks>( return StreamBuilder<MpvTracks>(
stream: player.stream.tracks, stream: player.streams.tracks,
initialData: player.state.tracks, initialData: player.state.tracks,
builder: (context, snapshot) { builder: (context, snapshot) {
final tracks = snapshot.data; 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; if (tracks == null) return false;
final audioTracks = tracks.audio final audioTracks = tracks.audio
.where((track) => track.id != 'auto' && track.id != 'no') .where((track) => track.id != 'auto' && track.id != 'no')
@@ -183,7 +183,7 @@ class TrackChapterControls extends StatelessWidget {
return audioTracks.length > 1; return audioTracks.length > 1;
} }
bool _hasSubtitles(Tracks? tracks) { bool _hasSubtitles(MpvTracks? tracks) {
if (tracks == null) return false; if (tracks == null) return false;
final subtitles = tracks.subtitle final subtitles = tracks.subtitle
.where((track) => track.id != 'auto' && track.id != 'no') .where((track) => track.id != 'auto' && track.id != 'no')
@@ -1,22 +1,22 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart';
import '../../../mpv/mpv.dart';
import '../../../services/settings_service.dart'; import '../../../services/settings_service.dart';
import '../../../i18n/strings.g.dart'; import '../../../i18n/strings.g.dart';
/// A volume control widget that displays a mute/unmute button and volume slider. /// 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]. /// the volume setting using [SettingsService].
class VolumeControl extends StatelessWidget { class VolumeControl extends StatelessWidget {
final Player player; final MpvPlayer player;
const VolumeControl({super.key, required this.player}); const VolumeControl({super.key, required this.player});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StreamBuilder<double>( return StreamBuilder<double>(
stream: player.stream.volume, stream: player.streams.volume,
initialData: player.state.volume, initialData: player.state.volume,
builder: (context, snapshot) { builder: (context, snapshot) {
final volume = snapshot.data ?? 100.0; final volume = snapshot.data ?? 100.0;
@@ -7,24 +7,15 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <hotkey_manager_linux/hotkey_manager_linux_plugin.h> #include <hotkey_manager_linux/hotkey_manager_linux_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h>
#include <os_media_controls/os_media_controls_plugin.h> #include <os_media_controls/os_media_controls_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h> #include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
#include <volume_controller/volume_controller_plugin.h>
#include <window_manager/window_manager_plugin.h> #include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) hotkey_manager_linux_registrar = g_autoptr(FlPluginRegistrar) hotkey_manager_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerLinuxPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerLinuxPlugin");
hotkey_manager_linux_plugin_register_with_registrar(hotkey_manager_linux_registrar); 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 = g_autoptr(FlPluginRegistrar) os_media_controls_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "OsMediaControlsPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "OsMediaControlsPlugin");
os_media_controls_plugin_register_with_registrar(os_media_controls_registrar); 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 = g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 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 = g_autoptr(FlPluginRegistrar) window_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar); window_manager_plugin_register_with_registrar(window_manager_registrar);
-3
View File
@@ -4,12 +4,9 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
hotkey_manager_linux hotkey_manager_linux
media_kit_libs_linux
media_kit_video
os_media_controls os_media_controls
screen_retriever_linux screen_retriever_linux
url_launcher_linux url_launcher_linux
volume_controller
window_manager window_manager
) )
@@ -8,8 +8,6 @@ import Foundation
import connectivity_plus import connectivity_plus
import hotkey_manager_macos import hotkey_manager_macos
import macos_window_utils import macos_window_utils
import media_kit_libs_macos_video
import media_kit_video
import os_media_controls import os_media_controls
import package_info_plus import package_info_plus
import path_provider_foundation import path_provider_foundation
@@ -17,16 +15,12 @@ import screen_retriever_macos
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin import sqflite_darwin
import url_launcher_macos import url_launcher_macos
import volume_controller
import wakelock_plus
import window_manager import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin")) HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin")) MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
OsMediaControlsPlugin.register(with: registry.registrar(forPlugin: "OsMediaControlsPlugin")) OsMediaControlsPlugin.register(with: registry.registrar(forPlugin: "OsMediaControlsPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
@@ -34,7 +28,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 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")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
} }
+1 -1
View File
@@ -1,4 +1,4 @@
platform :osx, '10.15' platform :osx, '14.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency. # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true' ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+1 -25
View File
@@ -8,10 +8,6 @@ PODS:
- HotKey - HotKey
- macos_window_utils (1.0.0): - macos_window_utils (1.0.0):
- FlutterMacOS - FlutterMacOS
- media_kit_libs_macos_video (1.0.4):
- FlutterMacOS
- media_kit_video (0.0.1):
- FlutterMacOS
- os_media_controls (0.0.1): - os_media_controls (0.0.1):
- FlutterMacOS - FlutterMacOS
- package_info_plus (0.0.1): - package_info_plus (0.0.1):
@@ -29,10 +25,6 @@ PODS:
- FlutterMacOS - FlutterMacOS
- url_launcher_macos (0.0.1): - url_launcher_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- volume_controller (0.0.1):
- FlutterMacOS
- wakelock_plus (0.0.1):
- FlutterMacOS
- window_manager (0.2.0): - window_manager (0.2.0):
- FlutterMacOS - FlutterMacOS
@@ -41,8 +33,6 @@ DEPENDENCIES:
- FlutterMacOS (from `Flutter/ephemeral`) - FlutterMacOS (from `Flutter/ephemeral`)
- hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`) - hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`)
- macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`) - macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`)
- 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`) - os_media_controls (from `Flutter/ephemeral/.symlinks/plugins/os_media_controls/macos`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/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`) - 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`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - 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`) - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
SPEC REPOS: SPEC REPOS:
@@ -67,10 +55,6 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos
macos_window_utils: macos_window_utils:
:path: Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos :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: os_media_controls:
:path: Flutter/ephemeral/.symlinks/plugins/os_media_controls/macos :path: Flutter/ephemeral/.symlinks/plugins/os_media_controls/macos
package_info_plus: package_info_plus:
@@ -85,10 +69,6 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
url_launcher_macos: url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/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: window_manager:
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
@@ -98,8 +78,6 @@ SPEC CHECKSUMS:
HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
hotkey_manager_macos: a4317849af96d2430fa89944d3c58977ca089fbe hotkey_manager_macos: a4317849af96d2430fa89944d3c58977ca089fbe
macos_window_utils: 23f54331a0fd51eea9e0ed347253bf48fd379d1d macos_window_utils: 23f54331a0fd51eea9e0ed347253bf48fd379d1d
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758
os_media_controls: c07c04c4afdf59dda0a3f398457a46823c4ce0ed os_media_controls: c07c04c4afdf59dda0a3f398457a46823c4ce0ed
package_info_plus: f0052d280d17aa382b932f399edf32507174e870 package_info_plus: f0052d280d17aa382b932f399edf32507174e870
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
@@ -107,10 +85,8 @@ SPEC CHECKSUMS:
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd
wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b
window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 PODFILE CHECKSUM: d16f5e5d196d1ca9863d7a71d5885cad7ffa7d2d
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2
+43 -3
View File
@@ -26,7 +26,10 @@
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; 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 */; }; 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 */; }; 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 */; }; FE52EE1D2D489FA88507D14E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84260356652A3270E664FED4 /* Pods_RunnerTests.framework */; };
/* End PBXBuildFile section */ /* 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 = "<group>"; }; 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 = "<group>"; };
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 = "<group>"; }; 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 = "<group>"; };
6AC86ED62EA70B4C0067BC66 /* plezy.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = plezy.icon; sourceTree = "<group>"; }; 6AC86ED62EA70B4C0067BC66 /* plezy.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = plezy.icon; sourceTree = "<group>"; };
6AD8B1632ED7B50000E9E1B4 /* MpvPlayerCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPlayerCore.swift; sourceTree = "<group>"; };
6AD8B1642ED7B50000E9E1B4 /* MpvPlayerPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPlayerPlugin.swift; sourceTree = "<group>"; };
78F9F23331B540A25C4A70C7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
84260356652A3270E664FED4 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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; buildActionMask = 2147483647;
files = ( files = (
9AECA605E2E1BECE3CA5BD01 /* Pods_Runner.framework in Frameworks */, 9AECA605E2E1BECE3CA5BD01 /* Pods_Runner.framework in Frameworks */,
6AB6B0952EDB27C100EAC8DB /* MPVKit-GPL in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -190,6 +196,7 @@
children = ( children = (
33CC10F02044A3C60003C045 /* AppDelegate.swift */, 33CC10F02044A3C60003C045 /* AppDelegate.swift */,
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
6AD8B1652ED7B50000E9E1B4 /* MpvPlayer */,
33E51913231747F40026EE4D /* DebugProfile.entitlements */, 33E51913231747F40026EE4D /* DebugProfile.entitlements */,
33E51914231749380026EE4D /* Release.entitlements */, 33E51914231749380026EE4D /* Release.entitlements */,
33CC11242044D66E0003C045 /* Resources */, 33CC11242044D66E0003C045 /* Resources */,
@@ -207,6 +214,15 @@
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
6AD8B1652ED7B50000E9E1B4 /* MpvPlayer */ = {
isa = PBXGroup;
children = (
6AD8B1632ED7B50000E9E1B4 /* MpvPlayerCore.swift */,
6AD8B1642ED7B50000E9E1B4 /* MpvPlayerPlugin.swift */,
);
path = MpvPlayer;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@@ -291,6 +307,9 @@
Base, Base,
); );
mainGroup = 33CC10E42044A3C60003C045; mainGroup = 33CC10E42044A3C60003C045;
packageReferences = (
6AB6B0932EDB27C100EAC8DB /* XCRemoteSwiftPackageReference "MPVKit" */,
);
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
projectDirPath = ""; projectDirPath = "";
projectRoot = ""; projectRoot = "";
@@ -439,6 +458,8 @@
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
6AD8B1612ED7B50000E9E1B4 /* MpvPlayerCore.swift in Sources */,
6AD8B1622ED7B50000E9E1B4 /* MpvPlayerPlugin.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -556,7 +577,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15; MACOSX_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx; SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;
@@ -638,7 +659,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15; MACOSX_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx; SDKROOT = macosx;
@@ -688,7 +709,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15; MACOSX_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx; SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;
@@ -795,6 +816,25 @@
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
/* End XCConfigurationList section */ /* 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 */; rootObject = 33CC10E52044A3C60003C045 /* Project object */;
} }
@@ -0,0 +1,14 @@
{
"originHash" : "c535d3cdb62bd4e11e67f3c9e68290fa7ee9306634c5f56d2b0396eb75ed41ee",
"pins" : [
{
"identity" : "mpvkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/edde746/MPVKit",
"state" : {
"revision" : "a8ae0d2f08df6188c662b7670bddb97131db8c5a"
}
}
],
"version" : 3
}
@@ -0,0 +1,14 @@
{
"originHash" : "c535d3cdb62bd4e11e67f3c9e68290fa7ee9306634c5f56d2b0396eb75ed41ee",
"pins" : [
{
"identity" : "mpvkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/edde746/MPVKit",
"state" : {
"revision" : "a8ae0d2f08df6188c662b7670bddb97131db8c5a"
}
}
],
"version" : 3
}
+8
View File
@@ -4,10 +4,18 @@ import FlutterMacOS
class MainFlutterWindow: NSWindow { class MainFlutterWindow: NSWindow {
override func awakeFromNib() { override func awakeFromNib() {
let flutterViewController = FlutterViewController() let flutterViewController = FlutterViewController()
// Enable transparency for Metal layer behind Flutter
self.backgroundColor = NSColor.clear
flutterViewController.backgroundColor = NSColor.clear
let windowFrame = self.frame let windowFrame = self.frame
self.contentViewController = flutterViewController self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: true) self.setFrame(windowFrame, display: true)
// Register MPV player plugin for video playback
MpvPlayerPlugin.register(with: flutterViewController.registrar(forPlugin: "MpvPlayerPlugin"))
RegisterGeneratedPlugins(registry: flutterViewController) RegisterGeneratedPlugins(registry: flutterViewController)
super.awakeFromNib() super.awakeFromNib()
+382
View File
@@ -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<MpvPlayerCore>.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<CChar>?] = ([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<CChar>?.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..<Int(list.num) {
if let item = convertNode(list.values[i]) {
array.append(item)
}
}
return array
case MPV_FORMAT_NODE_MAP:
guard let list = node.u.list?.pointee else { return nil }
var dict = [String: Any]()
for i in 0..<Int(list.num) {
if let key = list.keys?[i].map({ String(cString: $0) }),
let val = convertNode(list.values[i]) {
dict[key] = val
}
}
return dict
default:
return nil
}
}
private func checkError(_ status: CInt) {
if status < 0 {
print("[MpvPlayerCore] MPV error: \(String(cString: mpv_error_string(status)))")
}
}
// MARK: - Cleanup
func dispose() {
if let mpv = mpv {
mpv_terminate_destroy(mpv)
self.mpv = nil
}
metalLayer?.removeFromSuperlayer()
metalLayer = nil
isInitialized = false
print("[MpvPlayerCore] Disposed")
}
deinit {
dispose()
}
}
@@ -0,0 +1,249 @@
import Cocoa
import FlutterMacOS
/// Flutter plugin that bridges MPV player to Dart via method and event channels
class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerDelegate {
// MARK: - Properties
private var playerCore: MpvPlayerCore?
private var eventSink: FlutterEventSink?
private weak var registrar: FlutterPluginRegistrar?
// MARK: - FlutterPlugin Registration
static func register(with registrar: FlutterPluginRegistrar) {
// Method channel for commands
let methodChannel = FlutterMethodChannel(
name: "com.plezy/mpv_player",
binaryMessenger: registrar.messenger
)
// Event channel for state updates
let eventChannel = FlutterEventChannel(
name: "com.plezy/mpv_player/events",
binaryMessenger: registrar.messenger
)
let instance = MpvPlayerPlugin()
instance.registrar = registrar
registrar.addMethodCallDelegate(instance, channel: methodChannel)
eventChannel.setStreamHandler(instance)
print("[MpvPlayerPlugin] Registered with Flutter")
}
// MARK: - FlutterStreamHandler
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> 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
}
}
-136
View File
@@ -600,78 +600,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.1" 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: meta:
dependency: transitive dependency: transitive
description: description:
@@ -913,30 +841,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.28.0" 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: screen_retriever:
dependency: transitive dependency: transitive
description: description:
@@ -1222,22 +1126,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.3" 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: url_launcher:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1326,30 +1214,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.0.2" 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: watcher:
dependency: transitive dependency: transitive
description: description:
-35
View File
@@ -13,9 +13,6 @@ dependencies:
json_annotation: ^4.8.1 json_annotation: ^4.8.1
shared_preferences: ^2.2.2 shared_preferences: ^2.2.2
cached_network_image: ^3.4.1 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 macos_window_utils: ^1.9.0
url_launcher: ^6.3.0 url_launcher: ^6.3.0
uuid: ^4.4.0 uuid: ^4.4.0
@@ -36,38 +33,6 @@ dependencies:
ref: 75dc5642cd148c54c03fe96976702b8d31c48599 ref: 75dc5642cd148c54c03fe96976702b8d31c48599
rate_limiter: ^1.0.0 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: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
@@ -8,12 +8,9 @@
#include <connectivity_plus/connectivity_plus_windows_plugin.h> #include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <hotkey_manager_windows/hotkey_manager_windows_plugin_c_api.h> #include <hotkey_manager_windows/hotkey_manager_windows_plugin_c_api.h>
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
#include <media_kit_video/media_kit_video_plugin_c_api.h>
#include <os_media_controls/os_media_controls_plugin_c_api.h> #include <os_media_controls/os_media_controls_plugin_c_api.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h> #include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
#include <volume_controller/volume_controller_plugin_c_api.h>
#include <window_manager/window_manager_plugin.h> #include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
@@ -21,18 +18,12 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
HotkeyManagerWindowsPluginCApiRegisterWithRegistrar( HotkeyManagerWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi")); registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi"));
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi"));
MediaKitVideoPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
OsMediaControlsPluginCApiRegisterWithRegistrar( OsMediaControlsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("OsMediaControlsPluginCApi")); registry->GetRegistrarForPlugin("OsMediaControlsPluginCApi"));
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows")); registry->GetRegistrarForPlugin("UrlLauncherWindows"));
VolumeControllerPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("VolumeControllerPluginCApi"));
WindowManagerPluginRegisterWithRegistrar( WindowManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowManagerPlugin")); registry->GetRegistrarForPlugin("WindowManagerPlugin"));
} }
-3
View File
@@ -5,12 +5,9 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus connectivity_plus
hotkey_manager_windows hotkey_manager_windows
media_kit_libs_windows_video
media_kit_video
os_media_controls os_media_controls
screen_retriever_windows screen_retriever_windows
url_launcher_windows url_launcher_windows
volume_controller
window_manager window_manager
) )