linux
This commit is contained in:
@@ -6,6 +6,7 @@ import '../models/mpv_audio_track.dart';
|
||||
import '../models/mpv_subtitle_track.dart';
|
||||
import 'mpv_player_android.dart';
|
||||
import 'mpv_player_ios.dart';
|
||||
import 'mpv_player_linux.dart';
|
||||
import 'mpv_player_macos.dart';
|
||||
import 'mpv_player_state.dart';
|
||||
import 'mpv_player_streams.dart';
|
||||
@@ -173,6 +174,13 @@ abstract class MpvPlayer {
|
||||
/// Returns true if the operation was successful.
|
||||
Future<bool> setVisible(bool visible);
|
||||
|
||||
/// Notify the player about controls visibility.
|
||||
///
|
||||
/// On Linux, due to Flutter's lack of transparency support in GtkOverlay,
|
||||
/// the video layer is hidden when controls are visible and shown when
|
||||
/// controls are hidden. On other platforms, this is a no-op.
|
||||
Future<void> setControlsVisible(bool visible);
|
||||
|
||||
// ============================================
|
||||
// Lifecycle
|
||||
// ============================================
|
||||
@@ -193,6 +201,7 @@ abstract class MpvPlayer {
|
||||
/// - iOS: [MpvPlayerIOS] using MPVKit with Metal rendering
|
||||
/// - Android: [MpvPlayerAndroid] using libmpv
|
||||
/// - Windows: [MpvPlayerWindows] using libmpv with native window embedding
|
||||
/// - Linux: [MpvPlayerLinux] using libmpv with OpenGL rendering via GtkGLArea
|
||||
/// - Other platforms: [MpvPlayerStub] (placeholder)
|
||||
factory MpvPlayer() {
|
||||
if (Platform.isMacOS) {
|
||||
@@ -207,7 +216,9 @@ abstract class MpvPlayer {
|
||||
if (Platform.isWindows) {
|
||||
return MpvPlayerWindows();
|
||||
}
|
||||
// Future: Add Linux implementation
|
||||
if (Platform.isLinux) {
|
||||
return MpvPlayerLinux();
|
||||
}
|
||||
return MpvPlayerStub();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'mpv_player_native.dart';
|
||||
|
||||
/// Linux implementation of [MpvPlayer].
|
||||
///
|
||||
/// Uses libmpv with OpenGL rendering via GtkGLArea.
|
||||
/// The mpv video is rendered to a GtkGLArea positioned behind
|
||||
/// the Flutter view using a GtkOverlay, with transparent regions
|
||||
/// in the Flutter UI allowing the video to show through.
|
||||
class MpvPlayerLinux extends MpvPlayerNative {
|
||||
static const _methodChannel = MethodChannel('com.plezy/mpv_player');
|
||||
|
||||
@override
|
||||
int? get textureId => null; // Uses GtkGLArea, not Flutter texture
|
||||
|
||||
/// Updates the video rendering area.
|
||||
///
|
||||
/// On Linux, the GtkGLArea fills the entire overlay area,
|
||||
/// and mpv handles its own aspect ratio. This method triggers
|
||||
/// a redraw if needed.
|
||||
Future<void> setVideoRect({
|
||||
required int left,
|
||||
required int top,
|
||||
required int right,
|
||||
required int bottom,
|
||||
required double devicePixelRatio,
|
||||
}) async {
|
||||
await _methodChannel.invokeMethod('setVideoRect', {
|
||||
'left': left,
|
||||
'top': top,
|
||||
'right': right,
|
||||
'bottom': bottom,
|
||||
'devicePixelRatio': devicePixelRatio,
|
||||
});
|
||||
}
|
||||
|
||||
/// Sets the visibility of the video controls overlay.
|
||||
///
|
||||
/// On Linux, due to Flutter's lack of transparency support in GtkOverlay,
|
||||
/// we hide the video layer when controls are visible and show it when
|
||||
/// controls are hidden. This provides a workaround for the transparency
|
||||
/// limitation.
|
||||
@override
|
||||
Future<void> setControlsVisible(bool visible) async {
|
||||
await _methodChannel.invokeMethod('setControlsVisible', {
|
||||
'visible': visible,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -479,6 +479,11 @@ class MpvPlayerNative implements MpvPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setControlsVisible(bool visible) async {
|
||||
// No-op on most platforms. Override on Linux for transparency workaround.
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Lifecycle
|
||||
// ============================================
|
||||
|
||||
@@ -215,6 +215,11 @@ class MpvPlayerStub implements MpvPlayer {
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setControlsVisible(bool visible) async {
|
||||
// No-op on unsupported platforms
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Lifecycle
|
||||
// ============================================
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:io' show Platform;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../player/mpv_player.dart';
|
||||
import '../player/mpv_player_linux.dart';
|
||||
import '../player/mpv_player_windows.dart';
|
||||
|
||||
/// Video widget for displaying MPV player output.
|
||||
@@ -79,6 +80,19 @@ class _MpvVideoState extends State<MpvVideo> {
|
||||
},
|
||||
);
|
||||
}
|
||||
if (Platform.isLinux) {
|
||||
// On Linux, use GtkGLArea behind the Flutter view.
|
||||
// The GL area fills the entire overlay, and mpv handles aspect ratio.
|
||||
// We still communicate the rect for potential future use.
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_updateVideoRectLinux(context, constraints);
|
||||
});
|
||||
return const SizedBox.expand();
|
||||
},
|
||||
);
|
||||
}
|
||||
return const SizedBox.expand();
|
||||
}
|
||||
|
||||
@@ -120,4 +134,43 @@ class _MpvVideoState extends State<MpvVideo> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _updateVideoRectLinux(BuildContext context, BoxConstraints constraints) {
|
||||
final renderBox = context.findRenderObject() as RenderBox?;
|
||||
if (renderBox == null || !renderBox.hasSize) return;
|
||||
|
||||
final position = renderBox.localToGlobal(Offset.zero);
|
||||
final size = renderBox.size;
|
||||
final dpr = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
final newRect = Rect.fromLTWH(
|
||||
position.dx,
|
||||
position.dy,
|
||||
size.width,
|
||||
size.height,
|
||||
);
|
||||
|
||||
// Only update if the rect has changed significantly
|
||||
if (_lastRect != null &&
|
||||
(newRect.left - _lastRect!.left).abs() < 1 &&
|
||||
(newRect.top - _lastRect!.top).abs() < 1 &&
|
||||
(newRect.width - _lastRect!.width).abs() < 1 &&
|
||||
(newRect.height - _lastRect!.height).abs() < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
_lastRect = newRect;
|
||||
|
||||
// Update the Linux mpv player (triggers redraw)
|
||||
if (widget.player is MpvPlayerLinux) {
|
||||
final linuxPlayer = widget.player as MpvPlayerLinux;
|
||||
linuxPlayer.setVideoRect(
|
||||
left: (position.dx * dpr).toInt(),
|
||||
top: (position.dy * dpr).toInt(),
|
||||
right: ((position.dx + size.width) * dpr).toInt(),
|
||||
bottom: ((position.dy + size.height) * dpr).toInt(),
|
||||
devicePixelRatio: dpr,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ class AudioTrackSheet {
|
||||
BuildContext context,
|
||||
MpvPlayer player, {
|
||||
Function(MpvAudioTrack)? onTrackChanged,
|
||||
VoidCallback? onOpen,
|
||||
VoidCallback? onClose,
|
||||
}) {
|
||||
TrackSelectionSheet.show<MpvAudioTrack>(
|
||||
context: context,
|
||||
@@ -36,6 +38,8 @@ class AudioTrackSheet {
|
||||
},
|
||||
setTrack: (track) => player.selectAudioTrack(track),
|
||||
onTrackChanged: onTrackChanged,
|
||||
onOpen: onOpen,
|
||||
onClose: onClose,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,14 +34,19 @@ class BaseVideoControlSheet extends StatelessWidget {
|
||||
static Future<T?> showSheet<T>({
|
||||
required BuildContext context,
|
||||
required WidgetBuilder builder,
|
||||
VoidCallback? onOpen,
|
||||
VoidCallback? onClose,
|
||||
}) {
|
||||
onOpen?.call();
|
||||
return showModalBottomSheet<T>(
|
||||
context: context,
|
||||
backgroundColor: Colors.grey[900],
|
||||
isScrollControlled: true,
|
||||
constraints: getBottomSheetConstraints(context),
|
||||
builder: builder,
|
||||
);
|
||||
).whenComplete(() {
|
||||
onClose?.call();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -28,9 +28,13 @@ class ChapterSheet extends StatelessWidget {
|
||||
List<PlexChapter> chapters,
|
||||
bool chaptersLoaded, {
|
||||
String? serverId,
|
||||
VoidCallback? onOpen,
|
||||
VoidCallback? onClose,
|
||||
}) {
|
||||
BaseVideoControlSheet.showSheet(
|
||||
context: context,
|
||||
onOpen: onOpen,
|
||||
onClose: onClose,
|
||||
builder: (context) => ChapterSheet(
|
||||
player: player,
|
||||
chapters: chapters,
|
||||
|
||||
@@ -10,6 +10,8 @@ class SubtitleTrackSheet {
|
||||
BuildContext context,
|
||||
MpvPlayer player, {
|
||||
Function(MpvSubtitleTrack)? onTrackChanged,
|
||||
VoidCallback? onOpen,
|
||||
VoidCallback? onClose,
|
||||
}) {
|
||||
TrackSelectionSheet.show<MpvSubtitleTrack>(
|
||||
context: context,
|
||||
@@ -47,6 +49,8 @@ class SubtitleTrackSheet {
|
||||
showOffOption: true,
|
||||
createOffTrack: () => MpvSubtitleTrack.off,
|
||||
isOffTrack: (track) => track.id == 'no',
|
||||
onOpen: onOpen,
|
||||
onClose: onClose,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,9 +47,13 @@ class TrackSelectionSheet<T> extends StatelessWidget {
|
||||
bool showOffOption = false,
|
||||
T Function()? createOffTrack,
|
||||
bool Function(T track)? isOffTrack,
|
||||
VoidCallback? onOpen,
|
||||
VoidCallback? onClose,
|
||||
}) {
|
||||
BaseVideoControlSheet.showSheet(
|
||||
context: context,
|
||||
onOpen: onOpen,
|
||||
onClose: onClose,
|
||||
builder: (context) => TrackSelectionSheet<T>(
|
||||
player: player,
|
||||
title: title,
|
||||
|
||||
@@ -19,10 +19,14 @@ class VersionSheet extends StatelessWidget {
|
||||
BuildContext context,
|
||||
List<PlexMediaVersion> availableVersions,
|
||||
int selectedMediaIndex,
|
||||
Function(int) onVersionSelected,
|
||||
) {
|
||||
Function(int) onVersionSelected, {
|
||||
VoidCallback? onOpen,
|
||||
VoidCallback? onClose,
|
||||
}) {
|
||||
BaseVideoControlSheet.showSheet(
|
||||
context: context,
|
||||
onOpen: onOpen,
|
||||
onClose: onClose,
|
||||
builder: (context) => VersionSheet(
|
||||
availableVersions: availableVersions,
|
||||
selectedMediaIndex: selectedMediaIndex,
|
||||
|
||||
@@ -74,8 +74,11 @@ class VideoSettingsSheet extends StatefulWidget {
|
||||
BuildContext context,
|
||||
MpvPlayer player,
|
||||
int audioSyncOffset,
|
||||
int subtitleSyncOffset,
|
||||
) {
|
||||
int subtitleSyncOffset, {
|
||||
VoidCallback? onOpen,
|
||||
VoidCallback? onClose,
|
||||
}) {
|
||||
onOpen?.call();
|
||||
return showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.grey[900],
|
||||
@@ -86,7 +89,9 @@ class VideoSettingsSheet extends StatefulWidget {
|
||||
audioSyncOffset: audioSyncOffset,
|
||||
subtitleSyncOffset: subtitleSyncOffset,
|
||||
),
|
||||
);
|
||||
).whenComplete(() {
|
||||
onClose?.call();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -86,6 +86,7 @@ class PlexVideoControls extends StatefulWidget {
|
||||
class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
with WindowListener, WidgetsBindingObserver {
|
||||
bool _showControls = true;
|
||||
bool _controlsFullyHidden = false; // For Linux: true after fade-out completes
|
||||
List<PlexChapter> _chapters = [];
|
||||
bool _chaptersLoaded = false;
|
||||
Timer? _hideTimer;
|
||||
@@ -115,6 +116,8 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
bool _markersLoaded = false;
|
||||
// Playback state subscription for auto-hide timer
|
||||
StreamSubscription<bool>? _playingSubscription;
|
||||
// Completed subscription to show controls when video ends
|
||||
StreamSubscription<bool>? _completedSubscription;
|
||||
// Window resize pause state
|
||||
Timer? _resizeDebounceTimer;
|
||||
bool _wasPlayingBeforeResize = false;
|
||||
@@ -136,6 +139,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
_initKeyboardService();
|
||||
_listenToPosition();
|
||||
_listenToPlayingState();
|
||||
_listenToCompleted();
|
||||
// Add lifecycle observer to reload settings when app resumes
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
// Add window listener for tracking fullscreen state (for button icon)
|
||||
@@ -183,6 +187,24 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
});
|
||||
}
|
||||
|
||||
/// Listen to completed stream to show controls when video ends
|
||||
void _listenToCompleted() {
|
||||
_completedSubscription = widget.player.streams.completed.listen((completed) {
|
||||
if (completed && mounted) {
|
||||
// Show controls when video completes (for play next dialog etc.)
|
||||
setState(() {
|
||||
_showControls = true;
|
||||
_controlsFullyHidden = false;
|
||||
});
|
||||
_hideTimer?.cancel();
|
||||
// On Linux, ensure Flutter view is visible
|
||||
if (Platform.isLinux) {
|
||||
widget.player.setControlsVisible(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _skipMarker() {
|
||||
if (_currentMarker != null) {
|
||||
widget.player.seek(_currentMarker!.endTime);
|
||||
@@ -247,6 +269,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
_resizeDebounceTimer?.cancel();
|
||||
_seekThrottle.cancel();
|
||||
_playingSubscription?.cancel();
|
||||
_completedSubscription?.cancel();
|
||||
_focusNode.dispose();
|
||||
// Remove lifecycle observer
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
@@ -335,6 +358,18 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
if (Platform.isMacOS) {
|
||||
_updateTrafficLightVisibility();
|
||||
}
|
||||
// On Linux, fully hide after animation completes (200ms)
|
||||
if (Platform.isLinux) {
|
||||
Future.delayed(const Duration(milliseconds: 250), () {
|
||||
if (mounted && !_showControls) {
|
||||
setState(() {
|
||||
_controlsFullyHidden = true;
|
||||
});
|
||||
// Hide Flutter view to show only video
|
||||
widget.player.setControlsVisible(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -350,9 +385,27 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
void _toggleControls() {
|
||||
setState(() {
|
||||
_showControls = !_showControls;
|
||||
if (_showControls) {
|
||||
_controlsFullyHidden = false;
|
||||
// On Linux, show Flutter view when controls are shown
|
||||
if (Platform.isLinux) {
|
||||
widget.player.setControlsVisible(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (_showControls) {
|
||||
_startHideTimer();
|
||||
} else if (Platform.isLinux) {
|
||||
// On Linux, fully hide after animation completes (200ms)
|
||||
Future.delayed(const Duration(milliseconds: 250), () {
|
||||
if (mounted && !_showControls) {
|
||||
setState(() {
|
||||
_controlsFullyHidden = true;
|
||||
});
|
||||
// Hide Flutter view to show only video
|
||||
widget.player.setControlsVisible(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// On macOS, hide/show traffic lights with controls
|
||||
@@ -444,6 +497,8 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
await _loadSeekTimes();
|
||||
}
|
||||
},
|
||||
onCancelAutoHide: () => _hideTimer?.cancel(),
|
||||
onStartAutoHide: _startHideTimer,
|
||||
serverId: widget.metadata.serverId ?? '',
|
||||
);
|
||||
}
|
||||
@@ -620,7 +675,12 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
if (!_showControls) {
|
||||
setState(() {
|
||||
_showControls = true;
|
||||
_controlsFullyHidden = false;
|
||||
});
|
||||
// On Linux, show Flutter view when controls are shown
|
||||
if (Platform.isLinux) {
|
||||
widget.player.setControlsVisible(true);
|
||||
}
|
||||
_startHideTimer();
|
||||
// On macOS, show traffic lights when controls appear
|
||||
if (Platform.isMacOS) {
|
||||
@@ -639,29 +699,32 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
),
|
||||
),
|
||||
// Custom controls overlay - use AnimatedOpacity to keep widget tree alive
|
||||
// On Linux, use Offstage after fade completes to fully hide
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
ignoring: !_showControls,
|
||||
child: AnimatedOpacity(
|
||||
opacity: _showControls ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: GestureDetector(
|
||||
onTap: _toggleControls,
|
||||
behavior: HitTestBehavior.deferToChild,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withValues(alpha: 0.7),
|
||||
Colors.transparent,
|
||||
Colors.transparent,
|
||||
Colors.black.withValues(alpha: 0.7),
|
||||
],
|
||||
stops: const [0.0, 0.2, 0.8, 1.0],
|
||||
child: Offstage(
|
||||
offstage: Platform.isLinux && _controlsFullyHidden,
|
||||
child: IgnorePointer(
|
||||
ignoring: !_showControls,
|
||||
child: AnimatedOpacity(
|
||||
opacity: _showControls ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: GestureDetector(
|
||||
onTap: _toggleControls,
|
||||
behavior: HitTestBehavior.deferToChild,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withValues(alpha: 0.7),
|
||||
Colors.transparent,
|
||||
Colors.transparent,
|
||||
Colors.black.withValues(alpha: 0.7),
|
||||
],
|
||||
stops: const [0.0, 0.2, 0.8, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
child: isMobile
|
||||
? Listener(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
@@ -708,6 +771,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
getForwardIcon: getForwardIcon,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -34,6 +34,8 @@ class TrackChapterControls extends StatelessWidget {
|
||||
final Function(MpvAudioTrack)? onAudioTrackChanged;
|
||||
final Function(MpvSubtitleTrack)? onSubtitleTrackChanged;
|
||||
final VoidCallback? onLoadSeekTimes;
|
||||
final VoidCallback? onCancelAutoHide;
|
||||
final VoidCallback? onStartAutoHide;
|
||||
final String serverId;
|
||||
|
||||
const TrackChapterControls({
|
||||
@@ -56,6 +58,8 @@ class TrackChapterControls extends StatelessWidget {
|
||||
this.onAudioTrackChanged,
|
||||
this.onSubtitleTrackChanged,
|
||||
this.onLoadSeekTimes,
|
||||
this.onCancelAutoHide,
|
||||
this.onStartAutoHide,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -89,6 +93,8 @@ class TrackChapterControls extends StatelessWidget {
|
||||
player,
|
||||
audioSyncOffset,
|
||||
subtitleSyncOffset,
|
||||
onOpen: onCancelAutoHide,
|
||||
onClose: onStartAutoHide,
|
||||
);
|
||||
// Sheet is now closed, reload immediately
|
||||
onLoadSeekTimes?.call();
|
||||
@@ -104,6 +110,8 @@ class TrackChapterControls extends StatelessWidget {
|
||||
context,
|
||||
player,
|
||||
onTrackChanged: onAudioTrackChanged,
|
||||
onOpen: onCancelAutoHide,
|
||||
onClose: onStartAutoHide,
|
||||
),
|
||||
),
|
||||
if (_hasSubtitles(tracks))
|
||||
@@ -114,6 +122,8 @@ class TrackChapterControls extends StatelessWidget {
|
||||
context,
|
||||
player,
|
||||
onTrackChanged: onSubtitleTrackChanged,
|
||||
onOpen: onCancelAutoHide,
|
||||
onClose: onStartAutoHide,
|
||||
),
|
||||
),
|
||||
if (chapters.isNotEmpty)
|
||||
@@ -126,6 +136,8 @@ class TrackChapterControls extends StatelessWidget {
|
||||
chapters,
|
||||
chaptersLoaded,
|
||||
serverId: serverId,
|
||||
onOpen: onCancelAutoHide,
|
||||
onClose: onStartAutoHide,
|
||||
),
|
||||
),
|
||||
if (availableVersions.length > 1 && onSwitchVersion != null)
|
||||
@@ -137,6 +149,8 @@ class TrackChapterControls extends StatelessWidget {
|
||||
availableVersions,
|
||||
selectedMediaIndex,
|
||||
onSwitchVersion!,
|
||||
onOpen: onCancelAutoHide,
|
||||
onClose: onStartAutoHide,
|
||||
),
|
||||
),
|
||||
// BoxFit mode cycle button
|
||||
|
||||
@@ -9,6 +9,8 @@ project(runner LANGUAGES CXX)
|
||||
add_executable(${BINARY_NAME}
|
||||
"main.cc"
|
||||
"my_application.cc"
|
||||
"mpv/mpv_player.cc"
|
||||
"mpv/mpv_plugin.cc"
|
||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||
)
|
||||
|
||||
@@ -19,8 +21,16 @@ apply_standard_settings(${BINARY_NAME})
|
||||
# Add preprocessor definitions for the application ID.
|
||||
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
|
||||
|
||||
# Find mpv library.
|
||||
pkg_check_modules(MPV REQUIRED IMPORTED_TARGET mpv)
|
||||
|
||||
# Find epoxy (OpenGL loader).
|
||||
pkg_check_modules(EPOXY REQUIRED IMPORTED_TARGET epoxy)
|
||||
|
||||
# Add dependency libraries. Add any application-specific dependencies here.
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::MPV)
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::EPOXY)
|
||||
|
||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||
|
||||
@@ -0,0 +1,464 @@
|
||||
#include "mpv_player.h"
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
#include <epoxy/gl.h>
|
||||
#include <epoxy/egl.h>
|
||||
#include <epoxy/glx.h>
|
||||
#include <gdk/gdk.h>
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
#include <gdk/gdkx.h>
|
||||
#endif
|
||||
#ifdef GDK_WINDOWING_WAYLAND
|
||||
#include <gdk/gdkwayland.h>
|
||||
#endif
|
||||
#include <clocale>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
// Static helper to get proc address - must be defined outside the namespace
|
||||
// to have the correct function signature.
|
||||
static void* get_opengl_proc_address(void* ctx, const char* name) {
|
||||
(void)ctx;
|
||||
#ifdef GDK_WINDOWING_WAYLAND
|
||||
// On Wayland, use EGL
|
||||
if (epoxy_has_egl()) {
|
||||
return reinterpret_cast<void*>(eglGetProcAddress(name));
|
||||
}
|
||||
#endif
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
// On X11, use GLX
|
||||
return reinterpret_cast<void*>(glXGetProcAddressARB(
|
||||
reinterpret_cast<const GLubyte*>(name)));
|
||||
#else
|
||||
// Fallback: try EGL
|
||||
return reinterpret_cast<void*>(eglGetProcAddress(name));
|
||||
#endif
|
||||
}
|
||||
|
||||
namespace mpv {
|
||||
|
||||
MpvPlayer::MpvPlayer() {}
|
||||
|
||||
MpvPlayer::~MpvPlayer() {
|
||||
Dispose();
|
||||
}
|
||||
|
||||
bool MpvPlayer::Initialize(GtkGLArea* gl_area) {
|
||||
if (mpv_) {
|
||||
return true; // Already initialized.
|
||||
}
|
||||
|
||||
gl_area_ = gl_area;
|
||||
|
||||
// Check if GL area is realized
|
||||
if (!gtk_widget_get_realized(GTK_WIDGET(gl_area))) {
|
||||
g_warning("MPV: GL area not realized yet");
|
||||
return false;
|
||||
}
|
||||
|
||||
// MPV requires C locale for numeric formatting
|
||||
std::setlocale(LC_NUMERIC, "C");
|
||||
|
||||
// Create mpv instance.
|
||||
mpv_ = mpv_create();
|
||||
if (!mpv_) {
|
||||
g_warning("MPV: mpv_create() failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Configure mpv for embedded playback.
|
||||
mpv_set_option_string(mpv_, "vo", "libmpv"); // Render via mpv_render_context_render()
|
||||
mpv_set_option_string(mpv_, "hwdec", "auto");
|
||||
mpv_set_option_string(mpv_, "keep-open", "yes");
|
||||
mpv_set_option_string(mpv_, "idle", "yes");
|
||||
mpv_set_option_string(mpv_, "input-default-bindings", "no");
|
||||
mpv_set_option_string(mpv_, "input-vo-keyboard", "no");
|
||||
mpv_set_option_string(mpv_, "osc", "no");
|
||||
mpv_set_option_string(mpv_, "terminal", "no");
|
||||
|
||||
// Enable verbose logging for debugging.
|
||||
mpv_request_log_messages(mpv_, "v");
|
||||
|
||||
// Initialize mpv.
|
||||
int err = mpv_initialize(mpv_);
|
||||
if (err < 0) {
|
||||
g_warning("MPV: mpv_initialize() failed: %s", mpv_error_string(err));
|
||||
mpv_destroy(mpv_);
|
||||
mpv_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make the GL context current.
|
||||
gtk_gl_area_make_current(gl_area_);
|
||||
if (gtk_gl_area_get_error(gl_area_) != nullptr) {
|
||||
g_warning("MPV: Failed to make GL context current");
|
||||
mpv_terminate_destroy(mpv_);
|
||||
mpv_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set up OpenGL parameters for mpv.
|
||||
mpv_opengl_init_params gl_init_params{
|
||||
.get_proc_address = get_opengl_proc_address,
|
||||
.get_proc_address_ctx = nullptr,
|
||||
};
|
||||
|
||||
mpv_render_param params[] = {
|
||||
{MPV_RENDER_PARAM_API_TYPE,
|
||||
const_cast<char*>(MPV_RENDER_API_TYPE_OPENGL)},
|
||||
{MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init_params},
|
||||
{MPV_RENDER_PARAM_INVALID, nullptr},
|
||||
};
|
||||
|
||||
err = mpv_render_context_create(&mpv_gl_, mpv_, params);
|
||||
if (err < 0) {
|
||||
g_warning("MPV: mpv_render_context_create() failed: %s",
|
||||
mpv_error_string(err));
|
||||
mpv_terminate_destroy(mpv_);
|
||||
mpv_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set up event wakeup callback.
|
||||
mpv_set_wakeup_callback(mpv_, OnMpvWakeup, this);
|
||||
|
||||
// Set up render update callback.
|
||||
mpv_render_context_set_update_callback(mpv_gl_, OnMpvRenderUpdate, this);
|
||||
|
||||
g_message("MPV: Initialization successful");
|
||||
return true;
|
||||
}
|
||||
|
||||
void MpvPlayer::Dispose() {
|
||||
// Guard against multiple dispose calls (double-free protection)
|
||||
if (disposed_.exchange(true)) {
|
||||
return; // Already disposed
|
||||
}
|
||||
|
||||
// Clear mpv callbacks BEFORE freeing to prevent new callbacks being scheduled
|
||||
if (mpv_gl_) {
|
||||
mpv_render_context_set_update_callback(mpv_gl_, nullptr, nullptr);
|
||||
}
|
||||
if (mpv_) {
|
||||
mpv_set_wakeup_callback(mpv_, nullptr, nullptr);
|
||||
}
|
||||
|
||||
// Remove pending idle callbacks
|
||||
if (event_source_id_ != 0) {
|
||||
g_source_remove(event_source_id_);
|
||||
event_source_id_ = 0;
|
||||
}
|
||||
|
||||
// Now safe to free render context
|
||||
if (mpv_gl_) {
|
||||
mpv_render_context_free(mpv_gl_);
|
||||
mpv_gl_ = nullptr;
|
||||
}
|
||||
|
||||
// And terminate mpv
|
||||
if (mpv_) {
|
||||
mpv_terminate_destroy(mpv_);
|
||||
mpv_ = nullptr;
|
||||
}
|
||||
|
||||
observed_properties_.clear();
|
||||
gl_area_ = nullptr;
|
||||
}
|
||||
|
||||
void MpvPlayer::Command(const std::vector<std::string>& args) {
|
||||
if (disposed_ || !mpv_) return;
|
||||
|
||||
std::vector<const char*> c_args;
|
||||
c_args.reserve(args.size() + 1);
|
||||
for (const auto& arg : args) {
|
||||
c_args.push_back(arg.c_str());
|
||||
}
|
||||
c_args.push_back(nullptr);
|
||||
|
||||
mpv_command(mpv_, c_args.data());
|
||||
}
|
||||
|
||||
void MpvPlayer::SetProperty(const std::string& name, const std::string& value) {
|
||||
if (disposed_ || !mpv_) return;
|
||||
mpv_set_property_string(mpv_, name.c_str(), value.c_str());
|
||||
}
|
||||
|
||||
std::string MpvPlayer::GetProperty(const std::string& name) {
|
||||
if (disposed_ || !mpv_) return "";
|
||||
|
||||
char* value = mpv_get_property_string(mpv_, name.c_str());
|
||||
if (!value) return "";
|
||||
|
||||
std::string result(value);
|
||||
mpv_free(value);
|
||||
return result;
|
||||
}
|
||||
|
||||
void MpvPlayer::ObserveProperty(const std::string& name,
|
||||
const std::string& format) {
|
||||
if (disposed_ || !mpv_) return;
|
||||
|
||||
// Check if already observing.
|
||||
if (observed_properties_.find(name) != observed_properties_.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
mpv_format mpv_fmt = MPV_FORMAT_NONE;
|
||||
if (format == "string") {
|
||||
mpv_fmt = MPV_FORMAT_STRING;
|
||||
} else if (format == "flag" || format == "bool") {
|
||||
mpv_fmt = MPV_FORMAT_FLAG;
|
||||
} else if (format == "int64") {
|
||||
mpv_fmt = MPV_FORMAT_INT64;
|
||||
} else if (format == "double") {
|
||||
mpv_fmt = MPV_FORMAT_DOUBLE;
|
||||
} else if (format == "node") {
|
||||
mpv_fmt = MPV_FORMAT_NODE;
|
||||
}
|
||||
|
||||
uint64_t userdata = next_reply_userdata_++;
|
||||
observed_properties_[name] = userdata;
|
||||
mpv_observe_property(mpv_, userdata, name.c_str(), mpv_fmt);
|
||||
}
|
||||
|
||||
void MpvPlayer::Render(int width, int height, int fbo) {
|
||||
if (disposed_ || !mpv_gl_) return;
|
||||
|
||||
mpv_opengl_fbo mpv_fbo{
|
||||
.fbo = fbo,
|
||||
.w = width,
|
||||
.h = height,
|
||||
.internal_format = 0,
|
||||
};
|
||||
|
||||
int flip_y = 1;
|
||||
|
||||
mpv_render_param params[] = {
|
||||
{MPV_RENDER_PARAM_OPENGL_FBO, &mpv_fbo},
|
||||
{MPV_RENDER_PARAM_FLIP_Y, &flip_y},
|
||||
{MPV_RENDER_PARAM_INVALID, nullptr},
|
||||
};
|
||||
|
||||
mpv_render_context_render(mpv_gl_, params);
|
||||
}
|
||||
|
||||
void MpvPlayer::ReportMouseMove(int x, int y) {
|
||||
if (disposed_ || !mpv_) return;
|
||||
std::string x_str = std::to_string(x);
|
||||
std::string y_str = std::to_string(y);
|
||||
const char* args[] = {"mouse", x_str.c_str(), y_str.c_str(), nullptr};
|
||||
mpv_command_async(mpv_, 0, args);
|
||||
}
|
||||
|
||||
void MpvPlayer::SetEventCallback(EventCallback callback) {
|
||||
std::lock_guard<std::mutex> lock(callback_mutex_);
|
||||
event_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
void MpvPlayer::RequestRedraw() {
|
||||
if (disposed_) return;
|
||||
|
||||
needs_redraw_.store(true);
|
||||
if (gl_area_) {
|
||||
// Queue redraw on main thread
|
||||
GtkGLArea* area = gl_area_;
|
||||
g_idle_add(
|
||||
[](gpointer data) -> gboolean {
|
||||
GtkGLArea* area = static_cast<GtkGLArea*>(data);
|
||||
if (GTK_IS_GL_AREA(area)) {
|
||||
gtk_gl_area_queue_render(area);
|
||||
}
|
||||
return G_SOURCE_REMOVE;
|
||||
},
|
||||
area);
|
||||
}
|
||||
}
|
||||
|
||||
void MpvPlayer::OnMpvWakeup(void* ctx) {
|
||||
auto* player = static_cast<MpvPlayer*>(ctx);
|
||||
|
||||
// Don't schedule if already disposed
|
||||
if (player->disposed_) return;
|
||||
|
||||
// Schedule event processing on the main thread.
|
||||
g_idle_add(
|
||||
[](gpointer data) -> gboolean {
|
||||
auto* player = static_cast<MpvPlayer*>(data);
|
||||
// Check disposed again when callback runs
|
||||
if (!player->disposed_) {
|
||||
player->ProcessEvents();
|
||||
}
|
||||
return G_SOURCE_REMOVE;
|
||||
},
|
||||
player);
|
||||
}
|
||||
|
||||
void MpvPlayer::OnMpvRenderUpdate(void* ctx) {
|
||||
auto* player = static_cast<MpvPlayer*>(ctx);
|
||||
// RequestRedraw already checks disposed_
|
||||
player->RequestRedraw();
|
||||
}
|
||||
|
||||
bool MpvPlayer::ProcessEvents() {
|
||||
if (disposed_ || !mpv_) return false;
|
||||
|
||||
while (true) {
|
||||
mpv_event* event = mpv_wait_event(mpv_, 0);
|
||||
if (event->event_id == MPV_EVENT_NONE) {
|
||||
break;
|
||||
}
|
||||
if (event->event_id == MPV_EVENT_SHUTDOWN) {
|
||||
return false;
|
||||
}
|
||||
HandleMpvEvent(event);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void MpvPlayer::HandleMpvEvent(mpv_event* event) {
|
||||
switch (event->event_id) {
|
||||
case MPV_EVENT_LOG_MESSAGE: {
|
||||
auto* msg = static_cast<mpv_event_log_message*>(event->data);
|
||||
g_message("MPV [%s] %s: %s", msg->level, msg->prefix, msg->text);
|
||||
|
||||
FlValue* data = fl_value_new_map();
|
||||
fl_value_set_string_take(data, "prefix",
|
||||
fl_value_new_string(msg->prefix ? msg->prefix : ""));
|
||||
fl_value_set_string_take(data, "level",
|
||||
fl_value_new_string(msg->level ? msg->level : ""));
|
||||
fl_value_set_string_take(data, "text",
|
||||
fl_value_new_string(msg->text ? msg->text : ""));
|
||||
SendEvent("log-message", data);
|
||||
fl_value_unref(data);
|
||||
break;
|
||||
}
|
||||
case MPV_EVENT_PROPERTY_CHANGE: {
|
||||
auto* prop = static_cast<mpv_event_property*>(event->data);
|
||||
mpv_node node;
|
||||
node.format = prop->format;
|
||||
|
||||
switch (prop->format) {
|
||||
case MPV_FORMAT_STRING:
|
||||
node.u.string =
|
||||
prop->data ? *static_cast<char**>(prop->data) : nullptr;
|
||||
break;
|
||||
case MPV_FORMAT_FLAG:
|
||||
node.u.flag = prop->data ? *static_cast<int*>(prop->data) : 0;
|
||||
break;
|
||||
case MPV_FORMAT_INT64:
|
||||
node.u.int64 = prop->data ? *static_cast<int64_t*>(prop->data) : 0;
|
||||
break;
|
||||
case MPV_FORMAT_DOUBLE:
|
||||
node.u.double_ = prop->data ? *static_cast<double*>(prop->data) : 0.0;
|
||||
break;
|
||||
case MPV_FORMAT_NODE:
|
||||
if (prop->data) {
|
||||
node = *static_cast<mpv_node*>(prop->data);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
node.format = MPV_FORMAT_NONE;
|
||||
break;
|
||||
}
|
||||
|
||||
SendPropertyChange(prop->name, &node);
|
||||
break;
|
||||
}
|
||||
case MPV_EVENT_END_FILE: {
|
||||
auto* end = static_cast<mpv_event_end_file*>(event->data);
|
||||
FlValue* data = fl_value_new_map();
|
||||
fl_value_set_string_take(data, "reason",
|
||||
fl_value_new_int(static_cast<int>(end->reason)));
|
||||
if (end->reason == MPV_END_FILE_REASON_ERROR) {
|
||||
fl_value_set_string_take(data, "error",
|
||||
fl_value_new_int(static_cast<int>(end->error)));
|
||||
}
|
||||
SendEvent("end-file", data);
|
||||
fl_value_unref(data);
|
||||
break;
|
||||
}
|
||||
case MPV_EVENT_FILE_LOADED: {
|
||||
SendEvent("file-loaded");
|
||||
break;
|
||||
}
|
||||
case MPV_EVENT_PLAYBACK_RESTART: {
|
||||
SendEvent("playback-restart");
|
||||
break;
|
||||
}
|
||||
case MPV_EVENT_SEEK: {
|
||||
SendEvent("seek");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
FlValue* MpvPlayer::NodeToFlValue(mpv_node* node) {
|
||||
if (!node) return fl_value_new_null();
|
||||
|
||||
switch (node->format) {
|
||||
case MPV_FORMAT_STRING:
|
||||
return fl_value_new_string(node->u.string ? node->u.string : "");
|
||||
case MPV_FORMAT_FLAG:
|
||||
return fl_value_new_bool(node->u.flag != 0);
|
||||
case MPV_FORMAT_INT64:
|
||||
return fl_value_new_int(node->u.int64);
|
||||
case MPV_FORMAT_DOUBLE:
|
||||
return fl_value_new_float(node->u.double_);
|
||||
case MPV_FORMAT_NODE_ARRAY: {
|
||||
FlValue* list = fl_value_new_list();
|
||||
for (int i = 0; i < node->u.list->num; i++) {
|
||||
fl_value_append_take(list, NodeToFlValue(&node->u.list->values[i]));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
case MPV_FORMAT_NODE_MAP: {
|
||||
FlValue* map = fl_value_new_map();
|
||||
for (int i = 0; i < node->u.list->num; i++) {
|
||||
fl_value_set_string_take(
|
||||
map, node->u.list->keys[i],
|
||||
NodeToFlValue(&node->u.list->values[i]));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
default:
|
||||
return fl_value_new_null();
|
||||
}
|
||||
}
|
||||
|
||||
void MpvPlayer::SendPropertyChange(const char* name, mpv_node* data) {
|
||||
FlValue* event_map = fl_value_new_map();
|
||||
fl_value_set_string_take(event_map, "type", fl_value_new_string("property"));
|
||||
fl_value_set_string_take(event_map, "name",
|
||||
fl_value_new_string(name ? name : ""));
|
||||
|
||||
if (data) {
|
||||
fl_value_set_string_take(event_map, "value", NodeToFlValue(data));
|
||||
} else {
|
||||
fl_value_set_string_take(event_map, "value", fl_value_new_null());
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(callback_mutex_);
|
||||
if (event_callback_) {
|
||||
event_callback_(event_map);
|
||||
}
|
||||
fl_value_unref(event_map);
|
||||
}
|
||||
|
||||
void MpvPlayer::SendEvent(const std::string& name, FlValue* data) {
|
||||
FlValue* event_map = fl_value_new_map();
|
||||
fl_value_set_string_take(event_map, "type", fl_value_new_string("event"));
|
||||
fl_value_set_string_take(event_map, "name", fl_value_new_string(name.c_str()));
|
||||
if (data) {
|
||||
fl_value_set_string_take(event_map, "data", fl_value_ref(data));
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(callback_mutex_);
|
||||
if (event_callback_) {
|
||||
event_callback_(event_map);
|
||||
}
|
||||
fl_value_unref(event_map);
|
||||
}
|
||||
|
||||
} // namespace mpv
|
||||
@@ -0,0 +1,134 @@
|
||||
#ifndef MPV_PLAYER_H_
|
||||
#define MPV_PLAYER_H_
|
||||
|
||||
#include <mpv/client.h>
|
||||
#include <mpv/render.h>
|
||||
#include <mpv/render_gl.h>
|
||||
#include <gtk/gtk.h>
|
||||
#include <epoxy/gl.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
// Forward declaration for Flutter types
|
||||
struct _FlValue;
|
||||
|
||||
namespace mpv {
|
||||
|
||||
/// Callback function type for mpv events.
|
||||
/// Note: FlValue* is passed from the global namespace, not mpv namespace.
|
||||
using EventCallback = std::function<void(::_FlValue*)>;
|
||||
|
||||
/// Wrapper for libmpv that handles initialization, OpenGL rendering,
|
||||
/// commands, properties, and event dispatching.
|
||||
class MpvPlayer {
|
||||
public:
|
||||
MpvPlayer();
|
||||
~MpvPlayer();
|
||||
|
||||
/// Initializes mpv with OpenGL rendering context.
|
||||
/// Must be called from the GTK main thread after GL context is available.
|
||||
/// @param gl_area The GtkGLArea widget for rendering.
|
||||
/// @return true if initialization succeeded.
|
||||
bool Initialize(GtkGLArea* gl_area);
|
||||
|
||||
/// Disposes mpv and releases resources.
|
||||
void Dispose();
|
||||
|
||||
/// Returns true if mpv is initialized.
|
||||
bool IsInitialized() const { return mpv_ != nullptr && mpv_gl_ != nullptr; }
|
||||
|
||||
/// Executes an mpv command.
|
||||
/// @param args Command arguments (e.g., ["loadfile", "url", "replace"]).
|
||||
void Command(const std::vector<std::string>& args);
|
||||
|
||||
/// Sets an mpv property by name.
|
||||
/// @param name Property name.
|
||||
/// @param value Property value as string.
|
||||
void SetProperty(const std::string& name, const std::string& value);
|
||||
|
||||
/// Gets an mpv property value by name.
|
||||
/// @param name Property name.
|
||||
/// @return Property value as string, or empty if not found.
|
||||
std::string GetProperty(const std::string& name);
|
||||
|
||||
/// Observes an mpv property for changes.
|
||||
/// Changes will be reported via the event callback.
|
||||
/// @param name Property name to observe.
|
||||
/// @param format Format type ("string", "flag", "int64", "double", "node").
|
||||
void ObserveProperty(const std::string& name, const std::string& format);
|
||||
|
||||
/// Renders a frame to the current OpenGL context.
|
||||
/// Must be called from the GTK render callback.
|
||||
/// @param width Viewport width.
|
||||
/// @param height Viewport height.
|
||||
/// @param fbo Framebuffer object to render into (0 for default).
|
||||
void Render(int width, int height, int fbo = 0);
|
||||
|
||||
/// Reports that the mouse has moved.
|
||||
/// This is used to show/hide the cursor.
|
||||
void ReportMouseMove(int x, int y);
|
||||
|
||||
/// Sets the event callback for property changes and events.
|
||||
void SetEventCallback(EventCallback callback);
|
||||
|
||||
/// Returns the GtkGLArea widget.
|
||||
GtkGLArea* GetGLArea() const { return gl_area_; }
|
||||
|
||||
/// Returns true if a redraw is needed.
|
||||
bool NeedsRedraw() const { return needs_redraw_.load(); }
|
||||
|
||||
/// Clears the redraw flag.
|
||||
void ClearRedrawFlag() { needs_redraw_.store(false); }
|
||||
|
||||
/// Request a redraw.
|
||||
void RequestRedraw();
|
||||
|
||||
private:
|
||||
/// MPV event wakeup callback (called from mpv thread).
|
||||
static void OnMpvWakeup(void* ctx);
|
||||
|
||||
/// MPV render update callback (called when frame is ready).
|
||||
static void OnMpvRenderUpdate(void* ctx);
|
||||
|
||||
/// Processes pending mpv events.
|
||||
/// @return true to keep processing, false if shutdown.
|
||||
bool ProcessEvents();
|
||||
|
||||
/// Handles a single mpv event.
|
||||
void HandleMpvEvent(mpv_event* event);
|
||||
|
||||
/// Sends a property change notification.
|
||||
void SendPropertyChange(const char* name, mpv_node* data);
|
||||
|
||||
/// Sends an event notification.
|
||||
void SendEvent(const std::string& name, ::_FlValue* data = nullptr);
|
||||
|
||||
/// Helper to convert mpv_node to FlValue.
|
||||
::_FlValue* NodeToFlValue(mpv_node* node);
|
||||
|
||||
mpv_handle* mpv_ = nullptr;
|
||||
mpv_render_context* mpv_gl_ = nullptr;
|
||||
GtkGLArea* gl_area_ = nullptr;
|
||||
|
||||
std::atomic<bool> needs_redraw_{false};
|
||||
std::atomic<bool> disposed_{false};
|
||||
EventCallback event_callback_;
|
||||
std::mutex callback_mutex_;
|
||||
|
||||
uint64_t next_reply_userdata_ = 1;
|
||||
std::map<std::string, uint64_t> observed_properties_;
|
||||
|
||||
// GSource for processing events on main thread
|
||||
guint event_source_id_ = 0;
|
||||
};
|
||||
|
||||
} // namespace mpv
|
||||
|
||||
#endif // MPV_PLAYER_H_
|
||||
@@ -0,0 +1,411 @@
|
||||
#include "mpv_plugin.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
/// Plugin structure definition.
|
||||
struct _MpvPlugin {
|
||||
GObject parent_instance;
|
||||
|
||||
FlPluginRegistrar* registrar;
|
||||
FlMethodChannel* method_channel;
|
||||
FlEventChannel* event_channel;
|
||||
FlBasicMessageChannel* event_message_channel;
|
||||
|
||||
GtkOverlay* overlay;
|
||||
GtkGLArea* gl_area;
|
||||
GtkWidget* flutter_view;
|
||||
|
||||
std::unique_ptr<mpv::MpvPlayer> player;
|
||||
gboolean visible;
|
||||
gboolean initialized;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(MpvPlugin, mpv_plugin, G_TYPE_OBJECT)
|
||||
|
||||
// Forward declarations
|
||||
static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
|
||||
FlMethodCall* method_call,
|
||||
gpointer user_data);
|
||||
static gboolean on_gl_render(GtkGLArea* area,
|
||||
GdkGLContext* context,
|
||||
gpointer user_data);
|
||||
static void on_gl_realize(GtkGLArea* area, gpointer user_data);
|
||||
static void on_gl_unrealize(GtkGLArea* area, gpointer user_data);
|
||||
|
||||
static void mpv_plugin_dispose(GObject* object) {
|
||||
MpvPlugin* self = MPV_PLUGIN(object);
|
||||
|
||||
if (self->player) {
|
||||
self->player->Dispose();
|
||||
self->player.reset();
|
||||
}
|
||||
|
||||
g_clear_object(&self->method_channel);
|
||||
g_clear_object(&self->event_channel);
|
||||
g_clear_object(&self->registrar);
|
||||
|
||||
G_OBJECT_CLASS(mpv_plugin_parent_class)->dispose(object);
|
||||
}
|
||||
|
||||
static void mpv_plugin_class_init(MpvPluginClass* klass) {
|
||||
G_OBJECT_CLASS(klass)->dispose = mpv_plugin_dispose;
|
||||
}
|
||||
|
||||
static void mpv_plugin_init(MpvPlugin* self) {
|
||||
self->visible = FALSE;
|
||||
self->initialized = FALSE;
|
||||
}
|
||||
|
||||
/// Send an event through the event channel.
|
||||
static void send_event(MpvPlugin* self, FlValue* event) {
|
||||
if (self->event_channel) {
|
||||
g_autoptr(GError) error = nullptr;
|
||||
if (!fl_event_channel_send(self->event_channel, event, nullptr, &error)) {
|
||||
if (error != nullptr) {
|
||||
g_warning("Failed to send event: %s", error->message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MpvPlugin* mpv_plugin_new(FlPluginRegistrar* registrar,
|
||||
GtkOverlay* overlay,
|
||||
GtkGLArea* gl_area,
|
||||
GtkWidget* flutter_view) {
|
||||
MpvPlugin* self = MPV_PLUGIN(g_object_new(MPV_PLUGIN_TYPE, nullptr));
|
||||
|
||||
self->registrar = FL_PLUGIN_REGISTRAR(g_object_ref(registrar));
|
||||
self->overlay = overlay;
|
||||
self->gl_area = gl_area;
|
||||
self->flutter_view = flutter_view;
|
||||
self->player = std::make_unique<mpv::MpvPlayer>();
|
||||
|
||||
// Create method channel.
|
||||
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
|
||||
self->method_channel = fl_method_channel_new(
|
||||
fl_plugin_registrar_get_messenger(registrar),
|
||||
"com.plezy/mpv_player",
|
||||
FL_METHOD_CODEC(codec));
|
||||
|
||||
fl_method_channel_set_method_call_handler(
|
||||
self->method_channel,
|
||||
mpv_plugin_handle_method_call,
|
||||
self,
|
||||
nullptr);
|
||||
|
||||
// Create event channel.
|
||||
self->event_channel = fl_event_channel_new(
|
||||
fl_plugin_registrar_get_messenger(registrar),
|
||||
"com.plezy/mpv_player/events",
|
||||
FL_METHOD_CODEC(codec));
|
||||
|
||||
// Connect GtkGLArea signals.
|
||||
g_signal_connect(gl_area, "render", G_CALLBACK(on_gl_render), self);
|
||||
g_signal_connect(gl_area, "realize", G_CALLBACK(on_gl_realize), self);
|
||||
g_signal_connect(gl_area, "unrealize", G_CALLBACK(on_gl_unrealize), self);
|
||||
|
||||
// Set up auto-render to false - we control when to render.
|
||||
gtk_gl_area_set_auto_render(gl_area, FALSE);
|
||||
|
||||
// Use OpenGL 3.3 core profile.
|
||||
gtk_gl_area_set_required_version(gl_area, 3, 3);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
// Static reference to keep the plugin alive for the lifetime of the app.
|
||||
// The plugin will be disposed when the GL area is unrealized.
|
||||
static MpvPlugin* g_mpv_plugin = nullptr;
|
||||
|
||||
void mpv_plugin_register_with_registrar(FlPluginRegistrar* registrar,
|
||||
GtkOverlay* overlay,
|
||||
GtkGLArea* gl_area,
|
||||
GtkWidget* flutter_view) {
|
||||
g_mpv_plugin = mpv_plugin_new(registrar, overlay, gl_area, flutter_view);
|
||||
// Keep a reference - the plugin will be cleaned up when the app exits
|
||||
}
|
||||
|
||||
/// GtkGLArea render callback.
|
||||
static gboolean on_gl_render(GtkGLArea* area,
|
||||
GdkGLContext* context,
|
||||
gpointer user_data) {
|
||||
(void)context;
|
||||
MpvPlugin* self = MPV_PLUGIN(user_data);
|
||||
|
||||
if (!self->player || !self->player->IsInitialized() || !self->visible) {
|
||||
// Clear to transparent when not showing video.
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
int width = gtk_widget_get_allocated_width(GTK_WIDGET(area));
|
||||
int height = gtk_widget_get_allocated_height(GTK_WIDGET(area));
|
||||
|
||||
// Get the scale factor for HiDPI support.
|
||||
int scale = gtk_widget_get_scale_factor(GTK_WIDGET(area));
|
||||
width *= scale;
|
||||
height *= scale;
|
||||
|
||||
// Get the FBO that GtkGLArea is rendering to.
|
||||
// GtkGLArea uses its own FBO, not the default framebuffer (0).
|
||||
GLint fbo = 0;
|
||||
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &fbo);
|
||||
|
||||
// Render the video frame.
|
||||
self->player->Render(width, height, fbo);
|
||||
self->player->ClearRedrawFlag();
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/// GtkGLArea realize callback.
|
||||
static void on_gl_realize(GtkGLArea* area, gpointer user_data) {
|
||||
(void)user_data;
|
||||
gtk_gl_area_make_current(area);
|
||||
|
||||
// Check for GL errors.
|
||||
GError* error = gtk_gl_area_get_error(area);
|
||||
if (error != nullptr) {
|
||||
g_warning("MPV Plugin: GL area error: %s", error->message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable blending for transparency support.
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
g_message("MPV Plugin: GL area realized");
|
||||
}
|
||||
|
||||
/// GtkGLArea unrealize callback.
|
||||
static void on_gl_unrealize(GtkGLArea* area, gpointer user_data) {
|
||||
MpvPlugin* self = MPV_PLUGIN(user_data);
|
||||
|
||||
gtk_gl_area_make_current(area);
|
||||
|
||||
if (self->player) {
|
||||
self->player->Dispose();
|
||||
}
|
||||
|
||||
g_message("MPV Plugin: GL area unrealized");
|
||||
}
|
||||
|
||||
/// Method call handler.
|
||||
static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
|
||||
FlMethodCall* method_call,
|
||||
gpointer user_data) {
|
||||
(void)channel;
|
||||
MpvPlugin* self = MPV_PLUGIN(user_data);
|
||||
const gchar* method = fl_method_call_get_name(method_call);
|
||||
FlValue* args = fl_method_call_get_args(method_call);
|
||||
|
||||
g_autoptr(FlMethodResponse) response = nullptr;
|
||||
|
||||
if (strcmp(method, "initialize") == 0) {
|
||||
if (self->initialized) {
|
||||
response = FL_METHOD_RESPONSE(
|
||||
fl_method_success_response_new(fl_value_new_bool(TRUE)));
|
||||
} else {
|
||||
// Create player if it was disposed
|
||||
if (!self->player) {
|
||||
self->player = std::make_unique<mpv::MpvPlayer>();
|
||||
}
|
||||
|
||||
// Check if GL area is realized before trying to use it
|
||||
if (!gtk_widget_get_realized(GTK_WIDGET(self->gl_area))) {
|
||||
// Force realization of the GL area
|
||||
gtk_widget_realize(GTK_WIDGET(self->gl_area));
|
||||
}
|
||||
|
||||
// Initialize the player with the GL area.
|
||||
gtk_gl_area_make_current(self->gl_area);
|
||||
|
||||
GError* error = gtk_gl_area_get_error(self->gl_area);
|
||||
if (error != nullptr) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
"GL_ERROR", error->message, nullptr));
|
||||
} else if (self->player->Initialize(self->gl_area)) {
|
||||
self->initialized = TRUE;
|
||||
|
||||
// Set up event callback.
|
||||
self->player->SetEventCallback([self](FlValue* event) {
|
||||
// Send event - must be called from main thread
|
||||
// The event is already created on the main thread via g_idle_add in mpv_player.cc
|
||||
send_event(self, event);
|
||||
});
|
||||
|
||||
response = FL_METHOD_RESPONSE(
|
||||
fl_method_success_response_new(fl_value_new_bool(TRUE)));
|
||||
} else {
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
"INIT_FAILED", "Failed to initialize MPV player", nullptr));
|
||||
}
|
||||
}
|
||||
} else if (strcmp(method, "dispose") == 0) {
|
||||
if (self->player) {
|
||||
// Make GL context current before disposing mpv GL resources
|
||||
gtk_gl_area_make_current(self->gl_area);
|
||||
self->player->Dispose();
|
||||
self->player.reset();
|
||||
}
|
||||
self->initialized = FALSE;
|
||||
self->visible = FALSE;
|
||||
gtk_widget_set_visible(GTK_WIDGET(self->gl_area), FALSE);
|
||||
// Restore Flutter view opacity to 1.0 (may have been set to 0 by setControlsVisible)
|
||||
if (self->flutter_view != nullptr) {
|
||||
gtk_widget_set_opacity(self->flutter_view, 1.0);
|
||||
// Force Flutter view to redraw
|
||||
gtk_widget_queue_draw(self->flutter_view);
|
||||
}
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
} else if (strcmp(method, "command") == 0) {
|
||||
if (!self->player || !self->initialized) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
"NOT_INITIALIZED", "Player not initialized", nullptr));
|
||||
} else {
|
||||
FlValue* args_value = fl_value_lookup_string(args, "args");
|
||||
if (args_value == nullptr ||
|
||||
fl_value_get_type(args_value) != FL_VALUE_TYPE_LIST) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
"INVALID_ARGS", "Missing 'args' list", nullptr));
|
||||
} else {
|
||||
std::vector<std::string> command_args;
|
||||
size_t len = fl_value_get_length(args_value);
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
FlValue* item = fl_value_get_list_value(args_value, i);
|
||||
if (fl_value_get_type(item) == FL_VALUE_TYPE_STRING) {
|
||||
command_args.push_back(fl_value_get_string(item));
|
||||
}
|
||||
}
|
||||
self->player->Command(command_args);
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
}
|
||||
}
|
||||
} else if (strcmp(method, "setProperty") == 0) {
|
||||
if (!self->player || !self->initialized) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
"NOT_INITIALIZED", "Player not initialized", nullptr));
|
||||
} else {
|
||||
FlValue* name_value = fl_value_lookup_string(args, "name");
|
||||
FlValue* value_value = fl_value_lookup_string(args, "value");
|
||||
|
||||
if (name_value == nullptr ||
|
||||
fl_value_get_type(name_value) != FL_VALUE_TYPE_STRING) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
"INVALID_ARGS", "Missing 'name'", nullptr));
|
||||
} else if (value_value == nullptr ||
|
||||
fl_value_get_type(value_value) != FL_VALUE_TYPE_STRING) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
"INVALID_ARGS", "Missing 'value'", nullptr));
|
||||
} else {
|
||||
self->player->SetProperty(fl_value_get_string(name_value),
|
||||
fl_value_get_string(value_value));
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
}
|
||||
}
|
||||
} else if (strcmp(method, "getProperty") == 0) {
|
||||
if (!self->player || !self->initialized) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
"NOT_INITIALIZED", "Player not initialized", nullptr));
|
||||
} else {
|
||||
FlValue* name_value = fl_value_lookup_string(args, "name");
|
||||
|
||||
if (name_value == nullptr ||
|
||||
fl_value_get_type(name_value) != FL_VALUE_TYPE_STRING) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
"INVALID_ARGS", "Missing 'name'", nullptr));
|
||||
} else {
|
||||
std::string value =
|
||||
self->player->GetProperty(fl_value_get_string(name_value));
|
||||
if (value.empty()) {
|
||||
response =
|
||||
FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
} else {
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(
|
||||
fl_value_new_string(value.c_str())));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (strcmp(method, "observeProperty") == 0) {
|
||||
if (!self->player || !self->initialized) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
"NOT_INITIALIZED", "Player not initialized", nullptr));
|
||||
} else {
|
||||
FlValue* name_value = fl_value_lookup_string(args, "name");
|
||||
FlValue* format_value = fl_value_lookup_string(args, "format");
|
||||
|
||||
if (name_value == nullptr ||
|
||||
fl_value_get_type(name_value) != FL_VALUE_TYPE_STRING) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
"INVALID_ARGS", "Missing 'name'", nullptr));
|
||||
} else if (format_value == nullptr ||
|
||||
fl_value_get_type(format_value) != FL_VALUE_TYPE_STRING) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
"INVALID_ARGS", "Missing 'format'", nullptr));
|
||||
} else {
|
||||
self->player->ObserveProperty(fl_value_get_string(name_value),
|
||||
fl_value_get_string(format_value));
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
}
|
||||
}
|
||||
} else if (strcmp(method, "setVisible") == 0) {
|
||||
FlValue* visible_value = fl_value_lookup_string(args, "visible");
|
||||
|
||||
if (visible_value == nullptr ||
|
||||
fl_value_get_type(visible_value) != FL_VALUE_TYPE_BOOL) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
"INVALID_ARGS", "Missing 'visible'", nullptr));
|
||||
} else {
|
||||
gboolean visible = fl_value_get_bool(visible_value);
|
||||
self->visible = visible;
|
||||
|
||||
// Show/hide the GL area.
|
||||
gtk_widget_set_visible(GTK_WIDGET(self->gl_area), visible);
|
||||
|
||||
if (visible) {
|
||||
gtk_gl_area_queue_render(self->gl_area);
|
||||
}
|
||||
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
}
|
||||
} else if (strcmp(method, "setVideoRect") == 0) {
|
||||
// On Linux, the GtkGLArea fills the entire overlay area,
|
||||
// and mpv handles its own aspect ratio. So we just trigger a redraw.
|
||||
if (self->player && self->initialized && self->visible) {
|
||||
gtk_gl_area_queue_render(self->gl_area);
|
||||
}
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
} else if (strcmp(method, "setControlsVisible") == 0) {
|
||||
// Set Flutter view opacity when controls are hidden/shown.
|
||||
// This is a workaround for Flutter's lack of transparency support on Linux.
|
||||
// When controls are hidden, setting opacity to 0 shows only the video
|
||||
// while keeping the widget interactive for mouse events.
|
||||
FlValue* controls_visible_value = fl_value_lookup_string(args, "visible");
|
||||
|
||||
if (controls_visible_value == nullptr ||
|
||||
fl_value_get_type(controls_visible_value) != FL_VALUE_TYPE_BOOL) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
"INVALID_ARGS", "Missing 'visible'", nullptr));
|
||||
} else {
|
||||
gboolean controls_visible = fl_value_get_bool(controls_visible_value);
|
||||
|
||||
// When controls are hidden, set Flutter view opacity to 0.
|
||||
// When controls are visible, set opacity to 1.
|
||||
// Using opacity keeps the widget interactive for mouse events.
|
||||
if (self->flutter_view != nullptr) {
|
||||
gtk_widget_set_opacity(self->flutter_view, controls_visible ? 1.0 : 0.0);
|
||||
}
|
||||
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
}
|
||||
} else if (strcmp(method, "isInitialized") == 0) {
|
||||
gboolean initialized = self->player && self->initialized;
|
||||
response = FL_METHOD_RESPONSE(
|
||||
fl_method_success_response_new(fl_value_new_bool(initialized)));
|
||||
} else {
|
||||
response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
|
||||
}
|
||||
|
||||
fl_method_call_respond(method_call, response, nullptr);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#ifndef MPV_PLUGIN_H_
|
||||
#define MPV_PLUGIN_H_
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "mpv_player.h"
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/// Plugin for MPV video playback on Linux.
|
||||
///
|
||||
/// This plugin uses OpenGL rendering via GtkGLArea,
|
||||
/// positioned behind the Flutter view using a GtkOverlay.
|
||||
|
||||
#define MPV_PLUGIN_TYPE (mpv_plugin_get_type())
|
||||
|
||||
G_DECLARE_FINAL_TYPE(MpvPlugin, mpv_plugin, MPV, PLUGIN, GObject)
|
||||
|
||||
/// Creates a new MpvPlugin instance.
|
||||
/// @param registrar The Flutter plugin registrar.
|
||||
/// @param overlay The GtkOverlay containing the GtkGLArea and FlView.
|
||||
/// @param gl_area The GtkGLArea widget for video rendering.
|
||||
/// @param flutter_view The Flutter view widget (for visibility control).
|
||||
MpvPlugin* mpv_plugin_new(FlPluginRegistrar* registrar,
|
||||
GtkOverlay* overlay,
|
||||
GtkGLArea* gl_area,
|
||||
GtkWidget* flutter_view);
|
||||
|
||||
/// Registers the plugin with Flutter.
|
||||
void mpv_plugin_register_with_registrar(FlPluginRegistrar* registrar,
|
||||
GtkOverlay* overlay,
|
||||
GtkGLArea* gl_area,
|
||||
GtkWidget* flutter_view);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif // MPV_PLUGIN_H_
|
||||
@@ -6,14 +6,34 @@
|
||||
#endif
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
#include "mpv/mpv_plugin.h"
|
||||
|
||||
struct _MyApplication {
|
||||
GtkApplication parent_instance;
|
||||
char** dart_entrypoint_arguments;
|
||||
|
||||
// MPV-related widgets
|
||||
GtkOverlay* overlay;
|
||||
GtkGLArea* gl_area;
|
||||
FlView* flutter_view;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||
|
||||
/// Sets up an RGBA visual for transparency support.
|
||||
static void setup_rgba_visual(GtkWidget* widget) {
|
||||
GdkScreen* screen = gtk_widget_get_screen(widget);
|
||||
if (!gdk_screen_is_composited(screen)) {
|
||||
g_warning("Screen is not composited - transparency may not work");
|
||||
}
|
||||
GdkVisual* visual = gdk_screen_get_rgba_visual(screen);
|
||||
if (visual != nullptr) {
|
||||
gtk_widget_set_visual(widget, visual);
|
||||
} else {
|
||||
g_warning("No RGBA visual available");
|
||||
}
|
||||
}
|
||||
|
||||
// Implements GApplication::activate.
|
||||
static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
@@ -48,31 +68,90 @@ static void my_application_activate(GApplication* application) {
|
||||
}
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
gtk_widget_show(GTK_WIDGET(window));
|
||||
|
||||
// Set up RGBA visual for transparency support.
|
||||
gtk_widget_set_app_paintable(GTK_WIDGET(window), TRUE);
|
||||
setup_rgba_visual(GTK_WIDGET(window));
|
||||
|
||||
// Create the overlay container.
|
||||
// The overlay allows us to layer widgets on top of each other:
|
||||
// - Bottom layer: GtkGLArea for mpv video rendering
|
||||
// - Top layer: FlView (Flutter) with transparent background
|
||||
self->overlay = GTK_OVERLAY(gtk_overlay_new());
|
||||
gtk_widget_show(GTK_WIDGET(self->overlay));
|
||||
|
||||
// Create the GtkGLArea for mpv video rendering.
|
||||
// This will be the bottom layer (behind Flutter).
|
||||
self->gl_area = GTK_GL_AREA(gtk_gl_area_new());
|
||||
gtk_widget_set_hexpand(GTK_WIDGET(self->gl_area), TRUE);
|
||||
gtk_widget_set_vexpand(GTK_WIDGET(self->gl_area), TRUE);
|
||||
|
||||
// Configure GL area for transparency and proper rendering.
|
||||
gtk_gl_area_set_has_alpha(self->gl_area, TRUE);
|
||||
gtk_gl_area_set_has_depth_buffer(self->gl_area, FALSE);
|
||||
gtk_gl_area_set_has_stencil_buffer(self->gl_area, FALSE);
|
||||
|
||||
// Make GL area non-interactive so mouse events pass through to Flutter.
|
||||
gtk_widget_set_can_focus(GTK_WIDGET(self->gl_area), FALSE);
|
||||
gtk_widget_set_sensitive(GTK_WIDGET(self->gl_area), FALSE);
|
||||
|
||||
// Set the GL area as the base widget of the overlay.
|
||||
// Initially hidden - will be shown when video playback starts.
|
||||
gtk_widget_set_visible(GTK_WIDGET(self->gl_area), FALSE);
|
||||
gtk_container_add(GTK_CONTAINER(self->overlay), GTK_WIDGET(self->gl_area));
|
||||
|
||||
// Create the Flutter view.
|
||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
|
||||
fl_dart_project_set_dart_entrypoint_arguments(project,
|
||||
self->dart_entrypoint_arguments);
|
||||
|
||||
FlView* view = fl_view_new(project);
|
||||
gtk_widget_show(GTK_WIDGET(view));
|
||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||
self->flutter_view = fl_view_new(project);
|
||||
gtk_widget_set_hexpand(GTK_WIDGET(self->flutter_view), TRUE);
|
||||
gtk_widget_set_vexpand(GTK_WIDGET(self->flutter_view), TRUE);
|
||||
|
||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||
// Enable transparency for the Flutter view.
|
||||
gtk_widget_set_app_paintable(GTK_WIDGET(self->flutter_view), TRUE);
|
||||
setup_rgba_visual(GTK_WIDGET(self->flutter_view));
|
||||
|
||||
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||
// Enable transparent background for the Flutter view.
|
||||
// This allows the mpv video to show through transparent areas.
|
||||
GdkRGBA transparent = {0.0, 0.0, 0.0, 0.0};
|
||||
fl_view_set_background_color(self->flutter_view, &transparent);
|
||||
|
||||
// Add the Flutter view as an overlay on top of the GL area.
|
||||
gtk_widget_show(GTK_WIDGET(self->flutter_view));
|
||||
gtk_overlay_add_overlay(self->overlay, GTK_WIDGET(self->flutter_view));
|
||||
|
||||
// Add the overlay to the window.
|
||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(self->overlay));
|
||||
|
||||
// Register Flutter plugins.
|
||||
fl_register_plugins(FL_PLUGIN_REGISTRY(self->flutter_view));
|
||||
|
||||
// Register the MPV plugin with the GL area and Flutter view for video rendering.
|
||||
FlPluginRegistrar* registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(FL_PLUGIN_REGISTRY(self->flutter_view),
|
||||
"MpvPlugin");
|
||||
mpv_plugin_register_with_registrar(registrar, self->overlay, self->gl_area,
|
||||
GTK_WIDGET(self->flutter_view));
|
||||
|
||||
gtk_widget_show(GTK_WIDGET(window));
|
||||
gtk_widget_grab_focus(GTK_WIDGET(self->flutter_view));
|
||||
}
|
||||
|
||||
// Implements GApplication::local_command_line.
|
||||
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
|
||||
static gboolean my_application_local_command_line(GApplication* application,
|
||||
gchar*** arguments,
|
||||
int* exit_status) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
// Strip out the first argument as it is the binary name.
|
||||
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
||||
|
||||
g_autoptr(GError) error = nullptr;
|
||||
if (!g_application_register(application, nullptr, &error)) {
|
||||
g_warning("Failed to register: %s", error->message);
|
||||
*exit_status = 1;
|
||||
return TRUE;
|
||||
g_warning("Failed to register: %s", error->message);
|
||||
*exit_status = 1;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
g_application_activate(application);
|
||||
@@ -83,7 +162,7 @@ static gboolean my_application_local_command_line(GApplication* application, gch
|
||||
|
||||
// Implements GApplication::startup.
|
||||
static void my_application_startup(GApplication* application) {
|
||||
//MyApplication* self = MY_APPLICATION(object);
|
||||
// MyApplication* self = MY_APPLICATION(object);
|
||||
|
||||
// Perform any actions required at application startup.
|
||||
|
||||
@@ -92,7 +171,7 @@ static void my_application_startup(GApplication* application) {
|
||||
|
||||
// Implements GApplication::shutdown.
|
||||
static void my_application_shutdown(GApplication* application) {
|
||||
//MyApplication* self = MY_APPLICATION(object);
|
||||
// MyApplication* self = MY_APPLICATION(object);
|
||||
|
||||
// Perform any actions required at application shutdown.
|
||||
|
||||
@@ -108,13 +187,18 @@ static void my_application_dispose(GObject* object) {
|
||||
|
||||
static void my_application_class_init(MyApplicationClass* klass) {
|
||||
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
|
||||
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
|
||||
G_APPLICATION_CLASS(klass)->local_command_line =
|
||||
my_application_local_command_line;
|
||||
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
|
||||
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
|
||||
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
|
||||
}
|
||||
|
||||
static void my_application_init(MyApplication* self) {}
|
||||
static void my_application_init(MyApplication* self) {
|
||||
self->overlay = nullptr;
|
||||
self->gl_area = nullptr;
|
||||
self->flutter_view = nullptr;
|
||||
}
|
||||
|
||||
MyApplication* my_application_new() {
|
||||
// Set the program name to the application ID, which helps various systems
|
||||
|
||||
Reference in New Issue
Block a user