refactor: fix naming

This commit is contained in:
edde746
2025-12-02 16:53:54 +01:00
parent a122fb048e
commit 6d9a56fa73
45 changed files with 403 additions and 403 deletions
+18 -18
View File
@@ -17,7 +17,7 @@
/// import 'package:flutter_application_1/mpv/mpv.dart'; /// import 'package:flutter_application_1/mpv/mpv.dart';
/// ///
/// // Create a player /// // Create a player
/// final player = MpvPlayer(); /// final player = Player();
/// ///
/// // Configure player properties /// // Configure player properties
/// await player.setProperty('hwdec', 'auto'); /// await player.setProperty('hwdec', 'auto');
@@ -25,7 +25,7 @@
/// await player.setAudioPassthrough(true); /// await player.setAudioPassthrough(true);
/// ///
/// // Open and play media /// // Open and play media
/// await player.open(MpvMedia('https://example.com/video.mp4')); /// await player.open(Media('https://example.com/video.mp4'));
/// ///
/// // Listen to state changes /// // Listen to state changes
/// player.streams.position.listen((position) { /// player.streams.position.listen((position) {
@@ -33,7 +33,7 @@
/// }); /// });
/// ///
/// // Display video /// // Display video
/// MpvVideo( /// Video(
/// player: player, /// player: player,
/// controls: (context) => MyCustomControls(), /// controls: (context) => MyCustomControls(),
/// ) /// )
@@ -44,26 +44,26 @@
library; library;
// Import for type alias // Import for type alias
import 'src/models/mpv_track_selection.dart' as track_selection; import 'src/models/track_selection.dart' as track_selection;
// Player // Player
export 'src/player/mpv_player.dart'; export 'src/player/player.dart';
export 'src/player/mpv_player_state.dart'; export 'src/player/player_state.dart';
export 'src/player/mpv_player_streams.dart'; export 'src/player/player_streams.dart';
export 'src/player/mpv_player_stub.dart'; export 'src/player/player_stub.dart';
// Models // Models
export 'src/models/mpv_media.dart'; export 'src/models/media.dart';
export 'src/models/mpv_audio_device.dart'; export 'src/models/audio_device.dart';
export 'src/models/mpv_audio_track.dart'; export 'src/models/audio_track.dart';
export 'src/models/mpv_subtitle_track.dart'; export 'src/models/subtitle_track.dart';
export 'src/models/mpv_tracks.dart'; export 'src/models/tracks.dart';
export 'src/models/mpv_track_selection.dart'; export 'src/models/track_selection.dart';
export 'src/models/mpv_log.dart'; export 'src/models/player_log.dart';
export 'src/models/mpv_log_level.dart'; export 'src/models/player_log_level.dart';
// Video // Video
export 'src/video/mpv_video.dart'; export 'src/video/video.dart';
// Type alias for compatibility (must be after exports) // Type alias for compatibility (must be after exports)
typedef MpvTrack = track_selection.MpvTrackSelection; typedef Track = track_selection.TrackSelection;
@@ -1,26 +1,26 @@
/// Represents an audio output device. /// Represents an audio output device.
class MpvAudioDevice { class AudioDevice {
/// Unique identifier for the device. /// Unique identifier for the device.
final String name; final String name;
/// Human-readable description of the device. /// Human-readable description of the device.
final String description; final String description;
const MpvAudioDevice({ const AudioDevice({
required this.name, required this.name,
this.description = '', this.description = '',
}); });
/// Default/auto audio device. /// Default/auto audio device.
static const auto = MpvAudioDevice(name: 'auto', description: 'Auto'); static const auto = AudioDevice(name: 'auto', description: 'Auto');
@override @override
String toString() => 'MpvAudioDevice($name, $description)'; String toString() => 'AudioDevice($name, $description)';
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is MpvAudioDevice && other is AudioDevice &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
name == other.name; name == other.name;
@@ -1,5 +1,5 @@
/// Represents an audio track in the media. /// Represents an audio track in the media.
class MpvAudioTrack { class AudioTrack {
/// Unique identifier for the track. /// Unique identifier for the track.
final String id; final String id;
@@ -30,7 +30,7 @@ class MpvAudioTrack {
/// Whether this track is forced. /// Whether this track is forced.
final bool isForced; final bool isForced;
const MpvAudioTrack({ const AudioTrack({
required this.id, required this.id,
this.title, this.title,
this.language, this.language,
@@ -43,10 +43,10 @@ class MpvAudioTrack {
}); });
/// Auto-select track. /// Auto-select track.
static const auto = MpvAudioTrack(id: 'auto', title: 'Auto'); static const auto = AudioTrack(id: 'auto', title: 'Auto');
/// Disable audio. /// Disable audio.
static const off = MpvAudioTrack(id: 'no', title: 'Off'); static const off = AudioTrack(id: 'no', title: 'Off');
/// Returns a display name for the track. /// Returns a display name for the track.
String get displayName { String get displayName {
@@ -56,12 +56,12 @@ class MpvAudioTrack {
} }
@override @override
String toString() => 'MpvAudioTrack($id, $displayName)'; String toString() => 'AudioTrack($id, $displayName)';
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is MpvAudioTrack && other is AudioTrack &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
id == other.id; id == other.id;
@@ -1,5 +1,5 @@
/// Represents a media source for the MPV player. /// Represents a media source for the player.
class MpvMedia { class Media {
/// The URI of the media (file path, HTTP URL, etc.). /// The URI of the media (file path, HTTP URL, etc.).
final String uri; final String uri;
@@ -9,19 +9,19 @@ class MpvMedia {
/// Optional start position for playback. /// Optional start position for playback.
final Duration? start; final Duration? start;
const MpvMedia( const Media(
this.uri, { this.uri, {
this.headers, this.headers,
this.start, this.start,
}); });
@override @override
String toString() => 'MpvMedia($uri)'; String toString() => 'Media($uri)';
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is MpvMedia && other is Media &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
uri == other.uri && uri == other.uri &&
start == other.start; start == other.start;
@@ -1,30 +0,0 @@
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)';
}
@@ -1,9 +1,9 @@
import 'mpv_log_level.dart'; import 'player_log_level.dart';
/// A log entry from the MPV player. /// A log entry from the player.
class MpvLog { class PlayerLog {
/// The log level of this message. /// The log level of this message.
final MpvLogLevel level; final PlayerLogLevel level;
/// The prefix/category of the log message (e.g., 'cplayer', 'ffmpeg'). /// The prefix/category of the log message (e.g., 'cplayer', 'ffmpeg').
final String prefix; final String prefix;
@@ -11,7 +11,7 @@ class MpvLog {
/// The log message text. /// The log message text.
final String text; final String text;
const MpvLog({ const PlayerLog({
required this.level, required this.level,
required this.prefix, required this.prefix,
required this.text, required this.text,
@@ -1,5 +1,5 @@
/// Log level for MPV player messages. /// Log level for player messages.
enum MpvLogLevel { enum PlayerLogLevel {
/// No logging. /// No logging.
none, none,
@@ -1,5 +1,5 @@
/// Represents a subtitle track in the media. /// Represents a subtitle track in the media.
class MpvSubtitleTrack { class SubtitleTrack {
/// Unique identifier for the track. /// Unique identifier for the track.
final String id; final String id;
@@ -24,7 +24,7 @@ class MpvSubtitleTrack {
/// URI of external subtitle file (if isExternal is true). /// URI of external subtitle file (if isExternal is true).
final String? uri; final String? uri;
const MpvSubtitleTrack({ const SubtitleTrack({
required this.id, required this.id,
this.title, this.title,
this.language, this.language,
@@ -36,12 +36,12 @@ class MpvSubtitleTrack {
}); });
/// Create a subtitle track from an external URI. /// Create a subtitle track from an external URI.
factory MpvSubtitleTrack.uri( factory SubtitleTrack.uri(
String uri, { String uri, {
String? title, String? title,
String? language, String? language,
}) { }) {
return MpvSubtitleTrack( return SubtitleTrack(
id: 'external:$uri', id: 'external:$uri',
title: title, title: title,
language: language, language: language,
@@ -51,10 +51,10 @@ class MpvSubtitleTrack {
} }
/// Auto-select track. /// Auto-select track.
static const auto = MpvSubtitleTrack(id: 'auto', title: 'Auto'); static const auto = SubtitleTrack(id: 'auto', title: 'Auto');
/// Disable subtitles. /// Disable subtitles.
static const off = MpvSubtitleTrack(id: 'no', title: 'Off'); static const off = SubtitleTrack(id: 'no', title: 'Off');
/// Returns a display name for the track. /// Returns a display name for the track.
String get displayName { String get displayName {
@@ -65,12 +65,12 @@ class MpvSubtitleTrack {
} }
@override @override
String toString() => 'MpvSubtitleTrack($id, $displayName)'; String toString() => 'SubtitleTrack($id, $displayName)';
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is MpvSubtitleTrack && other is SubtitleTrack &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
id == other.id; id == other.id;
+30
View File
@@ -0,0 +1,30 @@
import 'audio_track.dart';
import 'subtitle_track.dart';
/// Represents the currently selected tracks.
class TrackSelection {
/// Currently selected audio track.
final AudioTrack? audio;
/// Currently selected subtitle track.
final SubtitleTrack? subtitle;
const TrackSelection({
this.audio,
this.subtitle,
});
/// Creates a copy with the given fields replaced.
TrackSelection copyWith({
AudioTrack? audio,
SubtitleTrack? subtitle,
}) {
return TrackSelection(
audio: audio ?? this.audio,
subtitle: subtitle ?? this.subtitle,
);
}
@override
String toString() => 'TrackSelection(audio: $audio, subtitle: $subtitle)';
}
@@ -1,25 +1,25 @@
import 'mpv_audio_track.dart'; import 'audio_track.dart';
import 'mpv_subtitle_track.dart'; import 'subtitle_track.dart';
/// Container for all available tracks in the media. /// Container for all available tracks in the media.
class MpvTracks { class Tracks {
/// Available audio tracks. /// Available audio tracks.
final List<MpvAudioTrack> audio; final List<AudioTrack> audio;
/// Available subtitle tracks. /// Available subtitle tracks.
final List<MpvSubtitleTrack> subtitle; final List<SubtitleTrack> subtitle;
const MpvTracks({ const Tracks({
this.audio = const [], this.audio = const [],
this.subtitle = const [], this.subtitle = const [],
}); });
/// Creates a copy with the given fields replaced. /// Creates a copy with the given fields replaced.
MpvTracks copyWith({ Tracks copyWith({
List<MpvAudioTrack>? audio, List<AudioTrack>? audio,
List<MpvSubtitleTrack>? subtitle, List<SubtitleTrack>? subtitle,
}) { }) {
return MpvTracks( return Tracks(
audio: audio ?? this.audio, audio: audio ?? this.audio,
subtitle: subtitle ?? this.subtitle, subtitle: subtitle ?? this.subtitle,
); );
@@ -27,5 +27,5 @@ class MpvTracks {
@override @override
String toString() => String toString() =>
'MpvTracks(audio: ${audio.length}, subtitle: ${subtitle.length})'; 'Tracks(audio: ${audio.length}, subtitle: ${subtitle.length})';
} }
@@ -1,5 +0,0 @@
import 'mpv_player_native.dart';
/// Android implementation of MpvPlayer.
/// Inherits all functionality from MpvPlayerNative.
class MpvPlayerAndroid extends MpvPlayerNative {}
-5
View File
@@ -1,5 +0,0 @@
import 'mpv_player_native.dart';
/// iOS implementation of MpvPlayer.
/// Inherits all functionality from MpvPlayerNative.
class MpvPlayerIOS extends MpvPlayerNative {}
-5
View File
@@ -1,5 +0,0 @@
import 'mpv_player_native.dart';
/// macOS implementation of MpvPlayer.
/// Inherits all functionality from MpvPlayerNative.
class MpvPlayerMacOS extends MpvPlayerNative {}
@@ -1,27 +1,27 @@
import 'dart:io' show Platform; import 'dart:io' show Platform;
import '../models/mpv_audio_device.dart'; import '../models/audio_device.dart';
import '../models/mpv_media.dart'; import '../models/media.dart';
import '../models/mpv_audio_track.dart'; import '../models/audio_track.dart';
import '../models/mpv_subtitle_track.dart'; import '../models/subtitle_track.dart';
import 'mpv_player_android.dart'; import 'player_android.dart';
import 'mpv_player_ios.dart'; import 'player_ios.dart';
import 'mpv_player_linux.dart'; import 'player_linux.dart';
import 'mpv_player_macos.dart'; import 'player_macos.dart';
import 'mpv_player_state.dart'; import 'player_state.dart';
import 'mpv_player_streams.dart'; import 'player_streams.dart';
import 'mpv_player_stub.dart'; import 'player_stub.dart';
import 'mpv_player_windows.dart'; import 'player_windows.dart';
/// Abstract interface for the MPV player. /// Abstract interface for the video player.
/// ///
/// This interface defines all playback control methods, state access, /// This interface defines all playback control methods, state access,
/// and reactive streams for the video player. /// and reactive streams for the video player.
/// ///
/// Example usage: /// Example usage:
/// ```dart /// ```dart
/// final player = MpvPlayer(); /// final player = Player();
/// await player.open(MpvMedia('https://example.com/video.mp4')); /// await player.open(Media('https://example.com/video.mp4'));
/// await player.play(); /// await player.play();
/// ///
/// // Configure player properties /// // Configure player properties
@@ -36,16 +36,16 @@ import 'mpv_player_windows.dart';
/// // Access current state /// // Access current state
/// print('Playing: ${player.state.playing}'); /// print('Playing: ${player.state.playing}');
/// ``` /// ```
abstract class MpvPlayer { abstract class Player {
/// Current synchronous state snapshot. /// Current synchronous state snapshot.
/// ///
/// Use this for immediate state access in UI. /// Use this for immediate state access in UI.
MpvPlayerState get state; PlayerState get state;
/// Reactive streams for state changes. /// Reactive streams for state changes.
/// ///
/// Use these for reactive UI updates. /// Use these for reactive UI updates.
MpvPlayerStreams get streams; PlayerStreams get streams;
/// Texture ID for Flutter's Texture widget (video rendering). /// Texture ID for Flutter's Texture widget (video rendering).
/// ///
@@ -61,7 +61,7 @@ abstract class MpvPlayer {
/// ///
/// [media] - The media source to open. /// [media] - The media source to open.
/// [play] - Whether to start playback immediately (default: true). /// [play] - Whether to start playback immediately (default: true).
Future<void> open(MpvMedia media, {bool play = true}); Future<void> open(Media media, {bool play = true});
/// Start or resume playback. /// Start or resume playback.
Future<void> play(); Future<void> play();
@@ -83,12 +83,12 @@ abstract class MpvPlayer {
// ============================================ // ============================================
/// Select an audio track. /// Select an audio track.
Future<void> selectAudioTrack(MpvAudioTrack track); Future<void> selectAudioTrack(AudioTrack track);
/// Select a subtitle track. /// Select a subtitle track.
/// ///
/// Pass [MpvSubtitleTrack.off] to disable subtitles. /// Pass [SubtitleTrack.off] to disable subtitles.
Future<void> selectSubtitleTrack(MpvSubtitleTrack track); Future<void> selectSubtitleTrack(SubtitleTrack track);
/// Add an external subtitle track. /// Add an external subtitle track.
/// ///
@@ -120,7 +120,7 @@ abstract class MpvPlayer {
/// Set the audio output device. /// Set the audio output device.
/// ///
/// [device] - The audio device to use. /// [device] - The audio device to use.
Future<void> setAudioDevice(MpvAudioDevice device); Future<void> setAudioDevice(AudioDevice device);
// ============================================ // ============================================
// MPV Properties (Advanced) // MPV Properties (Advanced)
@@ -194,31 +194,31 @@ abstract class MpvPlayer {
// Factory // Factory
// ============================================ // ============================================
/// Creates a new MPV player instance. /// Creates a new player instance.
/// ///
/// Returns a platform-specific implementation: /// Returns a platform-specific implementation:
/// - macOS: [MpvPlayerMacOS] using MPVKit with Metal rendering /// - macOS: [PlayerMacOS] using MPVKit with Metal rendering
/// - iOS: [MpvPlayerIOS] using MPVKit with Metal rendering /// - iOS: [PlayerIOS] using MPVKit with Metal rendering
/// - Android: [MpvPlayerAndroid] using libmpv /// - Android: [PlayerAndroid] using libmpv
/// - Windows: [MpvPlayerWindows] using libmpv with native window embedding /// - Windows: [PlayerWindows] using libmpv with native window embedding
/// - Linux: [MpvPlayerLinux] using libmpv with OpenGL rendering via GtkGLArea /// - Linux: [PlayerLinux] using libmpv with OpenGL rendering via GtkGLArea
/// - Other platforms: [MpvPlayerStub] (placeholder) /// - Other platforms: [PlayerStub] (placeholder)
factory MpvPlayer() { factory Player() {
if (Platform.isMacOS) { if (Platform.isMacOS) {
return MpvPlayerMacOS(); return PlayerMacOS();
} }
if (Platform.isIOS) { if (Platform.isIOS) {
return MpvPlayerIOS(); return PlayerIOS();
} }
if (Platform.isAndroid) { if (Platform.isAndroid) {
return MpvPlayerAndroid(); return PlayerAndroid();
} }
if (Platform.isWindows) { if (Platform.isWindows) {
return MpvPlayerWindows(); return PlayerWindows();
} }
if (Platform.isLinux) { if (Platform.isLinux) {
return MpvPlayerLinux(); return PlayerLinux();
} }
return MpvPlayerStub(); return PlayerStub();
} }
} }
+5
View File
@@ -0,0 +1,5 @@
import 'player_native.dart';
/// Android implementation of Player.
/// Inherits all functionality from PlayerNative.
class PlayerAndroid extends PlayerNative {}
+5
View File
@@ -0,0 +1,5 @@
import 'player_native.dart';
/// iOS implementation of Player.
/// Inherits all functionality from PlayerNative.
class PlayerIOS extends PlayerNative {}
@@ -1,14 +1,14 @@
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'mpv_player_native.dart'; import 'player_native.dart';
/// Linux implementation of [MpvPlayer]. /// Linux implementation of [Player].
/// ///
/// Uses libmpv with OpenGL rendering via GtkGLArea. /// Uses libmpv with OpenGL rendering via GtkGLArea.
/// The mpv video is rendered to a GtkGLArea positioned behind /// The mpv video is rendered to a GtkGLArea positioned behind
/// the Flutter view using a GtkOverlay, with transparent regions /// the Flutter view using a GtkOverlay, with transparent regions
/// in the Flutter UI allowing the video to show through. /// in the Flutter UI allowing the video to show through.
class MpvPlayerLinux extends MpvPlayerNative { class PlayerLinux extends PlayerNative {
static const _methodChannel = MethodChannel('com.plezy/mpv_player'); static const _methodChannel = MethodChannel('com.plezy/mpv_player');
@override @override
+5
View File
@@ -0,0 +1,5 @@
import 'player_native.dart';
/// macOS implementation of Player.
/// Inherits all functionality from PlayerNative.
class PlayerMacOS extends PlayerNative {}
@@ -2,32 +2,32 @@ import 'dart:async';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../models/mpv_audio_device.dart'; import '../models/audio_device.dart';
import '../models/mpv_audio_track.dart'; import '../models/audio_track.dart';
import '../models/mpv_log.dart'; import '../models/player_log.dart';
import '../models/mpv_media.dart'; import '../models/media.dart';
import '../models/mpv_subtitle_track.dart'; import '../models/subtitle_track.dart';
import '../models/mpv_track_selection.dart'; import '../models/track_selection.dart';
import '../models/mpv_tracks.dart'; import '../models/tracks.dart';
import 'mpv_player.dart'; import 'player.dart';
import 'mpv_player_state.dart'; import 'player_state.dart';
import 'mpv_player_streams.dart'; import 'player_streams.dart';
/// Shared native implementation of [MpvPlayer] for iOS and macOS. /// Shared native implementation of [Player] for iOS and macOS.
/// Uses MPVKit via platform channels with Metal rendering. /// Uses MPVKit via platform channels with Metal rendering.
class MpvPlayerNative implements MpvPlayer { class PlayerNative implements Player {
static const _methodChannel = MethodChannel('com.plezy/mpv_player'); static const _methodChannel = MethodChannel('com.plezy/mpv_player');
static const _eventChannel = EventChannel('com.plezy/mpv_player/events'); static const _eventChannel = EventChannel('com.plezy/mpv_player/events');
MpvPlayerState _state = const MpvPlayerState(); PlayerState _state = const PlayerState();
@override @override
MpvPlayerState get state => _state; PlayerState get state => _state;
late final MpvPlayerStreams _streams; late final PlayerStreams _streams;
@override @override
MpvPlayerStreams get streams => _streams; PlayerStreams get streams => _streams;
@override @override
int? get textureId => null; // Uses direct Metal layer, not Flutter texture int? get textureId => null; // Uses direct Metal layer, not Flutter texture
@@ -41,21 +41,21 @@ class MpvPlayerNative implements MpvPlayer {
final _bufferController = StreamController<Duration>.broadcast(); final _bufferController = StreamController<Duration>.broadcast();
final _volumeController = StreamController<double>.broadcast(); final _volumeController = StreamController<double>.broadcast();
final _rateController = StreamController<double>.broadcast(); final _rateController = StreamController<double>.broadcast();
final _tracksController = StreamController<MpvTracks>.broadcast(); final _tracksController = StreamController<Tracks>.broadcast();
final _trackController = StreamController<MpvTrackSelection>.broadcast(); final _trackController = StreamController<TrackSelection>.broadcast();
final _logController = StreamController<MpvLog>.broadcast(); final _logController = StreamController<PlayerLog>.broadcast();
final _errorController = StreamController<String>.broadcast(); final _errorController = StreamController<String>.broadcast();
final _audioDeviceController = StreamController<MpvAudioDevice>.broadcast(); final _audioDeviceController = StreamController<AudioDevice>.broadcast();
final _audioDevicesController = final _audioDevicesController =
StreamController<List<MpvAudioDevice>>.broadcast(); StreamController<List<AudioDevice>>.broadcast();
StreamSubscription? _eventSubscription; StreamSubscription? _eventSubscription;
bool _disposed = false; bool _disposed = false;
bool _initialized = false; bool _initialized = false;
bool _isVisible = false; bool _isVisible = false;
MpvPlayerNative() { PlayerNative() {
_streams = MpvPlayerStreams( _streams = PlayerStreams(
playing: _playingController.stream, playing: _playingController.stream,
completed: _completedController.stream, completed: _completedController.stream,
buffering: _bufferingController.stream, buffering: _bufferingController.stream,
@@ -195,9 +195,9 @@ class MpvPlayerNative implements MpvPlayer {
} }
} }
MpvTracks _parseTrackList(List trackList) { Tracks _parseTrackList(List trackList) {
final audioTracks = <MpvAudioTrack>[]; final audioTracks = <AudioTrack>[];
final subtitleTracks = <MpvSubtitleTrack>[]; final subtitleTracks = <SubtitleTrack>[];
for (final track in trackList) { for (final track in trackList) {
if (track is! Map) continue; if (track is! Map) continue;
@@ -206,7 +206,7 @@ class MpvPlayerNative implements MpvPlayer {
final id = track['id']?.toString() ?? ''; final id = track['id']?.toString() ?? '';
if (type == 'audio') { if (type == 'audio') {
audioTracks.add(MpvAudioTrack( audioTracks.add(AudioTrack(
id: id, id: id,
title: track['title'] as String?, title: track['title'] as String?,
language: track['lang'] as String?, language: track['lang'] as String?,
@@ -216,7 +216,7 @@ class MpvPlayerNative implements MpvPlayer {
isDefault: track['default'] as bool? ?? false, isDefault: track['default'] as bool? ?? false,
)); ));
} else if (type == 'sub') { } else if (type == 'sub') {
subtitleTracks.add(MpvSubtitleTrack( subtitleTracks.add(SubtitleTrack(
id: id, id: id,
title: track['title'] as String?, title: track['title'] as String?,
language: track['lang'] as String?, language: track['lang'] as String?,
@@ -227,15 +227,15 @@ class MpvPlayerNative implements MpvPlayer {
} }
} }
return MpvTracks(audio: audioTracks, subtitle: subtitleTracks); return Tracks(audio: audioTracks, subtitle: subtitleTracks);
} }
void _updateSelectedAudioTrack(dynamic trackId) { void _updateSelectedAudioTrack(dynamic trackId) {
final id = trackId?.toString(); final id = trackId?.toString();
MpvAudioTrack? selectedTrack; AudioTrack? selectedTrack;
if (id != null && id != 'no') { if (id != null && id != 'no') {
selectedTrack = _state.tracks.audio.cast<MpvAudioTrack?>().firstWhere( selectedTrack = _state.tracks.audio.cast<AudioTrack?>().firstWhere(
(t) => t?.id == id, (t) => t?.id == id,
orElse: () => null, orElse: () => null,
); );
@@ -249,11 +249,11 @@ class MpvPlayerNative implements MpvPlayer {
void _updateSelectedSubtitleTrack(dynamic trackId) { void _updateSelectedSubtitleTrack(dynamic trackId) {
final id = trackId?.toString(); final id = trackId?.toString();
MpvSubtitleTrack? selectedTrack; SubtitleTrack? selectedTrack;
if (id != null && id != 'no') { if (id != null && id != 'no') {
selectedTrack = selectedTrack =
_state.tracks.subtitle.cast<MpvSubtitleTrack?>().firstWhere( _state.tracks.subtitle.cast<SubtitleTrack?>().firstWhere(
(t) => t?.id == id, (t) => t?.id == id,
orElse: () => null, orElse: () => null,
); );
@@ -272,7 +272,7 @@ class MpvPlayerNative implements MpvPlayer {
final result = await _methodChannel.invokeMethod<bool>('initialize'); final result = await _methodChannel.invokeMethod<bool>('initialize');
_initialized = result == true; _initialized = result == true;
if (!_initialized) { if (!_initialized) {
throw Exception('Failed to initialize MPV player'); throw Exception('Failed to initialize player');
} }
// Subscribe to MPV properties // Subscribe to MPV properties
@@ -300,7 +300,7 @@ class MpvPlayerNative implements MpvPlayer {
void _checkDisposed() { void _checkDisposed() {
if (_disposed) { if (_disposed) {
throw StateError('MpvPlayer has been disposed'); throw StateError('Player has been disposed');
} }
} }
@@ -309,7 +309,7 @@ class MpvPlayerNative implements MpvPlayer {
// ============================================ // ============================================
@override @override
Future<void> open(MpvMedia media, {bool play = true}) async { Future<void> open(Media media, {bool play = true}) async {
_checkDisposed(); _checkDisposed();
await _ensureInitialized(); await _ensureInitialized();
@@ -376,13 +376,13 @@ class MpvPlayerNative implements MpvPlayer {
// ============================================ // ============================================
@override @override
Future<void> selectAudioTrack(MpvAudioTrack track) async { Future<void> selectAudioTrack(AudioTrack track) async {
_checkDisposed(); _checkDisposed();
await setProperty('aid', track.id); await setProperty('aid', track.id);
} }
@override @override
Future<void> selectSubtitleTrack(MpvSubtitleTrack track) async { Future<void> selectSubtitleTrack(SubtitleTrack track) async {
_checkDisposed(); _checkDisposed();
await setProperty('sid', track.id); await setProperty('sid', track.id);
} }
@@ -418,7 +418,7 @@ class MpvPlayerNative implements MpvPlayer {
} }
@override @override
Future<void> setAudioDevice(MpvAudioDevice device) async { Future<void> setAudioDevice(AudioDevice device) async {
_checkDisposed(); _checkDisposed();
await setProperty('audio-device', device.name); await setProperty('audio-device', device.name);
} }
@@ -1,12 +1,12 @@
import '../models/mpv_audio_device.dart'; import '../models/audio_device.dart';
import '../models/mpv_tracks.dart'; import '../models/tracks.dart';
import '../models/mpv_track_selection.dart'; import '../models/track_selection.dart';
/// Immutable snapshot of the current player state. /// Immutable snapshot of the current player state.
/// ///
/// This class provides synchronous access to the player's current state. /// This class provides synchronous access to the player's current state.
/// For reactive updates, use [MpvPlayerStreams]. /// For reactive updates, use [PlayerStreams].
class MpvPlayerState { class PlayerState {
/// Whether playback is currently active. /// Whether playback is currently active.
final bool playing; final bool playing;
@@ -32,10 +32,10 @@ class MpvPlayerState {
final double rate; final double rate;
/// Available tracks in the media. /// Available tracks in the media.
final MpvTracks tracks; final Tracks tracks;
/// Currently selected tracks. /// Currently selected tracks.
final MpvTrackSelection track; final TrackSelection track;
/// Audio delay/sync offset in seconds. /// Audio delay/sync offset in seconds.
final double audioDelay; final double audioDelay;
@@ -47,12 +47,12 @@ class MpvPlayerState {
final bool audioPassthrough; final bool audioPassthrough;
/// Current audio output device. /// Current audio output device.
final MpvAudioDevice audioDevice; final AudioDevice audioDevice;
/// Available audio output devices. /// Available audio output devices.
final List<MpvAudioDevice> audioDevices; final List<AudioDevice> audioDevices;
const MpvPlayerState({ const PlayerState({
this.playing = false, this.playing = false,
this.completed = false, this.completed = false,
this.buffering = false, this.buffering = false,
@@ -61,17 +61,17 @@ class MpvPlayerState {
this.buffer = Duration.zero, this.buffer = Duration.zero,
this.volume = 100.0, this.volume = 100.0,
this.rate = 1.0, this.rate = 1.0,
this.tracks = const MpvTracks(), this.tracks = const Tracks(),
this.track = const MpvTrackSelection(), this.track = const TrackSelection(),
this.audioDelay = 0.0, this.audioDelay = 0.0,
this.subtitleDelay = 0.0, this.subtitleDelay = 0.0,
this.audioPassthrough = false, this.audioPassthrough = false,
this.audioDevice = MpvAudioDevice.auto, this.audioDevice = AudioDevice.auto,
this.audioDevices = const [], this.audioDevices = const [],
}); });
/// Creates a copy with the given fields replaced. /// Creates a copy with the given fields replaced.
MpvPlayerState copyWith({ PlayerState copyWith({
bool? playing, bool? playing,
bool? completed, bool? completed,
bool? buffering, bool? buffering,
@@ -80,15 +80,15 @@ class MpvPlayerState {
Duration? buffer, Duration? buffer,
double? volume, double? volume,
double? rate, double? rate,
MpvTracks? tracks, Tracks? tracks,
MpvTrackSelection? track, TrackSelection? track,
double? audioDelay, double? audioDelay,
double? subtitleDelay, double? subtitleDelay,
bool? audioPassthrough, bool? audioPassthrough,
MpvAudioDevice? audioDevice, AudioDevice? audioDevice,
List<MpvAudioDevice>? audioDevices, List<AudioDevice>? audioDevices,
}) { }) {
return MpvPlayerState( return PlayerState(
playing: playing ?? this.playing, playing: playing ?? this.playing,
completed: completed ?? this.completed, completed: completed ?? this.completed,
buffering: buffering ?? this.buffering, buffering: buffering ?? this.buffering,
@@ -109,5 +109,5 @@ class MpvPlayerState {
@override @override
String toString() => String toString() =>
'MpvPlayerState(playing: $playing, position: $position, duration: $duration)'; 'PlayerState(playing: $playing, position: $position, duration: $duration)';
} }
@@ -1,13 +1,13 @@
import '../models/mpv_audio_device.dart'; import '../models/audio_device.dart';
import '../models/mpv_log.dart'; import '../models/player_log.dart';
import '../models/mpv_tracks.dart'; import '../models/tracks.dart';
import '../models/mpv_track_selection.dart'; import '../models/track_selection.dart';
/// Reactive streams for player state changes. /// Reactive streams for player state changes.
/// ///
/// Subscribe to these streams to receive updates when the player state changes. /// Subscribe to these streams to receive updates when the player state changes.
/// For synchronous state access, use [MpvPlayerState]. /// For synchronous state access, use [PlayerState].
class MpvPlayerStreams { class PlayerStreams {
/// Stream of playing state changes. /// Stream of playing state changes.
final Stream<bool> playing; final Stream<bool> playing;
@@ -33,24 +33,24 @@ class MpvPlayerStreams {
final Stream<double> rate; final Stream<double> rate;
/// Stream of available tracks updates. /// Stream of available tracks updates.
final Stream<MpvTracks> tracks; final Stream<Tracks> tracks;
/// Stream of track selection changes. /// Stream of track selection changes.
final Stream<MpvTrackSelection> track; final Stream<TrackSelection> track;
/// Stream of log messages from MPV. /// Stream of log messages from the player.
final Stream<MpvLog> log; final Stream<PlayerLog> log;
/// Stream of error messages. /// Stream of error messages.
final Stream<String> error; final Stream<String> error;
/// Stream of audio device changes. /// Stream of audio device changes.
final Stream<MpvAudioDevice> audioDevice; final Stream<AudioDevice> audioDevice;
/// Stream of available audio devices. /// Stream of available audio devices.
final Stream<List<MpvAudioDevice>> audioDevices; final Stream<List<AudioDevice>> audioDevices;
const MpvPlayerStreams({ const PlayerStreams({
required this.playing, required this.playing,
required this.completed, required this.completed,
required this.buffering, required this.buffering,
@@ -1,31 +1,31 @@
import 'dart:async'; import 'dart:async';
import '../models/mpv_audio_device.dart'; import '../models/audio_device.dart';
import '../models/mpv_media.dart'; import '../models/media.dart';
import '../models/mpv_audio_track.dart'; import '../models/audio_track.dart';
import '../models/mpv_subtitle_track.dart'; import '../models/subtitle_track.dart';
import '../models/mpv_tracks.dart'; import '../models/tracks.dart';
import '../models/mpv_track_selection.dart'; import '../models/track_selection.dart';
import '../models/mpv_log.dart'; import '../models/player_log.dart';
import 'mpv_player.dart'; import 'player.dart';
import 'mpv_player_state.dart'; import 'player_state.dart';
import 'mpv_player_streams.dart'; import 'player_streams.dart';
/// Stub implementation of [MpvPlayer]. /// Stub implementation of [Player].
/// ///
/// This implementation creates all the necessary stream controllers and /// This implementation creates all the necessary stream controllers and
/// state management, but throws [UnimplementedError] for all playback /// state management, but throws [UnimplementedError] for all playback
/// methods. It serves as a foundation for platform-specific implementations. /// methods. It serves as a foundation for platform-specific implementations.
class MpvPlayerStub implements MpvPlayer { class PlayerStub implements Player {
MpvPlayerState _state = const MpvPlayerState(); PlayerState _state = const PlayerState();
@override @override
MpvPlayerState get state => _state; PlayerState get state => _state;
late final MpvPlayerStreams _streams; late final PlayerStreams _streams;
@override @override
MpvPlayerStreams get streams => _streams; PlayerStreams get streams => _streams;
@override @override
int? get textureId => null; // Set by platform implementation int? get textureId => null; // Set by platform implementation
@@ -39,18 +39,18 @@ class MpvPlayerStub implements MpvPlayer {
final _bufferController = StreamController<Duration>.broadcast(); final _bufferController = StreamController<Duration>.broadcast();
final _volumeController = StreamController<double>.broadcast(); final _volumeController = StreamController<double>.broadcast();
final _rateController = StreamController<double>.broadcast(); final _rateController = StreamController<double>.broadcast();
final _tracksController = StreamController<MpvTracks>.broadcast(); final _tracksController = StreamController<Tracks>.broadcast();
final _trackController = StreamController<MpvTrackSelection>.broadcast(); final _trackController = StreamController<TrackSelection>.broadcast();
final _logController = StreamController<MpvLog>.broadcast(); final _logController = StreamController<PlayerLog>.broadcast();
final _errorController = StreamController<String>.broadcast(); final _errorController = StreamController<String>.broadcast();
final _audioDeviceController = StreamController<MpvAudioDevice>.broadcast(); final _audioDeviceController = StreamController<AudioDevice>.broadcast();
final _audioDevicesController = final _audioDevicesController =
StreamController<List<MpvAudioDevice>>.broadcast(); StreamController<List<AudioDevice>>.broadcast();
bool _disposed = false; bool _disposed = false;
MpvPlayerStub() { PlayerStub() {
_streams = MpvPlayerStreams( _streams = PlayerStreams(
playing: _playingController.stream, playing: _playingController.stream,
completed: _completedController.stream, completed: _completedController.stream,
buffering: _bufferingController.stream, buffering: _bufferingController.stream,
@@ -70,7 +70,7 @@ class MpvPlayerStub implements MpvPlayer {
void _checkDisposed() { void _checkDisposed() {
if (_disposed) { if (_disposed) {
throw StateError('MpvPlayer has been disposed'); throw StateError('Player has been disposed');
} }
} }
@@ -79,21 +79,21 @@ class MpvPlayerStub implements MpvPlayer {
// ============================================ // ============================================
@override @override
Future<void> open(MpvMedia media, {bool play = true}) async { Future<void> open(Media media, {bool play = true}) async {
_checkDisposed(); _checkDisposed();
throw UnimplementedError('MpvPlayer.open() is not yet implemented'); throw UnimplementedError('Player.open() is not yet implemented');
} }
@override @override
Future<void> play() async { Future<void> play() async {
_checkDisposed(); _checkDisposed();
throw UnimplementedError('MpvPlayer.play() is not yet implemented'); throw UnimplementedError('Player.play() is not yet implemented');
} }
@override @override
Future<void> pause() async { Future<void> pause() async {
_checkDisposed(); _checkDisposed();
throw UnimplementedError('MpvPlayer.pause() is not yet implemented'); throw UnimplementedError('Player.pause() is not yet implemented');
} }
@override @override
@@ -109,13 +109,13 @@ class MpvPlayerStub implements MpvPlayer {
@override @override
Future<void> stop() async { Future<void> stop() async {
_checkDisposed(); _checkDisposed();
throw UnimplementedError('MpvPlayer.stop() is not yet implemented'); throw UnimplementedError('Player.stop() is not yet implemented');
} }
@override @override
Future<void> seek(Duration position) async { Future<void> seek(Duration position) async {
_checkDisposed(); _checkDisposed();
throw UnimplementedError('MpvPlayer.seek() is not yet implemented'); throw UnimplementedError('Player.seek() is not yet implemented');
} }
// ============================================ // ============================================
@@ -123,17 +123,17 @@ class MpvPlayerStub implements MpvPlayer {
// ============================================ // ============================================
@override @override
Future<void> selectAudioTrack(MpvAudioTrack track) async { Future<void> selectAudioTrack(AudioTrack track) async {
_checkDisposed(); _checkDisposed();
throw UnimplementedError( throw UnimplementedError(
'MpvPlayer.selectAudioTrack() is not yet implemented'); 'Player.selectAudioTrack() is not yet implemented');
} }
@override @override
Future<void> selectSubtitleTrack(MpvSubtitleTrack track) async { Future<void> selectSubtitleTrack(SubtitleTrack track) async {
_checkDisposed(); _checkDisposed();
throw UnimplementedError( throw UnimplementedError(
'MpvPlayer.selectSubtitleTrack() is not yet implemented'); 'Player.selectSubtitleTrack() is not yet implemented');
} }
@override @override
@@ -145,7 +145,7 @@ class MpvPlayerStub implements MpvPlayer {
}) async { }) async {
_checkDisposed(); _checkDisposed();
throw UnimplementedError( throw UnimplementedError(
'MpvPlayer.addSubtitleTrack() is not yet implemented'); 'Player.addSubtitleTrack() is not yet implemented');
} }
// ============================================ // ============================================
@@ -155,20 +155,20 @@ class MpvPlayerStub implements MpvPlayer {
@override @override
Future<void> setVolume(double volume) async { Future<void> setVolume(double volume) async {
_checkDisposed(); _checkDisposed();
throw UnimplementedError('MpvPlayer.setVolume() is not yet implemented'); throw UnimplementedError('Player.setVolume() is not yet implemented');
} }
@override @override
Future<void> setRate(double rate) async { Future<void> setRate(double rate) async {
_checkDisposed(); _checkDisposed();
throw UnimplementedError('MpvPlayer.setRate() is not yet implemented'); throw UnimplementedError('Player.setRate() is not yet implemented');
} }
@override @override
Future<void> setAudioDevice(MpvAudioDevice device) async { Future<void> setAudioDevice(AudioDevice device) async {
_checkDisposed(); _checkDisposed();
throw UnimplementedError( throw UnimplementedError(
'MpvPlayer.setAudioDevice() is not yet implemented'); 'Player.setAudioDevice() is not yet implemented');
} }
// ============================================ // ============================================
@@ -178,19 +178,19 @@ class MpvPlayerStub implements MpvPlayer {
@override @override
Future<void> setProperty(String name, String value) async { Future<void> setProperty(String name, String value) async {
_checkDisposed(); _checkDisposed();
throw UnimplementedError('MpvPlayer.setProperty() is not yet implemented'); throw UnimplementedError('Player.setProperty() is not yet implemented');
} }
@override @override
Future<String?> getProperty(String name) async { Future<String?> getProperty(String name) async {
_checkDisposed(); _checkDisposed();
throw UnimplementedError('MpvPlayer.getProperty() is not yet implemented'); throw UnimplementedError('Player.getProperty() is not yet implemented');
} }
@override @override
Future<void> command(List<String> args) async { Future<void> command(List<String> args) async {
_checkDisposed(); _checkDisposed();
throw UnimplementedError('MpvPlayer.command() is not yet implemented'); throw UnimplementedError('Player.command() is not yet implemented');
} }
// ============================================ // ============================================
@@ -201,7 +201,7 @@ class MpvPlayerStub implements MpvPlayer {
Future<void> setAudioPassthrough(bool enabled) async { Future<void> setAudioPassthrough(bool enabled) async {
_checkDisposed(); _checkDisposed();
throw UnimplementedError( throw UnimplementedError(
'MpvPlayer.setAudioPassthrough() is not yet implemented'); 'Player.setAudioPassthrough() is not yet implemented');
} }
// ============================================ // ============================================
@@ -1,13 +1,13 @@
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'mpv_player_native.dart'; import 'player_native.dart';
/// Windows implementation of [MpvPlayer]. /// Windows implementation of [Player].
/// ///
/// Uses libmpv via platform channels with native window embedding. /// Uses libmpv via platform channels with native window embedding.
/// The mpv video window is positioned behind the Flutter window, /// The mpv video window is positioned behind the Flutter window,
/// with transparent regions allowing the video to show through. /// with transparent regions allowing the video to show through.
class MpvPlayerWindows extends MpvPlayerNative { class PlayerWindows extends PlayerNative {
static const _methodChannel = MethodChannel('com.plezy/mpv_player'); static const _methodChannel = MethodChannel('com.plezy/mpv_player');
@override @override
@@ -15,7 +15,7 @@ class MpvPlayerWindows extends MpvPlayerNative {
/// Updates the video window position and size. /// Updates the video window position and size.
/// ///
/// This is called by [MpvVideo] when the widget layout changes. /// This is called by [Video] when the widget layout changes.
Future<void> setVideoRect({ Future<void> setVideoRect({
required int left, required int left,
required int top, required int top,
@@ -2,28 +2,28 @@ import 'dart:io' show Platform;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../player/mpv_player.dart'; import '../player/player.dart';
import '../player/mpv_player_linux.dart'; import '../player/player_linux.dart';
import '../player/mpv_player_windows.dart'; import '../player/player_windows.dart';
/// Video widget for displaying MPV player output. /// Video widget for displaying player output.
/// ///
/// This widget displays the video output from an [MpvPlayer] instance /// This widget displays the video output from a [Player] instance
/// and optionally overlays custom controls. /// and optionally overlays custom controls.
/// ///
/// Example usage: /// Example usage:
/// ```dart /// ```dart
/// final player = MpvPlayer(); /// final player = Player();
/// ///
/// MpvVideo( /// Video(
/// player: player, /// player: player,
/// fit: BoxFit.contain, /// fit: BoxFit.contain,
/// controls: (context) => MyCustomControls(), /// controls: (context) => MyCustomControls(),
/// ) /// )
/// ``` /// ```
class MpvVideo extends StatefulWidget { class Video extends StatefulWidget {
/// The player instance. /// The player instance.
final MpvPlayer player; final Player player;
/// How the video should be inscribed into the widget's box. /// How the video should be inscribed into the widget's box.
final BoxFit fit; final BoxFit fit;
@@ -34,7 +34,7 @@ class MpvVideo extends StatefulWidget {
/// Background color shown behind the video. /// Background color shown behind the video.
final Color backgroundColor; final Color backgroundColor;
const MpvVideo({ const Video({
super.key, super.key,
required this.player, required this.player,
this.fit = BoxFit.contain, this.fit = BoxFit.contain,
@@ -43,10 +43,10 @@ class MpvVideo extends StatefulWidget {
}); });
@override @override
State<MpvVideo> createState() => _MpvVideoState(); State<Video> createState() => _VideoState();
} }
class _MpvVideoState extends State<MpvVideo> { class _VideoState extends State<Video> {
Rect? _lastRect; Rect? _lastRect;
@override @override
@@ -123,8 +123,8 @@ class _MpvVideoState extends State<MpvVideo> {
_lastRect = newRect; _lastRect = newRect;
// Update the native mpv window position // Update the native mpv window position
if (widget.player is MpvPlayerWindows) { if (widget.player is PlayerWindows) {
final windowsPlayer = widget.player as MpvPlayerWindows; final windowsPlayer = widget.player as PlayerWindows;
windowsPlayer.setVideoRect( windowsPlayer.setVideoRect(
left: (position.dx * dpr).toInt(), left: (position.dx * dpr).toInt(),
top: (position.dy * dpr).toInt(), top: (position.dy * dpr).toInt(),
@@ -161,9 +161,9 @@ class _MpvVideoState extends State<MpvVideo> {
_lastRect = newRect; _lastRect = newRect;
// Update the Linux mpv player (triggers redraw) // Update the Linux player (triggers redraw)
if (widget.player is MpvPlayerLinux) { if (widget.player is PlayerLinux) {
final linuxPlayer = widget.player as MpvPlayerLinux; final linuxPlayer = widget.player as PlayerLinux;
linuxPlayer.setVideoRect( linuxPlayer.setVideoRect(
left: (position.dx * dpr).toInt(), left: (position.dx * dpr).toInt(),
top: (position.dy * dpr).toInt(), top: (position.dy * dpr).toInt(),
+19 -19
View File
@@ -38,8 +38,8 @@ Future<void> loadIsoTable() async {
class VideoPlayerScreen extends StatefulWidget { class VideoPlayerScreen extends StatefulWidget {
final PlexMetadata metadata; final PlexMetadata metadata;
final MpvAudioTrack? preferredAudioTrack; final AudioTrack? preferredAudioTrack;
final MpvSubtitleTrack? preferredSubtitleTrack; final SubtitleTrack? preferredSubtitleTrack;
final double? preferredPlaybackRate; final double? preferredPlaybackRate;
final int selectedMediaIndex; final int selectedMediaIndex;
@@ -58,7 +58,7 @@ class VideoPlayerScreen extends StatefulWidget {
class VideoPlayerScreenState extends State<VideoPlayerScreen> class VideoPlayerScreenState extends State<VideoPlayerScreen>
with WidgetsBindingObserver { with WidgetsBindingObserver {
MpvPlayer? player; Player? player;
bool _isPlayerInitialized = false; bool _isPlayerInitialized = false;
PlexMetadata? _nextEpisode; PlexMetadata? _nextEpisode;
PlexMetadata? _previousEpisode; PlexMetadata? _previousEpisode;
@@ -67,13 +67,13 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
bool _isPhone = false; bool _isPhone = false;
List<PlexMediaVersion> _availableVersions = []; List<PlexMediaVersion> _availableVersions = [];
PlexMediaInfo? _currentMediaInfo; PlexMediaInfo? _currentMediaInfo;
StreamSubscription<MpvLog>? _logSubscription; StreamSubscription<PlayerLog>? _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<MpvTracks>? _trackLoadingSubscription; StreamSubscription<Tracks>? _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
@@ -244,7 +244,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
final debugLoggingEnabled = settingsService.getEnableDebugLogging(); final debugLoggingEnabled = settingsService.getEnableDebugLogging();
// Create player // Create player
player = MpvPlayer(); player = Player();
// Configure player properties // Configure player properties
await player!.setProperty('target-colorspace-hint', 'yes'); // Enable HDR passthrough await player!.setProperty('target-colorspace-hint', 'yes'); // Enable HDR passthrough
@@ -562,13 +562,13 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
selectedMediaIndex: widget.selectedMediaIndex, selectedMediaIndex: widget.selectedMediaIndex,
); );
// Open video through MpvPlayer // Open video through Player
if (result.videoUrl != null) { if (result.videoUrl != null) {
// Pass resume position if available // Pass resume position if available
final resumePosition = widget.metadata.viewOffset != null final resumePosition = widget.metadata.viewOffset != null
? Duration(milliseconds: widget.metadata.viewOffset!) ? Duration(milliseconds: widget.metadata.viewOffset!)
: null; : null;
await player!.open(MpvMedia(result.videoUrl!, start: resumePosition)); await player!.open(Media(result.videoUrl!, start: resumePosition));
} }
// Update available versions from the playback data // Update available versions from the playback data
@@ -709,22 +709,22 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
} }
} }
void _onPlayerLog(MpvLog log) { void _onPlayerLog(PlayerLog log) {
// Map MPV log levels to app logger levels // Map MPV log levels to app logger levels
switch (log.level) { switch (log.level) {
case MpvLogLevel.fatal: case PlayerLogLevel.fatal:
case MpvLogLevel.error: case PlayerLogLevel.error:
appLogger.e('[MPV:${log.prefix}] ${log.text}'); appLogger.e('[MPV:${log.prefix}] ${log.text}');
break; break;
case MpvLogLevel.warn: case PlayerLogLevel.warn:
appLogger.w('[MPV:${log.prefix}] ${log.text}'); appLogger.w('[MPV:${log.prefix}] ${log.text}');
break; break;
case MpvLogLevel.info: case PlayerLogLevel.info:
appLogger.i('[MPV:${log.prefix}] ${log.text}'); appLogger.i('[MPV:${log.prefix}] ${log.text}');
break; break;
case MpvLogLevel.debug: case PlayerLogLevel.debug:
case MpvLogLevel.trace: case PlayerLogLevel.trace:
case MpvLogLevel.verbose: case PlayerLogLevel.verbose:
appLogger.d('[MPV:${log.prefix}] ${log.text}'); appLogger.d('[MPV:${log.prefix}] ${log.text}');
break; break;
default: default:
@@ -767,7 +767,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
} }
/// Handle audio track changes from the user - save both stream selection and language preference /// Handle audio track changes from the user - save both stream selection and language preference
Future<void> _onAudioTrackChanged(MpvAudioTrack track) async { Future<void> _onAudioTrackChanged(AudioTrack 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
@@ -869,7 +869,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
} }
/// Handle subtitle track changes from the user - save both stream selection and language preference /// Handle subtitle track changes from the user - save both stream selection and language preference
Future<void> _onSubtitleTrackChanged(MpvSubtitleTrack track) async { Future<void> _onSubtitleTrackChanged(SubtitleTrack 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
@@ -1131,7 +1131,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
}); });
} }
return MpvVideo( return Video(
player: player!, player: player!,
fit: _videoFilterManager?.currentBoxFit ?? BoxFit.contain, fit: _videoFilterManager?.currentBoxFit ?? BoxFit.contain,
controls: (context) => plexVideoControlsBuilder( controls: (context) => plexVideoControlsBuilder(
+3 -3
View File
@@ -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 MpvPlayer? player, required Player? 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
MpvAudioTrack? currentAudioTrack; AudioTrack? currentAudioTrack;
MpvSubtitleTrack? currentSubtitleTrack; SubtitleTrack? currentSubtitleTrack;
double? currentPlaybackRate; double? currentPlaybackRate;
if (player != null) { if (player != null) {
+2 -2
View File
@@ -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,
MpvPlayer player, Player 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,
MpvPlayer player, Player player,
VoidCallback? onToggleFullscreen, VoidCallback? onToggleFullscreen,
VoidCallback? onToggleSubtitles, VoidCallback? onToggleSubtitles,
VoidCallback? onNextAudioTrack, VoidCallback? onNextAudioTrack,
+1 -1
View File
@@ -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 MpvPlayer player; final Player player;
/// Timer for periodic progress updates /// Timer for periodic progress updates
Timer? _progressTimer; Timer? _progressTimer;
+43 -43
View File
@@ -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 MpvPlayer player; final Player player;
final PlexUserProfile? profileSettings; final PlexUserProfile? profileSettings;
final PlexMetadata metadata; final PlexMetadata metadata;
@@ -69,11 +69,11 @@ class TrackSelectionService {
return null; return null;
} }
MpvAudioTrack? findBestAudioMatch( AudioTrack? findBestAudioMatch(
List<MpvAudioTrack> availableTracks, List<AudioTrack> availableTracks,
MpvAudioTrack preferred, AudioTrack preferred,
) { ) {
return findBestTrackMatch<MpvAudioTrack>( return findBestTrackMatch<AudioTrack>(
availableTracks, availableTracks,
preferred, preferred,
(t) => t.id, (t) => t.id,
@@ -82,8 +82,8 @@ class TrackSelectionService {
); );
} }
MpvAudioTrack? findAudioTrackByProfile( AudioTrack? findAudioTrackByProfile(
List<MpvAudioTrack> availableTracks, List<AudioTrack> 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<MpvAudioTrack>( final match = _findTrackByLanguageVariations<AudioTrack>(
availableTracks, availableTracks,
preferredLanguage, preferredLanguage,
languageVariations, languageVariations,
@@ -140,16 +140,16 @@ class TrackSelectionService {
return null; return null;
} }
MpvSubtitleTrack? findBestSubtitleMatch( SubtitleTrack? findBestSubtitleMatch(
List<MpvSubtitleTrack> availableTracks, List<SubtitleTrack> availableTracks,
MpvSubtitleTrack preferred, SubtitleTrack preferred,
) { ) {
// Handle special "no subtitles" case // Handle special "no subtitles" case
if (preferred.id == 'no') { if (preferred.id == 'no') {
return MpvSubtitleTrack.off; return SubtitleTrack.off;
} }
return findBestTrackMatch<MpvSubtitleTrack>( return findBestTrackMatch<SubtitleTrack>(
availableTracks, availableTracks,
preferred, preferred,
(t) => t.id, (t) => t.id,
@@ -158,10 +158,10 @@ class TrackSelectionService {
); );
} }
MpvSubtitleTrack? findSubtitleTrackByProfile( SubtitleTrack? findSubtitleTrackByProfile(
List<MpvSubtitleTrack> availableTracks, List<SubtitleTrack> availableTracks,
PlexUserProfile profile, { PlexUserProfile profile, {
MpvAudioTrack? selectedAudioTrack, AudioTrack? 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 MpvSubtitleTrack.off; return SubtitleTrack.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 MpvSubtitleTrack.off; return SubtitleTrack.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<MpvSubtitleTrack>( final match = _findTrackByLanguageVariations<SubtitleTrack>(
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<MpvSubtitleTrack> filterSubtitlesBySDH( List<SubtitleTrack> filterSubtitlesBySDH(
List<MpvSubtitleTrack> tracks, List<SubtitleTrack> 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<MpvSubtitleTrack> filterSubtitlesByForced( List<SubtitleTrack> filterSubtitlesByForced(
List<MpvSubtitleTrack> tracks, List<SubtitleTrack> tracks,
int preference, int preference,
) { ) {
if (preference == 0 || preference == 1) { if (preference == 0 || preference == 1) {
@@ -354,7 +354,7 @@ 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 mpv may not expose this directly, we infer from the title /// Since mpv may not expose this directly, we infer from the title
bool isSDH(MpvSubtitleTrack track) { bool isSDH(SubtitleTrack 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(MpvSubtitleTrack track) { bool isForced(SubtitleTrack 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<MpvAudioTrack> audioTracks, List<AudioTrack> audioTracks,
List<MpvSubtitleTrack> subtitleTracks, List<SubtitleTrack> 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
MpvAudioTrack? selectAudioTrack( AudioTrack? selectAudioTrack(
List<MpvAudioTrack> availableTracks, List<AudioTrack> availableTracks,
MpvAudioTrack? preferredAudioTrack, AudioTrack? preferredAudioTrack,
) { ) {
if (availableTracks.isEmpty) return null; if (availableTracks.isEmpty) return null;
MpvAudioTrack? trackToSelect; AudioTrack? 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
MpvSubtitleTrack selectSubtitleTrack( SubtitleTrack selectSubtitleTrack(
List<MpvSubtitleTrack> availableTracks, List<SubtitleTrack> availableTracks,
MpvSubtitleTrack? preferredSubtitleTrack, SubtitleTrack? preferredSubtitleTrack,
MpvAudioTrack? selectedAudioTrack, AudioTrack? selectedAudioTrack,
) { ) {
MpvSubtitleTrack? subtitleToSelect; SubtitleTrack? 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 MpvSubtitleTrack.off; return SubtitleTrack.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 MpvSubtitleTrack.off; return SubtitleTrack.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 MpvSubtitleTrack.off; return SubtitleTrack.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({
MpvAudioTrack? preferredAudioTrack, AudioTrack? preferredAudioTrack,
MpvSubtitleTrack? preferredSubtitleTrack, SubtitleTrack? preferredSubtitleTrack,
double? preferredPlaybackRate, double? preferredPlaybackRate,
Function(MpvAudioTrack)? onAudioTrackChanged, Function(AudioTrack)? onAudioTrackChanged,
Function(MpvSubtitleTrack)? onSubtitleTrackChanged, Function(SubtitleTrack)? onSubtitleTrackChanged,
}) async { }) async {
// Wait for tracks to be loaded // Wait for tracks to be loaded
int attempts = 0; int attempts = 0;
+1 -1
View File
@@ -14,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 MpvPlayer player; final Player player;
final List<PlexMediaVersion> availableVersions; final List<PlexMediaVersion> availableVersions;
final int selectedMediaIndex; final int selectedMediaIndex;
+1 -1
View File
@@ -2,7 +2,7 @@ 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(MpvPlayer player, Duration offset) { void seekWithClamping(Player 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;
+2 -2
View File
@@ -27,8 +27,8 @@ import '../services/settings_service.dart';
Future<bool?> navigateToVideoPlayer( Future<bool?> navigateToVideoPlayer(
BuildContext context, { BuildContext context, {
required PlexMetadata metadata, required PlexMetadata metadata,
MpvAudioTrack? preferredAudioTrack, AudioTrack? preferredAudioTrack,
MpvSubtitleTrack? preferredSubtitleTrack, SubtitleTrack? preferredSubtitleTrack,
double? preferredPlaybackRate, double? preferredPlaybackRate,
int? selectedMediaIndex, int? selectedMediaIndex,
bool usePushReplacement = false, bool usePushReplacement = false,
@@ -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 MpvPlayer player; final Player player;
final PlexMetadata metadata; final PlexMetadata metadata;
final VoidCallback? onNext; final VoidCallback? onNext;
final VoidCallback? onPrevious; final VoidCallback? onPrevious;
@@ -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 MpvPlayer player; final Player player;
final PlexMetadata metadata; final PlexMetadata metadata;
final List<PlexChapter> chapters; final List<PlexChapter> chapters;
final bool chaptersLoaded; final bool chaptersLoaded;
@@ -8,12 +8,12 @@ import 'track_selection_sheet.dart';
class AudioTrackSheet { class AudioTrackSheet {
static void show( static void show(
BuildContext context, BuildContext context,
MpvPlayer player, { Player player, {
Function(MpvAudioTrack)? onTrackChanged, Function(AudioTrack)? onTrackChanged,
VoidCallback? onOpen, VoidCallback? onOpen,
VoidCallback? onClose, VoidCallback? onClose,
}) { }) {
TrackSelectionSheet.show<MpvAudioTrack>( TrackSelectionSheet.show<AudioTrack>(
context: context, context: context,
player: player, player: player,
title: t.videoControls.audioLabel, title: t.videoControls.audioLabel,
@@ -9,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 MpvPlayer player; final Player 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
@@ -24,7 +24,7 @@ class ChapterSheet extends StatelessWidget {
static void show( static void show(
BuildContext context, BuildContext context,
MpvPlayer player, Player player,
List<PlexChapter> chapters, List<PlexChapter> chapters,
bool chaptersLoaded, { bool chaptersLoaded, {
String? serverId, String? serverId,
@@ -8,12 +8,12 @@ import 'track_selection_sheet.dart';
class SubtitleTrackSheet { class SubtitleTrackSheet {
static void show( static void show(
BuildContext context, BuildContext context,
MpvPlayer player, { Player player, {
Function(MpvSubtitleTrack)? onTrackChanged, Function(SubtitleTrack)? onTrackChanged,
VoidCallback? onOpen, VoidCallback? onOpen,
VoidCallback? onClose, VoidCallback? onClose,
}) { }) {
TrackSelectionSheet.show<MpvSubtitleTrack>( TrackSelectionSheet.show<SubtitleTrack>(
context: context, context: context,
player: player, player: player,
title: t.videoControls.subtitlesLabel, title: t.videoControls.subtitlesLabel,
@@ -47,7 +47,7 @@ class SubtitleTrackSheet {
setTrack: (track) => player.selectSubtitleTrack(track), setTrack: (track) => player.selectSubtitleTrack(track),
onTrackChanged: onTrackChanged, onTrackChanged: onTrackChanged,
showOffOption: true, showOffOption: true,
createOffTrack: () => MpvSubtitleTrack.off, createOffTrack: () => SubtitleTrack.off,
isOffTrack: (track) => track.id == 'no', isOffTrack: (track) => track.id == 'no',
onOpen: onOpen, onOpen: onOpen,
onClose: onClose, onClose: onClose,
@@ -5,13 +5,13 @@ 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 [MpvAudioTrack] or [MpvSubtitleTrack] /// Type parameter [T] should be either [AudioTrack] or [SubtitleTrack]
class TrackSelectionSheet<T> extends StatelessWidget { class TrackSelectionSheet<T> extends StatelessWidget {
final MpvPlayer player; final Player player;
final String title; final String title;
final IconData icon; final IconData icon;
final List<T> Function(MpvTracks?) extractTracks; final List<T> Function(Tracks?) extractTracks;
final T? Function(MpvTrack) getCurrentTrack; final T? Function(TrackSelection) 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;
@@ -36,11 +36,11 @@ class TrackSelectionSheet<T> extends StatelessWidget {
static void show<T>({ static void show<T>({
required BuildContext context, required BuildContext context,
required MpvPlayer player, required Player player,
required String title, required String title,
required IconData icon, required IconData icon,
required List<T> Function(MpvTracks?) extractTracks, required List<T> Function(Tracks?) extractTracks,
required T? Function(MpvTrack) getCurrentTrack, required T? Function(TrackSelection) 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,
@@ -71,9 +71,9 @@ class TrackSelectionSheet<T> extends StatelessWidget {
} }
String _getEmptyMessage() { String _getEmptyMessage() {
if (T == MpvSubtitleTrack) { if (T == SubtitleTrack) {
return 'No subtitles available'; return 'No subtitles available';
} else if (T == MpvAudioTrack) { } else if (T == AudioTrack) {
return 'No audio tracks available'; return 'No audio tracks available';
} }
return 'No tracks available'; return 'No tracks available';
@@ -81,16 +81,16 @@ class TrackSelectionSheet<T> extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StreamBuilder<MpvTracks>( return StreamBuilder<Tracks>(
stream: player.streams.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 MpvAudioTrack) { if (track is AudioTrack) {
return track.id != 'auto' && track.id != 'no'; return track.id != 'auto' && track.id != 'no';
} else if (track is MpvSubtitleTrack) { } else if (track is SubtitleTrack) {
return track.id != 'auto' && track.id != 'no'; return track.id != 'auto' && track.id != 'no';
} }
return true; return true;
@@ -106,7 +106,7 @@ class TrackSelectionSheet<T> extends StatelessWidget {
style: const TextStyle(color: Colors.white70), style: const TextStyle(color: Colors.white70),
), ),
) )
: StreamBuilder<MpvTrack>( : StreamBuilder<TrackSelection>(
stream: player.streams.track, stream: player.streams.track,
initialData: player.state.track, initialData: player.state.track,
builder: (context, selectedSnapshot) { builder: (context, selectedSnapshot) {
@@ -155,9 +155,9 @@ class TrackSelectionSheet<T> extends StatelessWidget {
// Check if this track is selected // Check if this track is selected
String trackId; String trackId;
if (track is MpvAudioTrack) { if (track is AudioTrack) {
trackId = track.id; trackId = track.id;
} else if (track is MpvSubtitleTrack) { } else if (track is SubtitleTrack) {
trackId = track.id; trackId = track.id;
} else { } else {
trackId = ''; trackId = '';
@@ -166,9 +166,9 @@ class TrackSelectionSheet<T> extends StatelessWidget {
String selectedId; String selectedId;
if (selectedTrack == null) { if (selectedTrack == null) {
selectedId = ''; selectedId = '';
} else if (selectedTrack is MpvAudioTrack) { } else if (selectedTrack is AudioTrack) {
selectedId = selectedTrack.id; selectedId = selectedTrack.id;
} else if (selectedTrack is MpvSubtitleTrack) { } else if (selectedTrack is SubtitleTrack) {
selectedId = selectedTrack.id; selectedId = selectedTrack.id;
} else { } else {
selectedId = ''; selectedId = '';
@@ -59,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 MpvPlayer player; final Player player;
final int audioSyncOffset; final int audioSyncOffset;
final int subtitleSyncOffset; final int subtitleSyncOffset;
@@ -72,7 +72,7 @@ class VideoSettingsSheet extends StatefulWidget {
static Future<void> show( static Future<void> show(
BuildContext context, BuildContext context,
MpvPlayer player, Player player,
int audioSyncOffset, int audioSyncOffset,
int subtitleSyncOffset, { int subtitleSyncOffset, {
VoidCallback? onOpen, VoidCallback? onOpen,
@@ -254,7 +254,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
// Audio Output Device (Desktop only) // Audio Output Device (Desktop only)
if (isDesktop) if (isDesktop)
StreamBuilder<MpvAudioDevice>( StreamBuilder<AudioDevice>(
stream: widget.player.streams.audioDevice, stream: widget.player.streams.audioDevice,
initialData: widget.player.state.audioDevice, initialData: widget.player.state.audioDevice,
builder: (context, snapshot) { builder: (context, snapshot) {
@@ -358,13 +358,13 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
} }
Widget _buildAudioDeviceView() { Widget _buildAudioDeviceView() {
return StreamBuilder<List<MpvAudioDevice>>( return StreamBuilder<List<AudioDevice>>(
stream: widget.player.streams.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<MpvAudioDevice>( return StreamBuilder<AudioDevice>(
stream: widget.player.streams.audioDevice, stream: widget.player.streams.audioDevice,
initialData: widget.player.state.audioDevice, initialData: widget.player.state.audioDevice,
builder: (context, selectedSnapshot) { builder: (context, selectedSnapshot) {
@@ -28,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(
MpvPlayer player, Player player,
PlexMetadata metadata, { PlexMetadata metadata, {
VoidCallback? onNext, VoidCallback? onNext,
VoidCallback? onPrevious, VoidCallback? onPrevious,
@@ -36,8 +36,8 @@ Widget plexVideoControlsBuilder(
int? selectedMediaIndex, int? selectedMediaIndex,
int boxFitMode = 0, int boxFitMode = 0,
VoidCallback? onCycleBoxFitMode, VoidCallback? onCycleBoxFitMode,
Function(MpvAudioTrack)? onAudioTrackChanged, Function(AudioTrack)? onAudioTrackChanged,
Function(MpvSubtitleTrack)? onSubtitleTrackChanged, Function(SubtitleTrack)? onSubtitleTrackChanged,
}) { }) {
return PlexVideoControls( return PlexVideoControls(
player: player, player: player,
@@ -54,7 +54,7 @@ Widget plexVideoControlsBuilder(
} }
class PlexVideoControls extends StatefulWidget { class PlexVideoControls extends StatefulWidget {
final MpvPlayer player; final Player player;
final PlexMetadata metadata; final PlexMetadata metadata;
final VoidCallback? onNext; final VoidCallback? onNext;
final VoidCallback? onPrevious; final VoidCallback? onPrevious;
@@ -62,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(MpvAudioTrack)? onAudioTrackChanged; final Function(AudioTrack)? onAudioTrackChanged;
final Function(MpvSubtitleTrack)? onSubtitleTrackChanged; final Function(SubtitleTrack)? onSubtitleTrackChanged;
const PlexVideoControls({ const PlexVideoControls({
super.key, super.key,
@@ -7,7 +7,7 @@ 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 MpvPlayer player; final Player player;
final SleepTimerService sleepTimer; final SleepTimerService sleepTimer;
final int? defaultDuration; final int? defaultDuration;
final VoidCallback? onCancel; final VoidCallback? onCancel;
@@ -7,7 +7,7 @@ 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 MpvPlayer player; final Player player;
final SleepTimerService sleepTimer; final SleepTimerService sleepTimer;
final int? defaultDuration; final int? defaultDuration;
@@ -6,7 +6,7 @@ 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 MpvPlayer player; final Player 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'
@@ -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 MpvPlayer player; final Player 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(MpvAudioTrack)? onAudioTrackChanged; final Function(AudioTrack)? onAudioTrackChanged;
final Function(MpvSubtitleTrack)? onSubtitleTrackChanged; final Function(SubtitleTrack)? onSubtitleTrackChanged;
final VoidCallback? onLoadSeekTimes; final VoidCallback? onLoadSeekTimes;
final VoidCallback? onCancelAutoHide; final VoidCallback? onCancelAutoHide;
final VoidCallback? onStartAutoHide; final VoidCallback? onStartAutoHide;
@@ -64,7 +64,7 @@ class TrackChapterControls extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StreamBuilder<MpvTracks>( return StreamBuilder<Tracks>(
stream: player.streams.tracks, stream: player.streams.tracks,
initialData: player.state.tracks, initialData: player.state.tracks,
builder: (context, snapshot) { builder: (context, snapshot) {
@@ -189,7 +189,7 @@ class TrackChapterControls extends StatelessWidget {
); );
} }
bool _hasMultipleAudioTracks(MpvTracks? tracks) { bool _hasMultipleAudioTracks(Tracks? 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')
@@ -197,7 +197,7 @@ class TrackChapterControls extends StatelessWidget {
return audioTracks.length > 1; return audioTracks.length > 1;
} }
bool _hasSubtitles(MpvTracks? tracks) { bool _hasSubtitles(Tracks? 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')
@@ -6,10 +6,10 @@ 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 [MpvPlayer] to control volume and persists /// This widget integrates with [Player] to control volume and persists
/// the volume setting using [SettingsService]. /// the volume setting using [SettingsService].
class VolumeControl extends StatelessWidget { class VolumeControl extends StatelessWidget {
final MpvPlayer player; final Player player;
const VolumeControl({super.key, required this.player}); const VolumeControl({super.key, required this.player});