This commit is contained in:
edde746
2025-12-02 15:46:19 +01:00
parent d9cdde66d8
commit 1daded6ec6
13 changed files with 1244 additions and 571 deletions
+6 -1
View File
@@ -4,6 +4,7 @@ import '../models/mpv_audio_device.dart';
import '../models/mpv_media.dart';
import '../models/mpv_audio_track.dart';
import '../models/mpv_subtitle_track.dart';
import 'mpv_player_ios.dart';
import 'mpv_player_macos.dart';
import 'mpv_player_state.dart';
import 'mpv_player_streams.dart';
@@ -187,12 +188,16 @@ abstract class MpvPlayer {
///
/// Returns a platform-specific implementation:
/// - macOS: [MpvPlayerMacOS] using MPVKit with Metal rendering
/// - iOS: [MpvPlayerIOS] using MPVKit with Metal rendering
/// - Other platforms: [MpvPlayerStub] (placeholder)
factory MpvPlayer() {
if (Platform.isMacOS) {
return MpvPlayerMacOS();
}
// Future: Add iOS, Android, Windows, Linux implementations
if (Platform.isIOS) {
return MpvPlayerIOS();
}
// Future: Add Android, Windows, Linux implementations
return MpvPlayerStub();
}
}
+5
View File
@@ -0,0 +1,5 @@
import 'mpv_player_native.dart';
/// iOS implementation of MpvPlayer.
/// Inherits all functionality from MpvPlayerNative.
class MpvPlayerIOS extends MpvPlayerNative {}
+4 -505
View File
@@ -1,506 +1,5 @@
import 'dart:async';
import 'mpv_player_native.dart';
import 'package:flutter/services.dart';
import '../models/mpv_audio_device.dart';
import '../models/mpv_audio_track.dart';
import '../models/mpv_log.dart';
import '../models/mpv_log_level.dart';
import '../models/mpv_media.dart';
import '../models/mpv_subtitle_track.dart';
import '../models/mpv_track_selection.dart';
import '../models/mpv_tracks.dart';
import 'mpv_player.dart';
import 'mpv_player_state.dart';
import 'mpv_player_streams.dart';
/// macOS implementation of [MpvPlayer] using MPVKit via platform channels.
class MpvPlayerMacOS implements MpvPlayer {
static const _methodChannel = MethodChannel('com.plezy/mpv_player');
static const _eventChannel = EventChannel('com.plezy/mpv_player/events');
MpvPlayerState _state = const MpvPlayerState();
@override
MpvPlayerState get state => _state;
late final MpvPlayerStreams _streams;
@override
MpvPlayerStreams get streams => _streams;
@override
int? get textureId => null; // macOS uses direct Metal layer, not Flutter texture
// Stream controllers
final _playingController = StreamController<bool>.broadcast();
final _completedController = StreamController<bool>.broadcast();
final _bufferingController = StreamController<bool>.broadcast();
final _positionController = StreamController<Duration>.broadcast();
final _durationController = StreamController<Duration>.broadcast();
final _bufferController = StreamController<Duration>.broadcast();
final _volumeController = StreamController<double>.broadcast();
final _rateController = StreamController<double>.broadcast();
final _tracksController = StreamController<MpvTracks>.broadcast();
final _trackController = StreamController<MpvTrackSelection>.broadcast();
final _logController = StreamController<MpvLog>.broadcast();
final _errorController = StreamController<String>.broadcast();
final _audioDeviceController = StreamController<MpvAudioDevice>.broadcast();
final _audioDevicesController =
StreamController<List<MpvAudioDevice>>.broadcast();
StreamSubscription? _eventSubscription;
bool _disposed = false;
bool _initialized = false;
bool _isVisible = false;
MpvPlayerMacOS() {
_streams = MpvPlayerStreams(
playing: _playingController.stream,
completed: _completedController.stream,
buffering: _bufferingController.stream,
position: _positionController.stream,
duration: _durationController.stream,
buffer: _bufferController.stream,
volume: _volumeController.stream,
rate: _rateController.stream,
tracks: _tracksController.stream,
track: _trackController.stream,
log: _logController.stream,
error: _errorController.stream,
audioDevice: _audioDeviceController.stream,
audioDevices: _audioDevicesController.stream,
);
_setupEventListener();
}
void _setupEventListener() {
_eventSubscription = _eventChannel.receiveBroadcastStream().listen(
_handleEvent,
onError: (error) {
_errorController.add(error.toString());
},
);
}
void _handleEvent(dynamic event) {
if (event is! Map) return;
final type = event['type'] as String?;
final name = event['name'] as String?;
if (type == 'property' && name != null) {
_handlePropertyChange(name, event['value']);
} else if (type == 'event' && name != null) {
_handleMpvEvent(name, event['data'] as Map?);
}
}
void _handlePropertyChange(String name, dynamic value) {
switch (name) {
case 'pause':
final playing = value == false;
_state = _state.copyWith(playing: playing);
_playingController.add(playing);
break;
case 'eof-reached':
final completed = value == true;
_state = _state.copyWith(completed: completed);
_completedController.add(completed);
break;
case 'paused-for-cache':
final buffering = value == true;
_state = _state.copyWith(buffering: buffering);
_bufferingController.add(buffering);
break;
case 'time-pos':
if (value is num) {
final position = Duration(milliseconds: (value * 1000).toInt());
_state = _state.copyWith(position: position);
_positionController.add(position);
}
break;
case 'duration':
if (value is num) {
final duration = Duration(milliseconds: (value * 1000).toInt());
_state = _state.copyWith(duration: duration);
_durationController.add(duration);
}
break;
case 'demuxer-cache-time':
if (value is num) {
final buffer = Duration(milliseconds: (value * 1000).toInt());
_state = _state.copyWith(buffer: buffer);
_bufferController.add(buffer);
}
break;
case 'volume':
if (value is num) {
final volume = value.toDouble();
_state = _state.copyWith(volume: volume);
_volumeController.add(volume);
}
break;
case 'speed':
if (value is num) {
final rate = value.toDouble();
_state = _state.copyWith(rate: rate);
_rateController.add(rate);
}
break;
case 'track-list':
if (value is List) {
final tracks = _parseTrackList(value);
_state = _state.copyWith(tracks: tracks);
_tracksController.add(tracks);
}
break;
case 'aid':
_updateSelectedAudioTrack(value);
break;
case 'sid':
_updateSelectedSubtitleTrack(value);
break;
}
}
void _handleMpvEvent(String name, Map? data) {
switch (name) {
case 'end-file':
final reason = data?['reason'] as String?;
if (reason == 'eof') {
_state = _state.copyWith(completed: true);
_completedController.add(true);
} else if (reason == 'error') {
_errorController.add('Playback error');
}
break;
case 'file-loaded':
// Reset completed state when new file is loaded
_state = _state.copyWith(completed: false);
_completedController.add(false);
break;
}
}
MpvTracks _parseTrackList(List trackList) {
final audioTracks = <MpvAudioTrack>[];
final subtitleTracks = <MpvSubtitleTrack>[];
for (final track in trackList) {
if (track is! Map) continue;
final type = track['type'] as String?;
final id = track['id']?.toString() ?? '';
if (type == 'audio') {
audioTracks.add(MpvAudioTrack(
id: id,
title: track['title'] as String?,
language: track['lang'] as String?,
codec: track['codec'] as String?,
channels: (track['demux-channel-count'] as num?)?.toInt(),
sampleRate: (track['demux-samplerate'] as num?)?.toInt(),
isDefault: track['default'] as bool? ?? false,
));
} else if (type == 'sub') {
subtitleTracks.add(MpvSubtitleTrack(
id: id,
title: track['title'] as String?,
language: track['lang'] as String?,
codec: track['codec'] as String?,
isExternal: track['external'] as bool? ?? false,
uri: track['external-filename'] as String?,
));
}
}
return MpvTracks(audio: audioTracks, subtitle: subtitleTracks);
}
void _updateSelectedAudioTrack(dynamic trackId) {
final id = trackId?.toString();
MpvAudioTrack? selectedTrack;
if (id != null && id != 'no') {
selectedTrack = _state.tracks.audio.cast<MpvAudioTrack?>().firstWhere(
(t) => t?.id == id,
orElse: () => null,
);
}
_state = _state.copyWith(
track: _state.track.copyWith(audio: selectedTrack),
);
_trackController.add(_state.track);
}
void _updateSelectedSubtitleTrack(dynamic trackId) {
final id = trackId?.toString();
MpvSubtitleTrack? selectedTrack;
if (id != null && id != 'no') {
selectedTrack =
_state.tracks.subtitle.cast<MpvSubtitleTrack?>().firstWhere(
(t) => t?.id == id,
orElse: () => null,
);
}
_state = _state.copyWith(
track: _state.track.copyWith(subtitle: selectedTrack),
);
_trackController.add(_state.track);
}
Future<void> _ensureInitialized() async {
if (_initialized) return;
try {
final result = await _methodChannel.invokeMethod<bool>('initialize');
_initialized = result == true;
if (!_initialized) {
throw Exception('Failed to initialize MPV player');
}
// Subscribe to MPV properties
await _observeProperty('time-pos', 'double');
await _observeProperty('duration', 'double');
await _observeProperty('pause', 'flag');
await _observeProperty('paused-for-cache', 'flag');
await _observeProperty('track-list', 'node');
await _observeProperty('eof-reached', 'flag');
await _observeProperty('volume', 'double');
await _observeProperty('aid', 'string');
await _observeProperty('sid', 'string');
} catch (e) {
_errorController.add('Initialization failed: $e');
rethrow;
}
}
Future<void> _observeProperty(String name, String format) async {
await _methodChannel.invokeMethod('observeProperty', {
'name': name,
'format': format,
});
}
void _checkDisposed() {
if (_disposed) {
throw StateError('MpvPlayer has been disposed');
}
}
// ============================================
// Playback Control
// ============================================
@override
Future<void> open(MpvMedia media, {bool play = true}) async {
_checkDisposed();
await _ensureInitialized();
// Show the video layer
await _methodChannel.invokeMethod('setVisible', {'visible': true});
_isVisible = true;
await command(['loadfile', media.uri, 'replace']);
if (!play) {
await setProperty('pause', 'yes');
}
}
@override
Future<void> play() async {
_checkDisposed();
await setProperty('pause', 'no');
}
@override
Future<void> pause() async {
_checkDisposed();
await setProperty('pause', 'yes');
}
@override
Future<void> playOrPause() async {
_checkDisposed();
if (_state.playing) {
await pause();
} else {
await play();
}
}
@override
Future<void> stop() async {
_checkDisposed();
await command(['stop']);
await _methodChannel.invokeMethod('setVisible', {'visible': false});
_isVisible = false;
}
@override
Future<void> seek(Duration position) async {
_checkDisposed();
await command([
'seek',
(position.inMilliseconds / 1000.0).toString(),
'absolute',
]);
}
// ============================================
// Track Selection
// ============================================
@override
Future<void> selectAudioTrack(MpvAudioTrack track) async {
_checkDisposed();
await setProperty('aid', track.id);
}
@override
Future<void> selectSubtitleTrack(MpvSubtitleTrack track) async {
_checkDisposed();
await setProperty('sid', track.id);
}
@override
Future<void> addSubtitleTrack({
required String uri,
String? title,
String? language,
bool select = false,
}) async {
_checkDisposed();
final args = ['sub-add', uri, select ? 'select' : 'auto'];
if (title != null) args.add('title=$title');
if (language != null) args.add('lang=$language');
await command(args);
}
// ============================================
// Volume and Rate
// ============================================
@override
Future<void> setVolume(double volume) async {
_checkDisposed();
await setProperty('volume', volume.toString());
}
@override
Future<void> setRate(double rate) async {
_checkDisposed();
await setProperty('speed', rate.toString());
}
@override
Future<void> setAudioDevice(MpvAudioDevice device) async {
_checkDisposed();
await setProperty('audio-device', device.name);
}
// ============================================
// MPV Properties
// ============================================
@override
Future<void> setProperty(String name, String value) async {
_checkDisposed();
await _methodChannel.invokeMethod('setProperty', {
'name': name,
'value': value,
});
}
@override
Future<String?> getProperty(String name) async {
_checkDisposed();
return await _methodChannel.invokeMethod<String>('getProperty', {
'name': name,
});
}
@override
Future<void> command(List<String> args) async {
_checkDisposed();
await _methodChannel.invokeMethod('command', {'args': args});
}
// ============================================
// Passthrough
// ============================================
@override
Future<void> setAudioPassthrough(bool enabled) async {
_checkDisposed();
if (enabled) {
await setProperty('audio-spdif', 'ac3,eac3,dts,dts-hd,truehd');
await setProperty('audio-exclusive', 'yes');
} else {
await setProperty('audio-spdif', '');
await setProperty('audio-exclusive', 'no');
}
}
// ============================================
// Visibility
// ============================================
@override
Future<bool> setVisible(bool visible) async {
_checkDisposed();
try {
await _methodChannel.invokeMethod('setVisible', {'visible': visible});
_isVisible = visible;
return true;
} catch (e) {
_errorController.add('Failed to set visibility: $e');
return false;
}
}
// ============================================
// Lifecycle
// ============================================
@override
Future<void> dispose() async {
if (_disposed) return;
_disposed = true;
await _eventSubscription?.cancel();
await _methodChannel.invokeMethod('dispose');
await _playingController.close();
await _completedController.close();
await _bufferingController.close();
await _positionController.close();
await _durationController.close();
await _bufferController.close();
await _volumeController.close();
await _rateController.close();
await _tracksController.close();
await _trackController.close();
await _logController.close();
await _errorController.close();
await _audioDeviceController.close();
await _audioDevicesController.close();
}
}
/// macOS implementation of MpvPlayer.
/// Inherits all functionality from MpvPlayerNative.
class MpvPlayerMacOS extends MpvPlayerNative {}
+506
View File
@@ -0,0 +1,506 @@
import 'dart:async';
import 'package:flutter/services.dart';
import '../models/mpv_audio_device.dart';
import '../models/mpv_audio_track.dart';
import '../models/mpv_log.dart';
import '../models/mpv_media.dart';
import '../models/mpv_subtitle_track.dart';
import '../models/mpv_track_selection.dart';
import '../models/mpv_tracks.dart';
import 'mpv_player.dart';
import 'mpv_player_state.dart';
import 'mpv_player_streams.dart';
/// Shared native implementation of [MpvPlayer] for iOS and macOS.
/// Uses MPVKit via platform channels with Metal rendering.
class MpvPlayerNative implements MpvPlayer {
static const _methodChannel = MethodChannel('com.plezy/mpv_player');
static const _eventChannel = EventChannel('com.plezy/mpv_player/events');
MpvPlayerState _state = const MpvPlayerState();
@override
MpvPlayerState get state => _state;
late final MpvPlayerStreams _streams;
@override
MpvPlayerStreams get streams => _streams;
@override
int? get textureId => null; // Uses direct Metal layer, not Flutter texture
// Stream controllers
final _playingController = StreamController<bool>.broadcast();
final _completedController = StreamController<bool>.broadcast();
final _bufferingController = StreamController<bool>.broadcast();
final _positionController = StreamController<Duration>.broadcast();
final _durationController = StreamController<Duration>.broadcast();
final _bufferController = StreamController<Duration>.broadcast();
final _volumeController = StreamController<double>.broadcast();
final _rateController = StreamController<double>.broadcast();
final _tracksController = StreamController<MpvTracks>.broadcast();
final _trackController = StreamController<MpvTrackSelection>.broadcast();
final _logController = StreamController<MpvLog>.broadcast();
final _errorController = StreamController<String>.broadcast();
final _audioDeviceController = StreamController<MpvAudioDevice>.broadcast();
final _audioDevicesController =
StreamController<List<MpvAudioDevice>>.broadcast();
StreamSubscription? _eventSubscription;
bool _disposed = false;
bool _initialized = false;
bool _isVisible = false;
MpvPlayerNative() {
_streams = MpvPlayerStreams(
playing: _playingController.stream,
completed: _completedController.stream,
buffering: _bufferingController.stream,
position: _positionController.stream,
duration: _durationController.stream,
buffer: _bufferController.stream,
volume: _volumeController.stream,
rate: _rateController.stream,
tracks: _tracksController.stream,
track: _trackController.stream,
log: _logController.stream,
error: _errorController.stream,
audioDevice: _audioDeviceController.stream,
audioDevices: _audioDevicesController.stream,
);
_setupEventListener();
}
void _setupEventListener() {
_eventSubscription = _eventChannel.receiveBroadcastStream().listen(
_handleEvent,
onError: (error) {
_errorController.add(error.toString());
},
);
}
void _handleEvent(dynamic event) {
if (event is! Map) return;
final type = event['type'] as String?;
final name = event['name'] as String?;
if (type == 'property' && name != null) {
_handlePropertyChange(name, event['value']);
} else if (type == 'event' && name != null) {
_handleMpvEvent(name, event['data'] as Map?);
}
}
void _handlePropertyChange(String name, dynamic value) {
switch (name) {
case 'pause':
final playing = value == false;
_state = _state.copyWith(playing: playing);
_playingController.add(playing);
break;
case 'eof-reached':
final completed = value == true;
_state = _state.copyWith(completed: completed);
_completedController.add(completed);
break;
case 'paused-for-cache':
final buffering = value == true;
_state = _state.copyWith(buffering: buffering);
_bufferingController.add(buffering);
break;
case 'time-pos':
if (value is num) {
final position = Duration(milliseconds: (value * 1000).toInt());
_state = _state.copyWith(position: position);
_positionController.add(position);
}
break;
case 'duration':
if (value is num) {
final duration = Duration(milliseconds: (value * 1000).toInt());
_state = _state.copyWith(duration: duration);
_durationController.add(duration);
}
break;
case 'demuxer-cache-time':
if (value is num) {
final buffer = Duration(milliseconds: (value * 1000).toInt());
_state = _state.copyWith(buffer: buffer);
_bufferController.add(buffer);
}
break;
case 'volume':
if (value is num) {
final volume = value.toDouble();
_state = _state.copyWith(volume: volume);
_volumeController.add(volume);
}
break;
case 'speed':
if (value is num) {
final rate = value.toDouble();
_state = _state.copyWith(rate: rate);
_rateController.add(rate);
}
break;
case 'track-list':
if (value is List) {
final tracks = _parseTrackList(value);
_state = _state.copyWith(tracks: tracks);
_tracksController.add(tracks);
}
break;
case 'aid':
_updateSelectedAudioTrack(value);
break;
case 'sid':
_updateSelectedSubtitleTrack(value);
break;
}
}
void _handleMpvEvent(String name, Map? data) {
switch (name) {
case 'end-file':
final reason = data?['reason'] as String?;
if (reason == 'eof') {
_state = _state.copyWith(completed: true);
_completedController.add(true);
} else if (reason == 'error') {
_errorController.add('Playback error');
}
break;
case 'file-loaded':
// Reset completed state when new file is loaded
_state = _state.copyWith(completed: false);
_completedController.add(false);
break;
}
}
MpvTracks _parseTrackList(List trackList) {
final audioTracks = <MpvAudioTrack>[];
final subtitleTracks = <MpvSubtitleTrack>[];
for (final track in trackList) {
if (track is! Map) continue;
final type = track['type'] as String?;
final id = track['id']?.toString() ?? '';
if (type == 'audio') {
audioTracks.add(MpvAudioTrack(
id: id,
title: track['title'] as String?,
language: track['lang'] as String?,
codec: track['codec'] as String?,
channels: (track['demux-channel-count'] as num?)?.toInt(),
sampleRate: (track['demux-samplerate'] as num?)?.toInt(),
isDefault: track['default'] as bool? ?? false,
));
} else if (type == 'sub') {
subtitleTracks.add(MpvSubtitleTrack(
id: id,
title: track['title'] as String?,
language: track['lang'] as String?,
codec: track['codec'] as String?,
isExternal: track['external'] as bool? ?? false,
uri: track['external-filename'] as String?,
));
}
}
return MpvTracks(audio: audioTracks, subtitle: subtitleTracks);
}
void _updateSelectedAudioTrack(dynamic trackId) {
final id = trackId?.toString();
MpvAudioTrack? selectedTrack;
if (id != null && id != 'no') {
selectedTrack = _state.tracks.audio.cast<MpvAudioTrack?>().firstWhere(
(t) => t?.id == id,
orElse: () => null,
);
}
_state = _state.copyWith(
track: _state.track.copyWith(audio: selectedTrack),
);
_trackController.add(_state.track);
}
void _updateSelectedSubtitleTrack(dynamic trackId) {
final id = trackId?.toString();
MpvSubtitleTrack? selectedTrack;
if (id != null && id != 'no') {
selectedTrack =
_state.tracks.subtitle.cast<MpvSubtitleTrack?>().firstWhere(
(t) => t?.id == id,
orElse: () => null,
);
}
_state = _state.copyWith(
track: _state.track.copyWith(subtitle: selectedTrack),
);
_trackController.add(_state.track);
}
Future<void> _ensureInitialized() async {
if (_initialized) return;
try {
final result = await _methodChannel.invokeMethod<bool>('initialize');
_initialized = result == true;
if (!_initialized) {
throw Exception('Failed to initialize MPV player');
}
// Subscribe to MPV properties
await _observeProperty('time-pos', 'double');
await _observeProperty('duration', 'double');
await _observeProperty('pause', 'flag');
await _observeProperty('paused-for-cache', 'flag');
await _observeProperty('track-list', 'node');
await _observeProperty('eof-reached', 'flag');
await _observeProperty('volume', 'double');
await _observeProperty('aid', 'string');
await _observeProperty('sid', 'string');
} catch (e) {
_errorController.add('Initialization failed: $e');
rethrow;
}
}
Future<void> _observeProperty(String name, String format) async {
await _methodChannel.invokeMethod('observeProperty', {
'name': name,
'format': format,
});
}
void _checkDisposed() {
if (_disposed) {
throw StateError('MpvPlayer has been disposed');
}
}
// ============================================
// Playback Control
// ============================================
@override
Future<void> open(MpvMedia media, {bool play = true}) async {
_checkDisposed();
await _ensureInitialized();
// Show the video layer
await _methodChannel.invokeMethod('setVisible', {'visible': true});
_isVisible = true;
await command(['loadfile', media.uri, 'replace']);
if (!play) {
await setProperty('pause', 'yes');
}
}
@override
Future<void> play() async {
_checkDisposed();
await setProperty('pause', 'no');
}
@override
Future<void> pause() async {
_checkDisposed();
await setProperty('pause', 'yes');
}
@override
Future<void> playOrPause() async {
_checkDisposed();
if (_state.playing) {
await pause();
} else {
await play();
}
}
@override
Future<void> stop() async {
_checkDisposed();
await command(['stop']);
await _methodChannel.invokeMethod('setVisible', {'visible': false});
_isVisible = false;
}
@override
Future<void> seek(Duration position) async {
_checkDisposed();
await command([
'seek',
(position.inMilliseconds / 1000.0).toString(),
'absolute',
]);
}
// ============================================
// Track Selection
// ============================================
@override
Future<void> selectAudioTrack(MpvAudioTrack track) async {
_checkDisposed();
await setProperty('aid', track.id);
}
@override
Future<void> selectSubtitleTrack(MpvSubtitleTrack track) async {
_checkDisposed();
await setProperty('sid', track.id);
}
@override
Future<void> addSubtitleTrack({
required String uri,
String? title,
String? language,
bool select = false,
}) async {
_checkDisposed();
final args = ['sub-add', uri, select ? 'select' : 'auto'];
if (title != null) args.add('title=$title');
if (language != null) args.add('lang=$language');
await command(args);
}
// ============================================
// Volume and Rate
// ============================================
@override
Future<void> setVolume(double volume) async {
_checkDisposed();
await setProperty('volume', volume.toString());
}
@override
Future<void> setRate(double rate) async {
_checkDisposed();
await setProperty('speed', rate.toString());
}
@override
Future<void> setAudioDevice(MpvAudioDevice device) async {
_checkDisposed();
await setProperty('audio-device', device.name);
}
// ============================================
// MPV Properties
// ============================================
@override
Future<void> setProperty(String name, String value) async {
_checkDisposed();
await _methodChannel.invokeMethod('setProperty', {
'name': name,
'value': value,
});
}
@override
Future<String?> getProperty(String name) async {
_checkDisposed();
return await _methodChannel.invokeMethod<String>('getProperty', {
'name': name,
});
}
@override
Future<void> command(List<String> args) async {
_checkDisposed();
await _methodChannel.invokeMethod('command', {'args': args});
}
// ============================================
// Passthrough
// ============================================
@override
Future<void> setAudioPassthrough(bool enabled) async {
_checkDisposed();
if (enabled) {
await setProperty('audio-spdif', 'ac3,eac3,dts,dts-hd,truehd');
await setProperty('audio-exclusive', 'yes');
} else {
await setProperty('audio-spdif', '');
await setProperty('audio-exclusive', 'no');
}
}
// ============================================
// Visibility
// ============================================
@override
Future<bool> setVisible(bool visible) async {
_checkDisposed();
try {
await _methodChannel.invokeMethod('setVisible', {'visible': visible});
_isVisible = visible;
return true;
} catch (e) {
_errorController.add('Failed to set visibility: $e');
return false;
}
}
// ============================================
// Lifecycle
// ============================================
@override
Future<void> dispose() async {
if (_disposed) return;
_disposed = true;
await _eventSubscription?.cancel();
await _methodChannel.invokeMethod('dispose');
await _playingController.close();
await _completedController.close();
await _bufferingController.close();
await _positionController.close();
await _durationController.close();
await _bufferController.close();
await _volumeController.close();
await _rateController.close();
await _tracksController.close();
await _trackController.close();
await _logController.close();
await _errorController.close();
await _audioDeviceController.close();
await _audioDevicesController.close();
}
}
+2 -40
View File
@@ -47,11 +47,8 @@ class MpvVideo extends StatefulWidget {
class _MpvVideoState extends State<MpvVideo> {
@override
Widget build(BuildContext context) {
// macOS uses transparent background so native Metal layer shows through
final bgColor = Platform.isMacOS ? Colors.transparent : widget.backgroundColor;
return Container(
color: bgColor,
color: Colors.transparent,
child: Stack(
fit: StackFit.expand,
children: [
@@ -66,41 +63,6 @@ class _MpvVideoState extends State<MpvVideo> {
}
Widget _buildVideoSurface() {
// macOS: Video renders behind Flutter via native Metal layer
// Just show transparent container so native layer shows through
if (Platform.isMacOS) {
return const SizedBox.expand();
}
// When textureId is available, use Flutter's Texture widget
final textureId = widget.player.textureId;
if (textureId != null) {
return FittedBox(
fit: widget.fit,
child: Texture(textureId: textureId),
);
}
// Placeholder when video is not ready
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.play_circle_outline,
size: 64,
color: Colors.white.withValues(alpha: 0.3),
),
const SizedBox(height: 16),
Text(
'MPV Video',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.3),
fontSize: 14,
),
),
],
),
);
return const SizedBox.expand();
}
}