refactor: fix naming
This commit is contained in:
+18
-18
@@ -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;
|
||||||
|
|
||||||
@@ -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 {}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import 'mpv_player_native.dart';
|
|
||||||
|
|
||||||
/// iOS implementation of MpvPlayer.
|
|
||||||
/// Inherits all functionality from MpvPlayerNative.
|
|
||||||
class MpvPlayerIOS extends MpvPlayerNative {}
|
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import 'player_native.dart';
|
||||||
|
|
||||||
|
/// Android implementation of Player.
|
||||||
|
/// Inherits all functionality from PlayerNative.
|
||||||
|
class PlayerAndroid extends PlayerNative {}
|
||||||
@@ -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
|
||||||
@@ -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)';
|
||||||
}
|
}
|
||||||
+13
-13
@@ -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(),
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user