Vendor media_kit_video 2.0.1 with the X11/EGL fix (hardware rendering on X11 with Flutter >= 3.38)

Flutter >= 3.38 keeps its EGL contexts on the raster thread, so
media_kit's video output found none on the platform thread and fell
back to software rendering (no shaders, ~5 fps) on X11. The patched
output opens Flutter's EGL display itself, steps GDK's GLX context aside
on X11 and releases its own context. Patch from a friend's agent, kept
in third_party/media_kit_video/starsync/. Wayland/NVIDIA still renders
on the GPU (checked); the X11 path was verified on the friend's machine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-16 01:13:14 +03:00
1 parent 25b19f2d6a
commit a87ee98faa
131 files changed
+15532 -6

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
export 'package:media_kit_video/src/video_controller/platform_video_controller.dart';
export 'package:media_kit_video/src/video_controller/video_controller.dart';
export 'package:media_kit_video/src/video_view_parameters.dart';
export 'package:media_kit_video/src/video/video.dart';
export 'package:media_kit_video/src/subtitle/subtitle_view.dart';
export 'package:media_kit_video/media_kit_video_controls/media_kit_video_controls.dart';
@@ -0,0 +1,13 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
export 'package:media_kit_video/media_kit_video_controls/src/controls/no.dart';
export 'package:media_kit_video/media_kit_video_controls/src/controls/adaptive.dart';
export 'package:media_kit_video/media_kit_video_controls/src/controls/material.dart';
export 'package:media_kit_video/media_kit_video_controls/src/controls/material_desktop.dart';
export 'package:media_kit_video/media_kit_video_controls/src/controls/methods/fullscreen.dart';
export 'package:media_kit_video/media_kit_video_controls/src/controls/widgets/fullscreen_inherited_widget.dart';
export 'package:media_kit_video/media_kit_video_controls/src/controls/widgets/video_state_inherited_widget.dart';
@@ -0,0 +1,27 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
// ignore_for_file: non_constant_identifier_names
import 'package:flutter/material.dart';
import 'package:media_kit_video/media_kit_video.dart';
/// {@template adaptive_video_controls}
///
/// [Video] controls based on the running platform.
///
/// {@endtemplate}
Widget AdaptiveVideoControls(VideoState state) {
switch (Theme.of(state.context).platform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
return MaterialVideoControls(state);
case TargetPlatform.macOS:
case TargetPlatform.windows:
case TargetPlatform.linux:
return MaterialDesktopVideoControls(state);
default:
return NoVideoControls(state);
}
}
@@ -0,0 +1,42 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
/// Extension methods for [Duration].
extension DurationExtension on Duration {
/// Returns clamp of [Duration] between [min] and [max].
Duration clamp(Duration min, Duration max) {
if (this < min) return min;
if (this > max) return max;
return this;
}
/// Returns a [String] representation of [Duration].
String label({Duration? reference}) {
reference ??= this;
reference = reference.abs();
if (isNegative) {
return abs().label(reference: reference);
}
if (reference > const Duration(days: 1)) {
final days = inDays.toString().padLeft(3, '0');
final hours = (inHours - (inDays * 24)).toString().padLeft(2, '0');
final minutes = (inMinutes - (inHours * 60)).toString().padLeft(2, '0');
final seconds = (inSeconds - (inMinutes * 60)).toString().padLeft(2, '0');
return '$days:$hours:$minutes:$seconds';
} else if (reference > const Duration(hours: 1)) {
final hours = inHours.toString().padLeft(2, '0');
final minutes = (inMinutes - (inHours * 60)).toString().padLeft(2, '0');
final seconds = (inSeconds - (inMinutes * 60)).toString().padLeft(2, '0');
return '$hours:$minutes:$seconds';
} else {
final minutes = inMinutes.toString().padLeft(2, '0');
final seconds = (inSeconds - (inMinutes * 60)).toString().padLeft(2, '0');
return '$minutes:$seconds';
}
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,119 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:synchronized/synchronized.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:media_kit_video/media_kit_video_controls/src/controls/methods/video_state.dart';
import 'package:media_kit_video/media_kit_video_controls/src/controls/widgets/video_controls_theme_data_injector.dart';
/// Whether a [Video] present in the current [BuildContext] is in fullscreen or not.
bool isFullscreen(BuildContext context) =>
FullscreenInheritedWidget.maybeOf(context) != null;
/// Makes the [Video] present in the current [BuildContext] enter fullscreen.
Future<void> enterFullscreen(BuildContext context) {
return lock.synchronized(() async {
if (!isFullscreen(context)) {
if (context.mounted) {
final stateValue = state(context);
final contextNotifierValue = contextNotifier(context);
final videoViewParametersNotifierValue =
videoViewParametersNotifier(context);
final controllerValue = controller(context);
Navigator.of(context, rootNavigator: true).push(
PageRouteBuilder(
pageBuilder: (_, __, ___) => Material(
child: VideoControlsThemeDataInjector(
// NOTE: Make various *VideoControlsThemeData from the parent context available in the fullscreen context.
context: context,
child: VideoStateInheritedWidget(
state: stateValue,
contextNotifier: contextNotifierValue,
videoViewParametersNotifier: videoViewParametersNotifierValue,
disposeNotifiers: false,
child: FullscreenInheritedWidget(
parent: stateValue,
// Another [VideoStateInheritedWidget] inside [FullscreenInheritedWidget] is important to notify about the fullscreen [BuildContext].
child: VideoStateInheritedWidget(
state: stateValue,
contextNotifier: contextNotifierValue,
videoViewParametersNotifier:
videoViewParametersNotifierValue,
disposeNotifiers: false,
child: Video(
controller: controllerValue,
// Do not restrict the video's width & height in fullscreen mode:
width: null,
height: null,
fit: videoViewParametersNotifierValue.value.fit,
fill: videoViewParametersNotifierValue.value.fill,
alignment:
videoViewParametersNotifierValue.value.alignment,
aspectRatio:
videoViewParametersNotifierValue.value.aspectRatio,
filterQuality: videoViewParametersNotifierValue
.value.filterQuality,
controls:
videoViewParametersNotifierValue.value.controls,
// Do not acquire or modify existing wakelock in fullscreen mode:
wakelock: false,
pauseUponEnteringBackgroundMode:
stateValue.widget.pauseUponEnteringBackgroundMode,
resumeUponEnteringForegroundMode:
stateValue.widget.resumeUponEnteringForegroundMode,
subtitleViewConfiguration:
videoViewParametersNotifierValue
.value.subtitleViewConfiguration,
focusNode:
videoViewParametersNotifierValue.value.focusNode,
onEnterFullscreen: stateValue.widget.onEnterFullscreen,
onExitFullscreen: stateValue.widget.onExitFullscreen,
),
),
),
),
),
),
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
),
);
await onEnterFullscreen(context)?.call();
}
}
});
}
/// Makes the [Video] present in the current [BuildContext] exit fullscreen.
Future<void> exitFullscreen(BuildContext context) {
return lock.synchronized(() async {
if (isFullscreen(context)) {
if (context.mounted) {
await Navigator.of(context).maybePop();
// It is known that this [context] will have a [FullscreenInheritedWidget] above it.
if (context.mounted) {
FullscreenInheritedWidget.of(context).parent.refreshView();
}
}
// [exitNativeFullscreen] is moved to [WillPopScope] in [FullscreenInheritedWidget].
// This is because [exitNativeFullscreen] needs to be called when the user presses the back button.
}
});
}
/// Toggles fullscreen for the [Video] present in the current [BuildContext].
Future<void> toggleFullscreen(BuildContext context) {
if (isFullscreen(context)) {
return exitFullscreen(context);
} else {
return enterFullscreen(context);
}
}
/// For synchronizing [enterFullscreen] & [exitFullscreen] operations.
final Lock lock = Lock();
@@ -0,0 +1,33 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
import 'package:flutter/widgets.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:media_kit_video/src/utils/dispose_safe_notifer.dart';
/// Returns the [VideoState] associated with the [Video] present in the current [BuildContext].
VideoState state(BuildContext context) =>
VideoStateInheritedWidget.of(context).state;
/// Returns the [ValueNotifier<BuildContext>] associated with the [Video] present in the current [BuildContext].
DisposeSafeNotifier<BuildContext?> contextNotifier(BuildContext context) =>
VideoStateInheritedWidget.of(context).contextNotifier;
/// Returns the [ValueNotifier<VideoViewParameters>] associated with the [Video] present in the current [BuildContext].
ValueNotifier<VideoViewParameters> videoViewParametersNotifier(
BuildContext context) =>
VideoStateInheritedWidget.of(context).videoViewParametersNotifier;
/// Returns the [VideoController] associated with the [Video] present in the current [BuildContext].
VideoController controller(BuildContext context) =>
VideoStateInheritedWidget.of(context).state.widget.controller;
/// Returns the callback which must be invoked when the video enters fullscreen mode.
Future<void> Function()? onEnterFullscreen(BuildContext context) =>
VideoStateInheritedWidget.of(context).state.widget.onEnterFullscreen;
/// Returns the callback which must be invoked when the video exits fullscreen mode.
Future<void> Function()? onExitFullscreen(BuildContext context) =>
VideoStateInheritedWidget.of(context).state.widget.onExitFullscreen;
@@ -0,0 +1,14 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
// ignore_for_file: constant_identifier_names
import 'package:media_kit_video/media_kit_video.dart';
/// {@template no_video_controls}
///
/// Disables [Video] controls.
///
/// {@endtemplate}
const NoVideoControls = null;
@@ -0,0 +1,72 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
import 'package:flutter/widgets.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:media_kit_video/media_kit_video_controls/src/controls/methods/video_state.dart';
/// {@template fullscreen_inherited_widget}
///
/// Inherited widget used to identify whether parent [Video] is in fullscreen or not.
///
/// {@endtemplate}
class FullscreenInheritedWidget extends InheritedWidget {
final VideoState parent;
FullscreenInheritedWidget({
super.key,
required this.parent,
required Widget child,
}) : super(child: _FullscreenInheritedWidgetPopScope(child: child));
static FullscreenInheritedWidget? maybeOf(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<FullscreenInheritedWidget>();
}
static FullscreenInheritedWidget of(BuildContext context) {
final FullscreenInheritedWidget? result = maybeOf(context);
assert(
result != null,
'No [FullscreenInheritedWidget] found in [context]',
);
return result!;
}
@override
bool updateShouldNotify(FullscreenInheritedWidget oldWidget) =>
identical(parent, oldWidget.parent);
}
/// {@template fullscreen_inherited_widget_pop_scope}
///
/// This widget is used to exit native fullscreen when this route is popped from the navigator.
///
/// {@endtemplate}
class _FullscreenInheritedWidgetPopScope extends StatefulWidget {
final Widget child;
const _FullscreenInheritedWidgetPopScope({
required this.child,
});
@override
State<_FullscreenInheritedWidgetPopScope> createState() =>
_FullscreenInheritedWidgetPopScopeState();
}
class _FullscreenInheritedWidgetPopScopeState
extends State<_FullscreenInheritedWidgetPopScope> {
@override
Widget build(BuildContext context) {
return PopScope(
onPopInvokedWithResult: (_, __) {
// Make sure to exit native fullscreen when this route is popped from the navigator.
onExitFullscreen(context)?.call();
},
child: widget.child,
);
}
}
@@ -0,0 +1,83 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
import 'package:flutter/widgets.dart';
import 'package:media_kit_video/media_kit_video_controls/src/controls/material.dart';
import 'package:media_kit_video/media_kit_video_controls/src/controls/material_desktop.dart';
/// {@template video_controls_theme_data_injector}
///
/// VideoControlsThemeDataInjector
/// ------------------------------
///
/// Makes the various `*VideoControlsTheme` [InheritedWidget]s from provided [context] to current [BuildContext] and its descendants.
///
/// The fallback to default values is done by [VideoControlsThemeDataInjector] itself.
///
/// * [CupertinoVideoControlsTheme]
/// * [MaterialVideoControlsTheme]
/// * [MaterialDesktopVideoControlsTheme]
///
/// {@endtemplate}
class VideoControlsThemeDataInjector extends StatefulWidget {
final Widget child;
final BuildContext? context;
/// {@macro video_controls_theme_data_injector}
const VideoControlsThemeDataInjector({
super.key,
required this.child,
this.context,
});
@override
State<VideoControlsThemeDataInjector> createState() =>
_VideoControlsThemeDataInjectorState();
}
class _VideoControlsThemeDataInjectorState
extends State<VideoControlsThemeDataInjector> {
late final builders = <Widget Function(Widget)>[
// MaterialVideoControlsTheme
(child) {
final theme = MaterialVideoControlsTheme.maybeOf(
widget.context ?? context,
);
final normal = theme?.normal ?? kDefaultMaterialVideoControlsThemeData;
final fullscreen =
theme?.fullscreen ?? kDefaultMaterialVideoControlsThemeDataFullscreen;
return MaterialVideoControlsTheme(
normal: normal,
fullscreen: fullscreen,
child: child,
);
},
// MaterialDesktopVideoControlsTheme
(child) {
final theme = MaterialDesktopVideoControlsTheme.maybeOf(
widget.context ?? context,
);
final normal =
theme?.normal ?? kDefaultMaterialDesktopVideoControlsThemeData;
final fullscreen = theme?.fullscreen ??
kDefaultMaterialDesktopVideoControlsThemeDataFullscreen;
return MaterialDesktopVideoControlsTheme(
normal: normal,
fullscreen: fullscreen,
child: child,
);
},
// NOTE: Add more builders if more *VideoControlsTheme are implemented.
];
@override
Widget build(BuildContext context) {
return builders.fold<Widget>(
widget.child,
(child, builder) => builder(child),
);
}
}
@@ -0,0 +1,110 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
import 'dart:collection';
import 'package:flutter/widgets.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:media_kit_video/src/utils/dispose_safe_notifer.dart';
/// {@template video_state_inherited_widget}
///
/// Inherited widget which provides [VideoState] associated with the parent [Video] widget to descendant widgets.
///
/// {@endtemplate}
class VideoStateInheritedWidget extends InheritedWidget {
final VideoState state;
final DisposeSafeNotifier<BuildContext?> contextNotifier;
final ValueNotifier<VideoViewParameters> videoViewParametersNotifier;
final bool disposeNotifiers;
VideoStateInheritedWidget({
super.key,
required this.state,
required this.contextNotifier,
required this.videoViewParametersNotifier,
required Widget child,
this.disposeNotifiers = true,
}) : super(
child: VideoStateInheritedWidgetContextNotifier(
state: state,
contextNotifier: contextNotifier,
videoViewParametersNotifier: videoViewParametersNotifier,
disposeNotifiers: disposeNotifiers,
child: child,
),
);
static VideoStateInheritedWidget? maybeOf(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<VideoStateInheritedWidget>();
}
static VideoStateInheritedWidget of(BuildContext context) {
final VideoStateInheritedWidget? result = maybeOf(context);
assert(
result != null,
'No [VideoStateInheritedWidget] found in [context]',
);
return result!;
}
@override
bool updateShouldNotify(VideoStateInheritedWidget oldWidget) =>
identical(state, oldWidget.state) &&
identical(contextNotifier, oldWidget.contextNotifier);
}
/// {@template video_state_inherited_widget_context_notifier}
///
/// This widget is used to notify the [VideoState._contextNotifier] about the most recent [BuildContext] associated with the [Video] widget.
///
/// {@endtemplate}
class VideoStateInheritedWidgetContextNotifier extends StatefulWidget {
final VideoState state;
final DisposeSafeNotifier<BuildContext?> contextNotifier;
final ValueNotifier<VideoViewParameters?> videoViewParametersNotifier;
final bool disposeNotifiers;
final Widget child;
const VideoStateInheritedWidgetContextNotifier({
super.key,
required this.state,
required this.contextNotifier,
required this.videoViewParametersNotifier,
required this.disposeNotifiers,
required this.child,
});
@override
State<VideoStateInheritedWidgetContextNotifier> createState() =>
VideoStateInheritedWidgetContextNotifierState();
}
class VideoStateInheritedWidgetContextNotifierState
extends State<VideoStateInheritedWidgetContextNotifier> {
static final fallback = HashMap<VideoState, BuildContext>.identity();
@override
void dispose() {
if (!widget.contextNotifier.disposed) {
// Restore the original [BuildContext] associated with this [Video] widget.
widget.contextNotifier.value = fallback[widget.state];
}
super.dispose();
}
@override
Widget build(BuildContext context) {
// Only update the [BuildContext] associated with this [Video] widget if it is not already set or if the [Video] widget is in fullscreen mode.
// This is being done because the [Video] widget is rebuilt when it enters/exits fullscreen mode... & things don't work properly if we let [BuildContext] update in every rebuild.
if (widget.contextNotifier.value == null || isFullscreen(context)) {
widget.contextNotifier.value = context;
fallback[widget.state] ??= context;
}
return widget.child;
}
}
@@ -0,0 +1,163 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:media_kit_video/src/video_controller/video_controller.dart';
/// {@template subtitle_view}
///
/// SubtitleView
/// ------------
///
/// [SubtitleView] widget is used to display the subtitles on top of the [Video].
///
/// {@endtemplate}
class SubtitleView extends StatefulWidget {
/// The [VideoController] reference to control this [SubtitleView] output.
final VideoController controller;
/// The configuration to be used for the subtitles.
final SubtitleViewConfiguration configuration;
/// {@macro subtitle_view}
const SubtitleView({
super.key,
required this.controller,
required this.configuration,
});
@override
SubtitleViewState createState() => SubtitleViewState();
}
class SubtitleViewState extends State<SubtitleView> {
late List<String> subtitle = widget.controller.player.state.subtitle;
late EdgeInsets padding = widget.configuration.padding;
late Duration duration = const Duration(milliseconds: 100);
// The [StreamSubscription] to listen to the subtitle changes.
StreamSubscription<List<String>>? subscription;
// The reference width for calculating the visible text scale factor.
static const kTextScaleFactorReferenceWidth = 1920.0;
// The reference height for calculating the visible text scale factor.
static const kTextScaleFactorReferenceHeight = 1080.0;
@override
void initState() {
subscription = widget.controller.player.stream.subtitle.listen((value) {
setState(() {
subtitle = value;
});
});
super.initState();
}
@override
void dispose() {
subscription?.cancel();
super.dispose();
}
/// Sets the padding to be used for the subtitles.
///
/// The [duration] argument may be specified to set the duration of the animation.
void setPadding(
EdgeInsets padding, {
Duration duration = const Duration(milliseconds: 100),
}) {
if (this.duration != duration) {
setState(() {
this.duration = duration;
});
}
setState(() {
this.padding = padding;
});
}
/// {@macro subtitle_view}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
// Calculate the visible text scale factor.
final nr = (constraints.maxWidth * constraints.maxHeight);
const dr =
kTextScaleFactorReferenceWidth * kTextScaleFactorReferenceHeight;
final textScaleFactor = sqrt((nr / dr).clamp(0.0, 1.0));
final textScaler = widget.configuration.textScaler ??
TextScaler.linear(textScaleFactor);
return Material(
color: Colors.transparent,
child: AnimatedContainer(
padding: padding,
duration: duration,
alignment: Alignment.bottomCenter,
child: Text(
[
for (final line in subtitle)
if (line.trim().isNotEmpty) line.trim(),
].join('\n'),
style: widget.configuration.style,
textAlign: widget.configuration.textAlign,
textScaler: textScaler,
),
),
);
},
);
}
}
/// {@template subtitle_view_configuration}
/// SubtitleViewConfiguration
/// -------------------------
///
/// Configurable options for customizing the [SubtitleView] behaviour.
/// {@endtemplate}
class SubtitleViewConfiguration {
/// Whether the subtitles should be visible or not.
final bool visible;
/// The text style to be used for the subtitles.
final TextStyle style;
/// The text alignment to be used for the subtitles.
final TextAlign textAlign;
/// The text scaler to be used for the subtitles.
final TextScaler? textScaler;
/// The padding to be used for the subtitles.
final EdgeInsets padding;
/// {@macro subtitle_view_configuration}
const SubtitleViewConfiguration({
this.visible = true,
this.style = const TextStyle(
height: 1.4,
fontSize: 32.0,
letterSpacing: 0.0,
wordSpacing: 0.0,
color: Color(0xffffffff),
fontWeight: FontWeight.normal,
backgroundColor: Color(0xaa000000),
),
this.textAlign = TextAlign.center,
this.textScaler,
this.padding = const EdgeInsets.fromLTRB(
16.0,
0.0,
16.0,
24.0,
),
});
}
@@ -0,0 +1,12 @@
import 'package:flutter/widgets.dart';
class DisposeSafeNotifier<T> extends ValueNotifier<T> {
bool disposed = false;
DisposeSafeNotifier(super.value);
@override
void dispose() {
disposed = true;
super.dispose();
}
}
@@ -0,0 +1,68 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
import 'dart:ffi';
import 'dart:collection';
import 'package:synchronized/synchronized.dart';
// ignore_for_file: unused_import, implementation_imports
import 'package:media_kit/ffi/ffi.dart';
import 'package:media_kit/src/player/native/core/native_library.dart';
import 'package:media_kit/generated/libmpv/bindings.dart';
/// Returns the list of available decoders available in libavcodec.
/// Takes raw address to `mpv_handle*` as [handle].
///
/// https://mpv.io/manual/stable/#command-interface-decoder-list
Future<HashSet<String>> queryDecoders(int handle) {
return _lock.synchronized(() {
if (_decoders.isNotEmpty) {
return _decoders;
}
NativeLibrary.ensureInitialized();
final mpv = MPV(DynamicLibrary.open(NativeLibrary.path));
if (_decoders.isEmpty) {
final name = 'decoder-list'.toNativeUtf8();
final data = calloc<mpv_node>();
mpv.mpv_get_property(
Pointer.fromAddress(handle),
name.cast(),
mpv_format.MPV_FORMAT_NODE,
data.cast(),
);
if (data.ref.format == mpv_format.MPV_FORMAT_NODE_ARRAY) {
for (int i = 0; i < data.ref.u.list.ref.num; i++) {
final decoder = data.ref.u.list.ref.values[i];
if (decoder.format == mpv_format.MPV_FORMAT_NODE_MAP) {
String? name;
for (int j = 0; j < decoder.u.list.ref.num; j++) {
final k = decoder.u.list.ref.keys[j].cast<Utf8>().toDartString();
final v = decoder.u.list.ref.values[j];
if (k == 'codec' && v.format == mpv_format.MPV_FORMAT_STRING) {
name ??= v.u.string.cast<Utf8>().toDartString();
}
if (k == 'driver' && v.format == mpv_format.MPV_FORMAT_STRING) {
name ??= v.u.string.cast<Utf8>().toDartString();
}
}
if (name != null) {
_decoders.add(name);
}
}
}
}
mpv.mpv_free_node_contents(data);
calloc.free(name);
calloc.free(data);
}
return _decoders;
});
}
final Lock _lock = Lock();
final HashSet<String> _decoders = HashSet<String>();
+51
View File
@@ -0,0 +1,51 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Julien Muret <birros@protonmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
import 'package:wakelock_plus/wakelock_plus.dart';
/// {@template wakelock}
/// Wakelock
/// --------
///
/// This class acquires & releases the wakelock based on reference count.
///
/// {@endtemplate}
class Wakelock {
/// {@macro wakelock}
Wakelock();
/// Marks the wakelock as enabled for this instance.
void enable() {
if (!_enabled) {
_enabled = true;
_count++;
_update();
}
}
/// Marks the wakelock as disabled for this instance.
void disable() {
if (_enabled) {
_enabled = false;
_count--;
_update();
}
}
/// Acquires the wakelock if enabled count is greater than 0.
void _update() {
if (_count > 0) {
WakelockPlus.enable().catchError((_) {});
} else {
WakelockPlus.disable().catchError((_) {});
}
}
/// Whether the wakelock is enabled for this instance.
bool _enabled = false;
/// The number of [Wakelock] instances in enabled state.
static int _count = 0;
}
+7
View File
@@ -0,0 +1,7 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
export 'video_texture.dart' if (dart.library.js_interop) 'video_web.dart';
@@ -0,0 +1,534 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
import 'dart:io';
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:flutter/services.dart';
import 'package:media_kit_video/media_kit_video_controls/media_kit_video_controls.dart';
import 'package:media_kit_video/src/subtitle/subtitle_view.dart';
import 'package:media_kit_video/media_kit_video_controls/media_kit_video_controls.dart'
as media_kit_video_controls;
import 'package:media_kit_video/src/utils/dispose_safe_notifer.dart';
import 'package:media_kit_video/src/utils/wakelock.dart';
import 'package:media_kit_video/src/video_view_parameters.dart';
import 'package:media_kit_video/src/video_controller/video_controller.dart';
import 'package:media_kit_video/src/video_controller/platform_video_controller.dart';
/// {@template video}
///
/// Video
/// -----
/// [Video] widget is used to display video output.
///
/// Use [VideoController] to initialize & handle the video rendering.
///
/// **Example:**
///
/// ```dart
/// class MyScreen extends StatefulWidget {
/// const MyScreen({Key? key}) : super(key: key);
/// @override
/// State<MyScreen> createState() => MyScreenState();
/// }
///
/// class MyScreenState extends State<MyScreen> {
/// late final player = Player();
/// late final controller = VideoController(player);
///
/// @override
/// void initState() {
/// super.initState();
/// player.open(Media('https://user-images.githubusercontent.com/28951144/229373695-22f88f13-d18f-4288-9bf1-c3e078d83722.mp4'));
/// }
///
/// @override
/// void dispose() {
/// player.dispose();
/// super.dispose();
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: Video(
/// controller: controller,
/// ),
/// );
/// }
/// }
/// ```
///
/// {@endtemplate}
class Video extends StatefulWidget {
/// The [VideoController] reference to control this [Video] output.
final VideoController controller;
/// Width of this viewport.
final double? width;
/// Height of this viewport.
final double? height;
/// Fit of the viewport.
final BoxFit fit;
/// Background color to fill the video background.
final Color fill;
/// Alignment of the viewport.
final Alignment alignment;
/// Preferred aspect ratio of the viewport.
final double? aspectRatio;
/// Filter quality of the [Texture] widget displaying the video output.
final FilterQuality filterQuality;
/// Video controls builder.
final VideoControlsBuilder? controls;
/// Whether to acquire wake lock while playing the video.
final bool wakelock;
/// Whether to pause the video when application enters background mode.
final bool pauseUponEnteringBackgroundMode;
/// Whether to resume the video when application enters foreground mode.
///
/// This attribute is only applicable if [pauseUponEnteringBackgroundMode] is `true`.
///
final bool resumeUponEnteringForegroundMode;
/// The configuration for subtitles e.g. [TextStyle] & padding etc.
final SubtitleViewConfiguration subtitleViewConfiguration;
/// The callback invoked when the [Video] enters fullscreen.
final Future<void> Function() onEnterFullscreen;
/// The callback invoked when the [Video] exits fullscreen.
final Future<void> Function() onExitFullscreen;
/// FocusNode for keyboard input.
final FocusNode? focusNode;
/// {@macro video}
const Video({
super.key,
required this.controller,
this.width,
this.height,
this.fit = BoxFit.contain,
this.fill = const Color(0xFF000000),
this.alignment = Alignment.center,
this.aspectRatio,
this.filterQuality = FilterQuality.low,
this.controls = media_kit_video_controls.AdaptiveVideoControls,
this.wakelock = true,
this.pauseUponEnteringBackgroundMode = true,
this.resumeUponEnteringForegroundMode = false,
this.subtitleViewConfiguration = const SubtitleViewConfiguration(),
this.onEnterFullscreen = defaultEnterNativeFullscreen,
this.onExitFullscreen = defaultExitNativeFullscreen,
this.focusNode,
});
@override
State<Video> createState() => VideoState();
}
class VideoState extends State<Video> with WidgetsBindingObserver {
late final _contextNotifier = DisposeSafeNotifier<BuildContext?>(null);
late ValueNotifier<VideoViewParameters> videoViewParametersNotifier;
late bool _disposeNotifiers;
final _subtitleViewKey = GlobalKey<SubtitleViewState>();
final _wakelock = Wakelock();
final _subscriptions = <StreamSubscription>[];
late int? _width = widget.controller.player.state.width;
late int? _height = widget.controller.player.state.height;
late bool _visible = (_width ?? 0) > 0 && (_height ?? 0) > 0;
bool _pauseDueToPauseUponEnteringBackgroundMode = false;
// Public API:
bool isFullscreen() {
return media_kit_video_controls.isFullscreen(_contextNotifier.value!);
}
Future<void> enterFullscreen() {
return media_kit_video_controls.enterFullscreen(_contextNotifier.value!);
}
Future<void> exitFullscreen() {
return media_kit_video_controls.exitFullscreen(_contextNotifier.value!);
}
Future<void> toggleFullscreen() {
return media_kit_video_controls.toggleFullscreen(_contextNotifier.value!);
}
void setSubtitleViewPadding(
EdgeInsets padding, {
Duration duration = const Duration(milliseconds: 100),
}) {
return _subtitleViewKey.currentState?.setPadding(
padding,
duration: duration,
);
}
void update({
double? width,
double? height,
BoxFit? fit,
Color? fill,
Alignment? alignment,
double? aspectRatio,
FilterQuality? filterQuality,
VideoControlsBuilder? controls,
SubtitleViewConfiguration? subtitleViewConfiguration,
FocusNode? focusNode,
}) {
videoViewParametersNotifier.value =
videoViewParametersNotifier.value.copyWith(
width: width,
height: height,
fit: fit,
fill: fill,
alignment: alignment,
aspectRatio: aspectRatio,
filterQuality: filterQuality,
controls: controls,
subtitleViewConfiguration: subtitleViewConfiguration,
focusNode: focusNode,
);
}
@override
void didChangeDependencies() {
videoViewParametersNotifier =
media_kit_video_controls.VideoStateInheritedWidget.maybeOf(
context,
)?.videoViewParametersNotifier ??
ValueNotifier<VideoViewParameters>(
VideoViewParameters(
width: widget.width,
height: widget.height,
fit: widget.fit,
fill: widget.fill,
alignment: widget.alignment,
aspectRatio: widget.aspectRatio,
filterQuality: widget.filterQuality,
controls: widget.controls,
subtitleViewConfiguration: widget.subtitleViewConfiguration,
focusNode: widget.focusNode,
),
);
_disposeNotifiers =
media_kit_video_controls.VideoStateInheritedWidget.maybeOf(
context,
)?.disposeNotifiers ??
true;
super.didChangeDependencies();
}
@override
void didUpdateWidget(Video oldWidget) {
super.didUpdateWidget(oldWidget);
final currentParams = videoViewParametersNotifier.value;
final newParams = currentParams.copyWith(
width:
widget.width != oldWidget.width ? widget.width : currentParams.width,
height: widget.height != oldWidget.height
? widget.height
: currentParams.height,
fit: widget.fit != oldWidget.fit ? widget.fit : currentParams.fit,
fill: widget.fill != oldWidget.fill ? widget.fill : currentParams.fill,
alignment: widget.alignment != oldWidget.alignment
? widget.alignment
: currentParams.alignment,
aspectRatio: widget.aspectRatio != oldWidget.aspectRatio
? widget.aspectRatio
: currentParams.aspectRatio,
filterQuality: widget.filterQuality != oldWidget.filterQuality
? widget.filterQuality
: currentParams.filterQuality,
controls: widget.controls != oldWidget.controls
? widget.controls
: currentParams.controls,
subtitleViewConfiguration: widget.subtitleViewConfiguration !=
oldWidget.subtitleViewConfiguration
? widget.subtitleViewConfiguration
: currentParams.subtitleViewConfiguration,
focusNode: widget.focusNode != oldWidget.focusNode
? widget.focusNode
: currentParams.focusNode,
);
if (newParams != currentParams) {
WidgetsBinding.instance.addPostFrameCallback((_) {
videoViewParametersNotifier.value = newParams;
});
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (widget.pauseUponEnteringBackgroundMode) {
if ([
AppLifecycleState.paused,
AppLifecycleState.detached,
].contains(state)) {
if (widget.controller.player.state.playing) {
_pauseDueToPauseUponEnteringBackgroundMode = true;
widget.controller.player.pause();
}
} else {
if (widget.resumeUponEnteringForegroundMode &&
_pauseDueToPauseUponEnteringBackgroundMode) {
_pauseDueToPauseUponEnteringBackgroundMode = false;
widget.controller.player.play();
}
}
}
super.didChangeAppLifecycleState(state);
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// --------------------------------------------------
// Do not show the video frame until width & height are available.
// Since [ValueNotifier<Rect?>] inside [VideoController] only gets updated by the render loop (i.e. it will not fire when video's width & height are not available etc.), it's important to handle this separately here.
_subscriptions.addAll(
[
widget.controller.player.stream.width.listen(
(value) {
_width = value;
final visible = (_width ?? 0) > 0 && (_height ?? 0) > 0;
if (_visible != visible) {
setState(() {
_visible = visible;
});
}
},
),
widget.controller.player.stream.height.listen(
(value) {
_height = value;
final visible = (_width ?? 0) > 0 && (_height ?? 0) > 0;
if (_visible != visible) {
setState(() {
_visible = visible;
});
}
},
),
],
);
// --------------------------------------------------
if (widget.wakelock) {
if (widget.controller.player.state.playing) {
_wakelock.enable();
}
_subscriptions.add(
widget.controller.player.stream.playing.listen(
(value) {
if (value) {
_wakelock.enable();
} else {
_wakelock.disable();
}
},
),
);
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_wakelock.disable();
for (final subscription in _subscriptions) {
subscription.cancel();
}
if (_disposeNotifiers) {
videoViewParametersNotifier.dispose();
_contextNotifier.dispose();
VideoStateInheritedWidgetContextNotifierState.fallback.remove(this);
}
super.dispose();
}
void refreshView() {}
@override
Widget build(BuildContext context) {
return media_kit_video_controls.VideoStateInheritedWidget(
state: this as dynamic,
contextNotifier: _contextNotifier,
videoViewParametersNotifier: videoViewParametersNotifier,
child: ValueListenableBuilder<VideoViewParameters>(
valueListenable: videoViewParametersNotifier,
builder: (context, videoViewParameters, _) {
return Container(
clipBehavior: Clip.none,
width: videoViewParameters.width,
height: videoViewParameters.height,
color: videoViewParameters.fill,
child: Stack(
fit: StackFit.expand,
children: [
ClipRect(
child: FittedBox(
fit: videoViewParameters.fit,
alignment: videoViewParameters.alignment,
child: ValueListenableBuilder<PlatformVideoController?>(
valueListenable: widget.controller.notifier,
builder: (context, notifier, _) => notifier == null
? const SizedBox.shrink()
: ValueListenableBuilder<int?>(
valueListenable: notifier.id,
builder: (context, id, _) {
return ValueListenableBuilder<Rect?>(
valueListenable: notifier.rect,
builder: (context, rect, _) {
if (id != null &&
rect != null &&
_visible) {
return SizedBox(
// Apply aspect ratio if provided.
width:
videoViewParameters.aspectRatio ==
null
? rect.width
: rect.height *
videoViewParameters
.aspectRatio!,
height: rect.height,
child: Stack(
children: [
const SizedBox(),
Positioned.fill(
child: Texture(
textureId: id,
filterQuality:
videoViewParameters
.filterQuality,
),
),
// Keep the |Texture| hidden before the first frame renders. In native implementation, if no default frame size is passed (through VideoController), a starting 1 pixel sized texture/surface is created to initialize the render context & check for H/W support.
// This is then resized based on the video dimensions & accordingly texture ID, texture, EGLDisplay, EGLSurface etc. (depending upon platform) are also changed. Just don't show that 1 pixel texture to the UI.
// NOTE: Unmounting |Texture| causes the |MarkTextureFrameAvailable| to not do anything on GNU/Linux.
if (rect.width <= 1.0 &&
rect.height <= 1.0)
Positioned.fill(
child: Container(
color:
videoViewParameters.fill,
),
),
],
),
);
}
return const SizedBox.shrink();
},
);
},
),
),
),
),
if (videoViewParameters.subtitleViewConfiguration.visible &&
!(widget.controller.player.platform?.configuration.libass ??
false))
Positioned.fill(
child: SubtitleView(
controller: widget.controller,
key: _subtitleViewKey,
configuration:
videoViewParameters.subtitleViewConfiguration,
),
),
if (videoViewParameters.controls != null)
Positioned.fill(
child: videoViewParameters.controls!.call(this),
),
],
),
);
},
),
);
}
}
typedef VideoControlsBuilder = Widget Function(VideoState state);
// --------------------------------------------------
/// Makes the native window enter fullscreen.
Future<void> defaultEnterNativeFullscreen() async {
try {
if (Platform.isAndroid || Platform.isIOS) {
await Future.wait(
[
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.immersiveSticky,
overlays: [],
),
SystemChrome.setPreferredOrientations(
[
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
],
),
],
);
} else if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
await const MethodChannel('com.alexmercerind/media_kit_video')
.invokeMethod(
'Utils.EnterNativeFullscreen',
);
}
} catch (exception, stacktrace) {
debugPrint(exception.toString());
debugPrint(stacktrace.toString());
}
}
/// Makes the native window exit fullscreen.
Future<void> defaultExitNativeFullscreen() async {
try {
if (Platform.isAndroid || Platform.isIOS) {
await Future.wait(
[
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: SystemUiOverlay.values,
),
SystemChrome.setPreferredOrientations(
[],
),
],
);
} else if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
await const MethodChannel('com.alexmercerind/media_kit_video')
.invokeMethod(
'Utils.ExitNativeFullscreen',
);
}
} catch (exception, stacktrace) {
debugPrint(exception.toString());
debugPrint(stacktrace.toString());
}
}
// --------------------------------------------------
+483
View File
@@ -0,0 +1,483 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
// ignore_for_file: avoid_web_libraries_in_flutter
import 'dart:js_interop';
import 'package:web/web.dart';
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:media_kit_video/media_kit_video_controls/media_kit_video_controls.dart'
as media_kit_video_controls;
import 'package:media_kit_video/src/utils/dispose_safe_notifer.dart';
import 'package:media_kit_video/src/utils/wakelock.dart';
/// {@template video}
///
/// Video
/// -----
/// [Video] widget is used to display video output.
///
/// Use [VideoController] to initialize & handle the video rendering.
///
/// **Example:**
///
/// ```dart
/// class MyScreen extends StatefulWidget {
/// const MyScreen({Key? key}) : super(key: key);
/// @override
/// State<MyScreen> createState() => MyScreenState();
/// }
///
/// class MyScreenState extends State<MyScreen> {
/// late final player = Player();
/// late final controller = VideoController(player);
///
/// @override
/// void initState() {
/// super.initState();
/// player.open(Media('https://user-images.githubusercontent.com/28951144/229373695-22f88f13-d18f-4288-9bf1-c3e078d83722.mp4'));
/// }
///
/// @override
/// void dispose() {
/// player.dispose();
/// super.dispose();
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: Video(
/// controller: controller,
/// ),
/// );
/// }
/// }
/// ```
///
/// {@endtemplate}
class Video extends StatefulWidget {
/// The [VideoController] reference to control this [Video] output.
final VideoController controller;
/// Height of this viewport.
final double? width;
/// Width of this viewport.
final double? height;
/// Fit of the viewport.
final BoxFit fit;
/// Background color to fill the video background.
final Color fill;
/// Alignment of the viewport.
final Alignment alignment;
/// Preferred aspect ratio of the viewport.
final double? aspectRatio;
/// Filter quality of the [Texture] widget displaying the video output.
final FilterQuality filterQuality;
/// Video controls builder.
final /* VideoControlsBuilder? */ dynamic controls;
/// Whether to acquire wake lock while playing the video.
final bool wakelock;
/// Whether to pause the video when application enters background mode.
final bool pauseUponEnteringBackgroundMode;
/// Whether to resume the video when application enters foreground mode.
///
/// This attribute is only applicable if [pauseUponEnteringBackgroundMode] is `true`.
///
final bool resumeUponEnteringForegroundMode;
/// The configuration for subtitles e.g. [TextStyle] & padding etc.
final SubtitleViewConfiguration subtitleViewConfiguration;
/// The callback invoked when the [Video] enters fullscreen.
final Future<void> Function() onEnterFullscreen;
/// The callback invoked when the [Video] exits fullscreen.
final Future<void> Function() onExitFullscreen;
/// FocusNode for keyboard input.
final FocusNode? focusNode;
/// {@macro video}
const Video({
super.key,
required this.controller,
this.width,
this.height,
this.fit = BoxFit.contain,
this.fill = const Color(0xFF000000),
this.alignment = Alignment.center,
this.aspectRatio,
this.filterQuality = FilterQuality.low,
this.controls = media_kit_video_controls.AdaptiveVideoControls,
this.wakelock = true,
this.pauseUponEnteringBackgroundMode = true,
this.resumeUponEnteringForegroundMode = false,
this.subtitleViewConfiguration = const SubtitleViewConfiguration(),
this.onEnterFullscreen = defaultEnterNativeFullscreen,
this.onExitFullscreen = defaultExitNativeFullscreen,
this.focusNode,
});
@override
State<Video> createState() => VideoState();
}
class VideoState extends State<Video> with WidgetsBindingObserver {
late final _contextNotifier = DisposeSafeNotifier<BuildContext?>(null);
late ValueNotifier<VideoViewParameters> videoViewParametersNotifier;
late bool _disposeNotifiers;
final _subtitleViewKey = GlobalKey<SubtitleViewState>();
final _wakelock = Wakelock();
final _subscriptions = <StreamSubscription>[];
late int? _width = widget.controller.player.state.width;
late int? _height = widget.controller.player.state.height;
late bool _visible = (_width ?? 0) > 0 && (_height ?? 0) > 0;
bool _pauseDueToPauseUponEnteringBackgroundMode = false;
ValueKey _key = const ValueKey(true);
// Public API:
bool isFullscreen() {
return media_kit_video_controls.isFullscreen(_contextNotifier.value!);
}
Future<void> enterFullscreen() {
return media_kit_video_controls.enterFullscreen(_contextNotifier.value!);
}
Future<void> exitFullscreen() {
return media_kit_video_controls.exitFullscreen(_contextNotifier.value!);
}
Future<void> toggleFullscreen() {
return media_kit_video_controls.toggleFullscreen(_contextNotifier.value!);
}
void setSubtitleViewPadding(
EdgeInsets padding, {
Duration duration = const Duration(milliseconds: 100),
}) {
return _subtitleViewKey.currentState?.setPadding(
padding,
duration: duration,
);
}
void update({
double? width,
double? height,
BoxFit? fit,
Color? fill,
Alignment? alignment,
double? aspectRatio,
FilterQuality? filterQuality,
/* VideoControlsBuilder? */ dynamic controls,
SubtitleViewConfiguration? subtitleViewConfiguration,
FocusNode? focusNode,
}) {
videoViewParametersNotifier.value =
videoViewParametersNotifier.value.copyWith(
width: width,
height: height,
fit: fit,
fill: fill,
alignment: alignment,
aspectRatio: aspectRatio,
filterQuality: filterQuality,
controls: controls,
subtitleViewConfiguration: subtitleViewConfiguration,
focusNode: focusNode,
);
}
@override
void didUpdateWidget(Video oldWidget) {
super.didUpdateWidget(oldWidget);
final currentParams = videoViewParametersNotifier.value;
final newParams = currentParams.copyWith(
width:
widget.width != oldWidget.width ? widget.width : currentParams.width,
height: widget.height != oldWidget.height
? widget.height
: currentParams.height,
fit: widget.fit != oldWidget.fit ? widget.fit : currentParams.fit,
fill: widget.fill != oldWidget.fill ? widget.fill : currentParams.fill,
alignment: widget.alignment != oldWidget.alignment
? widget.alignment
: currentParams.alignment,
aspectRatio: widget.aspectRatio != oldWidget.aspectRatio
? widget.aspectRatio
: currentParams.aspectRatio,
filterQuality: widget.filterQuality != oldWidget.filterQuality
? widget.filterQuality
: currentParams.filterQuality,
controls: widget.controls != oldWidget.controls
? widget.controls
: currentParams.controls,
subtitleViewConfiguration: widget.subtitleViewConfiguration !=
oldWidget.subtitleViewConfiguration
? widget.subtitleViewConfiguration
: currentParams.subtitleViewConfiguration,
focusNode: widget.focusNode != oldWidget.focusNode
? widget.focusNode
: currentParams.focusNode,
);
if (newParams != currentParams) {
WidgetsBinding.instance.addPostFrameCallback((_) {
videoViewParametersNotifier.value = newParams;
});
}
}
@override
void didChangeDependencies() {
videoViewParametersNotifier =
media_kit_video_controls.VideoStateInheritedWidget.maybeOf(
context,
)?.videoViewParametersNotifier ??
ValueNotifier<VideoViewParameters>(
VideoViewParameters(
width: widget.width,
height: widget.height,
fit: widget.fit,
fill: widget.fill,
alignment: widget.alignment,
aspectRatio: widget.aspectRatio,
filterQuality: widget.filterQuality,
controls: widget.controls,
subtitleViewConfiguration: widget.subtitleViewConfiguration,
focusNode: widget.focusNode,
),
);
_disposeNotifiers =
media_kit_video_controls.VideoStateInheritedWidget.maybeOf(
context,
)?.disposeNotifiers ??
true;
super.didChangeDependencies();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (widget.pauseUponEnteringBackgroundMode) {
if ([
AppLifecycleState.paused,
AppLifecycleState.detached,
].contains(state)) {
if (widget.controller.player.state.playing) {
_pauseDueToPauseUponEnteringBackgroundMode = true;
widget.controller.player.pause();
}
} else {
if (widget.resumeUponEnteringForegroundMode &&
_pauseDueToPauseUponEnteringBackgroundMode) {
_pauseDueToPauseUponEnteringBackgroundMode = false;
widget.controller.player.play();
}
}
}
super.didChangeAppLifecycleState(state);
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// --------------------------------------------------
// Do not show the video frame until width & height are available.
// Since [ValueNotifier<Rect?>] inside [VideoController] only gets updated by the render loop (i.e. it will not fire when video's width & height are not available etc.), it's important to handle this separately here.
_subscriptions.addAll(
[
widget.controller.player.stream.width.listen(
(value) {
_width = value;
final visible = (_width ?? 0) > 0 && (_height ?? 0) > 0;
if (_visible != visible) {
setState(() {
_visible = visible;
});
}
},
),
widget.controller.player.stream.height.listen(
(value) {
_height = value;
final visible = (_width ?? 0) > 0 && (_height ?? 0) > 0;
if (_visible != visible) {
setState(() {
_visible = visible;
});
}
},
),
],
);
// --------------------------------------------------
if (widget.wakelock) {
if (widget.controller.player.state.playing) {
_wakelock.enable();
}
_subscriptions.add(
widget.controller.player.stream.playing.listen(
(value) {
if (value) {
_wakelock.enable();
} else {
_wakelock.disable();
}
},
),
);
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_wakelock.disable();
for (final subscription in _subscriptions) {
subscription.cancel();
}
if (_disposeNotifiers) {
videoViewParametersNotifier.dispose();
_contextNotifier.dispose();
// ignore: collection_methods_unrelated_type
VideoStateInheritedWidgetContextNotifierState.fallback.remove(this);
}
super.dispose();
}
void refreshView() {
setState(() {
_key = ValueKey(!_key.value);
});
}
@override
Widget build(BuildContext context) {
return media_kit_video_controls.VideoStateInheritedWidget(
state: this as dynamic,
contextNotifier: _contextNotifier,
videoViewParametersNotifier: videoViewParametersNotifier,
child: ValueListenableBuilder<VideoViewParameters>(
valueListenable: videoViewParametersNotifier,
builder: (context, videoViewParameters, _) {
return Container(
clipBehavior: Clip.none,
width: videoViewParameters.width,
height: videoViewParameters.height,
color: videoViewParameters.fill,
child: Stack(
fit: StackFit.expand,
children: [
ClipRect(
child: FittedBox(
fit: videoViewParameters.fit,
alignment: videoViewParameters.alignment,
child: ValueListenableBuilder<PlatformVideoController?>(
valueListenable: widget.controller.notifier,
builder: (context, notifier, _) => notifier == null
? const SizedBox.shrink()
: ValueListenableBuilder<int?>(
valueListenable: notifier.id,
builder: (context, id, _) {
return ValueListenableBuilder<Rect?>(
valueListenable: notifier.rect,
builder: (context, rect, _) {
if (id != null &&
rect != null &&
_visible) {
return SizedBox(
// Apply aspect ratio if provided.
width:
videoViewParameters.aspectRatio ==
null
? rect.width
: rect.height *
videoViewParameters
.aspectRatio!,
height: rect.height,
child: HtmlElementView(
key: _key,
viewType:
'com.alexmercerind.media_kit_video.$id',
),
);
}
return const SizedBox.shrink();
},
);
},
),
),
),
),
if (videoViewParameters.subtitleViewConfiguration.visible &&
!(widget.controller.player.platform?.configuration.libass ??
false))
Positioned.fill(
child: SubtitleView(
controller: widget.controller,
key: _subtitleViewKey,
configuration:
videoViewParameters.subtitleViewConfiguration,
),
),
if (videoViewParameters.controls != null)
Positioned.fill(
child: videoViewParameters.controls!.call(this as dynamic),
),
],
),
);
},
),
);
}
}
// --------------------------------------------------
/// Makes the native window enter fullscreen.
Future<void> defaultEnterNativeFullscreen() async {
try {
await document.documentElement?.requestFullscreen().toDart;
} catch (exception, stacktrace) {
debugPrint(exception.toString());
debugPrint(stacktrace.toString());
}
}
/// Makes the native window exit fullscreen.
Future<void> defaultExitNativeFullscreen() async {
try {
await document.exitFullscreen().toDart;
} catch (exception, stacktrace) {
debugPrint(exception.toString());
debugPrint(stacktrace.toString());
}
}
// --------------------------------------------------
@@ -0,0 +1,6 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
export 'real.dart' if (dart.library.js_interop) 'stub.dart';
@@ -0,0 +1,299 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
import 'dart:io';
import 'dart:async';
import 'dart:collection';
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart';
import 'package:synchronized/synchronized.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/src/utils/query_decoders.dart';
import 'package:media_kit_video/src/video_controller/platform_video_controller.dart';
/// {@template android_video_controller}
///
/// AndroidVideoController
/// ----------------------
///
/// The [PlatformVideoController] implementation based on native JNI & C/C++ used on Android.
///
/// {@endtemplate}
class AndroidVideoController extends PlatformVideoController {
/// Whether [AndroidVideoController] is supported on the current platform or not.
static bool get supported => Platform.isAndroid;
/// Pointer address to the global object reference of `android.view.Surface` i.e. `(intptr_t)(*android.view.Surface)`.
final ValueNotifier<int?> wid = ValueNotifier<int?>(null);
/// [Lock] used to synchronize [onLoadHooks], [onUnloadHooks] & [subscription].
final lock = Lock();
NativePlayer get platform => player.platform as NativePlayer;
Future<void> setProperty(String key, String value) async {
await platform.setProperty(key, value, waitForInitialization: false);
}
Future<void> setProperties(Map<String, String> properties) async {
for (final entry in properties.entries) {
await setProperty(entry.key, entry.value);
}
}
/// Listener for updating the --wid property.
Future<void> widListener() {
return lock.synchronized(() async {
final width = rect.value?.width.toInt() ?? 1;
final height = rect.value?.height.toInt() ?? 1;
final androidSurfaceSizeValue = [width, height].join('x');
final widValue = wid.value?.toString() ?? '0';
// When --wid is 0, vo=null is required to avoid SIGSEGV.
final voValue = widValue == '0' ? 'null' : configuration.vo!;
final vidValue = widValue == '0' ? 'no' : 'auto';
// It is important to re-initialize --vo after --android-surface-size.
await setProperty('vo', 'null');
await setProperties(
{
// ORDER IS IMPORTANT.
'android-surface-size': androidSurfaceSizeValue,
'wid': widValue,
'vo': voValue,
// It is important to re-initialize --vid in-case of --vo=mediacodec_embed.
// Not doing so causes error "Could not open codec." & video never gets rendered.
if (configuration.vo == 'mediacodec_embed') 'vid': vidValue,
},
);
// Instead of seeking to the start (Duration.zero), seek to the current playback position
// without jumping the user to the start of the media.
final currentPosition = player.state.position;
await player.seek(currentPosition);
});
}
/// [StreamSubscription] for listening to video [Rect].
StreamSubscription<VideoParams>? videoParamsSubscription;
/// {@macro android_video_controller}
AndroidVideoController._(
super.player,
super.configuration,
) {
wid.addListener(widListener);
videoParamsSubscription = player.stream.videoParams.listen(
(event) => lock.synchronized(() async {
final int width;
final int height;
if (event.rotate == 0 || event.rotate == 180) {
width = event.dw ?? 0;
height = event.dh ?? 0;
} else {
// width & height are swapped for 90 or 270 degrees rotation.
width = event.dh ?? 0;
height = event.dw ?? 0;
}
final isZero = width == 0 || height == 0;
final isSame = width == rect.value?.width.toInt() &&
height == rect.value?.height.toInt();
if (isZero || isSame) {
return;
}
final handle = await player.handle;
await _channel.invokeMethod(
'VideoOutputManager.SetSurfaceSize',
{
'handle': handle.toString(),
'width': width.toString(),
'height': height.toString(),
},
);
rect.value = Rect.fromLTWH(
0.0,
0.0,
width.toDouble(),
height.toDouble(),
);
if (!waitUntilFirstFrameRenderedCompleter.isCompleted) {
waitUntilFirstFrameRenderedCompleter.complete();
}
}),
);
}
/// {@macro android_video_controller}
static Future<PlatformVideoController> create(
Player player,
VideoControllerConfiguration configuration,
) async {
Future<String> getDefaultHwdec() async {
// Enforce software rendering in emulators.
bool hw = configuration.enableHardwareAcceleration;
final bool isEmulator = await _channel.invokeMethod('Utils.IsEmulator');
if (isEmulator) {
hw = false;
debugPrint('media_kit: Emulator detected.');
debugPrint('media_kit: Enforcing S/W rendering.');
}
return hw ? 'auto-safe' : 'no';
}
// Update [configuration] to have default values.
configuration = configuration.copyWith(
vo: configuration.vo ?? 'gpu',
hwdec: configuration.hwdec ?? await getDefaultHwdec(),
);
// Retrieve the native handle of the [Player].
final handle = await player.handle;
// Return the existing [VideoController] if it's already created.
if (_controllers.containsKey(handle)) {
return _controllers[handle]!;
}
// In case no video-decoders are found, this means media_kit_libs_***_audio is being used.
// Thus, --vid=no is required to prevent libmpv from trying to decode video (otherwise bad things may happen).
//
// Search for common H264 decoder to check if video support is available.
final decoders = await queryDecoders(handle);
if (!decoders.contains('h264')) {
throw UnsupportedError(
'[VideoController] is not available.'
' '
'Please use media_kit_libs_***_video instead of media_kit_libs_***_audio.',
);
}
// Creation:
final controller = AndroidVideoController._(
player,
configuration,
);
// Register [_dispose] for execution upon [Player.dispose].
player.platform?.release.add(controller._dispose);
// Store the [VideoController] in the [_controllers].
_controllers[handle] = controller;
await _channel.invokeMethod(
'VideoOutputManager.Create',
{
'handle': handle.toString(),
},
);
await controller.setProperties(
{
// It is necessary to set vo=null here to avoid SIGSEGV, --wid must be assigned before vo=gpu is set.
'vo': 'null',
'hwdec': configuration.hwdec!,
'vid': 'auto',
'opengl-es': 'yes',
'force-window': 'yes',
'gpu-context': 'android',
'sub-use-margins': 'no',
'sub-font-provider': 'none',
'sub-scale-with-window': 'yes',
'hwdec-codecs': 'h264,hevc,mpeg4,mpeg2video,vp8,vp9,av1',
},
);
// Return the [PlatformVideoController].
return controller;
}
/// Sets the required size of the video output.
/// This may yield substantial performance improvements if a small [width] & [height] is specified.
///
/// Remember:
/// * “Premature optimization is the root of all evil”
/// * “With great power comes great responsibility”
@override
Future<void> setSize({
int? width,
int? height,
}) {
throw UnsupportedError(
'[AndroidVideoController.setSize] is not available on Android',
);
}
/// Disposes the instance. Releases allocated resources back to the system.
Future<void> _dispose() async {
super.dispose();
wid.dispose();
wid.removeListener(widListener);
await videoParamsSubscription?.cancel();
final handle = await player.handle;
_controllers.remove(handle);
await _channel.invokeMethod(
'VideoOutputManager.Dispose',
{
'handle': handle.toString(),
},
);
}
/// Currently created [AndroidVideoController]s.
static final _controllers = HashMap<int, AndroidVideoController>();
/// [MethodChannel] for invoking platform specific native implementation.
static final _channel =
const MethodChannel('com.alexmercerind/media_kit_video')
..setMethodCallHandler(
(MethodCall call) async {
try {
debugPrint(call.method.toString());
debugPrint(call.arguments.toString());
switch (call.method) {
case 'VideoOutput.Resize':
{
// Notify about updated texture ID & [Rect].
final int handle = call.arguments['handle'];
final Rect rect = Rect.fromLTWH(
call.arguments['rect']['left'] * 1.0,
call.arguments['rect']['top'] * 1.0,
call.arguments['rect']['width'] * 1.0,
call.arguments['rect']['height'] * 1.0,
);
final int id = call.arguments['id'];
final int wid = call.arguments['wid'];
_controllers[handle]?.rect.value = rect;
_controllers[handle]?.id.value = id;
_controllers[handle]?.wid.value = wid;
break;
}
case 'VideoOutput.WaitUntilFirstFrameRenderedNotify':
{
// Notify about updated texture ID & [Rect].
final int handle = call.arguments['handle'];
debugPrint(handle.toString());
// Notify about the first frame being rendered.
final completer = _controllers[handle]
?.waitUntilFirstFrameRenderedCompleter;
if (!(completer?.isCompleted ?? true)) {
completer?.complete();
}
break;
}
default:
{
break;
}
}
} catch (exception, stacktrace) {
debugPrint(exception.toString());
debugPrint(stacktrace.toString());
}
},
);
}
@@ -0,0 +1,28 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/src/video_controller/platform_video_controller.dart';
// Stub declaration for avoiding compilation errors on Dart JS using conditional imports.
class AndroidVideoController extends PlatformVideoController {
static const bool supported = false;
AndroidVideoController._(
super.player,
super.configuration,
);
static Future<PlatformVideoController> create(
Player player,
VideoControllerConfiguration configuration,
) =>
throw UnimplementedError();
@override
Future<void> setSize({int? width, int? height}) => throw UnimplementedError();
}
@@ -0,0 +1,6 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
export 'real.dart' if (dart.library.js_interop) 'stub.dart';
@@ -0,0 +1,299 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
import 'dart:io';
import 'dart:async';
import 'dart:collection';
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart';
import 'package:synchronized/synchronized.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/src/utils/query_decoders.dart';
import 'package:media_kit_video/src/video_controller/platform_video_controller.dart';
/// {@template native_video_controller}
///
/// NativeVideoController
/// ---------------------
///
/// The [PlatformVideoController] implementation based on native C/C++ used on:
/// * Windows
/// * GNU/Linux
/// * macOS
/// * iOS
///
/// {@endtemplate}
class NativeVideoController extends PlatformVideoController {
/// Whether [NativeVideoController] is supported on the current platform or not.
static bool get supported =>
Platform.isWindows ||
Platform.isLinux ||
Platform.isMacOS ||
Platform.isIOS;
/// Fixed width of the video output.
int? width;
/// Fixed height of the video output.
int? height;
/// Width of the video (from [VideoParams]).
int? videoParamsWidth;
/// Height of the video (from [VideoParams]).
int? videoParamsHeight;
/// [Lock] used to synchronize [onLoadHooks], [onUnloadHooks] & [subscription].
final lock = Lock();
NativePlayer get platform => player.platform as NativePlayer;
Future<void> setProperty(String key, String value) async {
await platform.setProperty(key, value, waitForInitialization: false);
}
Future<void> setProperties(Map<String, String> properties) async {
// ORDER IS IMPORTANT.
for (final entry in properties.entries) {
await setProperty(entry.key, entry.value);
}
}
/// [StreamSubscription] for listening to video [Rect].
StreamSubscription<VideoParams>? videoParamsSubscription;
/// {@macro native_video_controller}
NativeVideoController._(
super.player,
super.configuration,
) : width = configuration.width,
height = configuration.height {
videoParamsSubscription = player.stream.videoParams.listen(
(event) => lock.synchronized(() async {
if ([0, null].contains(event.dw) || [0, null].contains(event.dh)) {
return;
}
final int handle = await player.handle;
final int width;
final int height;
if (event.rotate == 0 || event.rotate == 180) {
width = event.dw ?? 0;
height = event.dh ?? 0;
} else {
// width & height are swapped for 90 or 270 degrees rotation.
width = event.dh ?? 0;
height = event.dw ?? 0;
}
if (videoParamsWidth == width && videoParamsHeight == height) {
return;
}
videoParamsWidth = width;
videoParamsHeight = height;
await _channel.invokeMethod(
'VideoOutputManager.SetSize',
{
'handle': handle.toString(),
'width': width.toString(),
'height': height.toString(),
},
);
}),
);
}
/// {@macro native_video_controller}
static Future<PlatformVideoController> create(
Player player,
VideoControllerConfiguration configuration,
) async {
// Update [configuration] to have default values.
configuration = configuration.copyWith(
vo: configuration.vo ?? 'libmpv',
hwdec: configuration.hwdec ?? 'auto',
);
// Retrieve the native handle of the [Player].
final handle = await player.handle;
// Return the existing [VideoController] if it's already created.
if (_controllers.containsKey(handle)) {
return _controllers[handle]!;
}
// In case no video-decoders are found, this means media_kit_libs_***_audio is being used.
// Thus, --vid=no is required to prevent libmpv from trying to decode video (otherwise bad things may happen).
//
// Search for common H264 decoder to check if video support is available.
final decoders = await queryDecoders(handle);
if (!decoders.contains('h264')) {
throw UnsupportedError(
'[VideoController] is not available.'
' '
'Please use media_kit_libs_***_video instead of media_kit_libs_***_audio.',
);
}
// Creation:
final controller = NativeVideoController._(
player,
configuration,
);
// Register [_dispose] for execution upon [Player.dispose].
player.platform?.release.add(controller._dispose);
// Store the [NativeVideoController] in the [_controllers].
_controllers[handle] = controller;
await controller.setProperties(
{
'vo': configuration.vo!,
'hwdec': configuration.hwdec!,
'vid': 'auto',
},
);
// Wait until first texture ID is received.
// We are not waiting on the native-side itself because it will block the UI thread.
final completer = Completer<void>();
void listener() {
final value = controller.id.value;
if (value != null) {
debugPrint('NativeVideoController: Texture ID: $value');
completer.complete();
}
}
controller.id.addListener(listener);
await _channel.invokeMethod(
'VideoOutputManager.Create',
{
'handle': handle.toString(),
'configuration': {
'width': configuration.width.toString(),
'height': configuration.height.toString(),
'enableHardwareAcceleration':
configuration.enableHardwareAcceleration,
},
},
);
await completer.future;
controller.id.removeListener(listener);
// Return the [VideoController].
return controller;
}
/// Sets the required size of the video output.
/// This may yield substantial performance improvements if a small [width] & [height] is specified.
///
/// Remember:
/// * “Premature optimization is the root of all evil”
/// * “With great power comes great responsibility”
@override
Future<void> setSize({
int? width,
int? height,
}) async {
final handle = await player.handle;
if (this.width == width && this.height == height) {
// No need to resize if the requested size is same as the current size.
return;
}
if (width != null && height != null) {
this.width = width;
this.height = height;
await _channel.invokeMethod(
'VideoOutputManager.SetSize',
{
'handle': handle.toString(),
'width': width.toString(),
'height': height.toString(),
},
);
} else {
this.width = null;
this.height = null;
await _channel.invokeMethod(
'VideoOutputManager.SetSize',
{
'handle': handle.toString(),
'width': videoParamsWidth?.toString() ?? 'null',
'height': videoParamsHeight?.toString() ?? 'null',
},
);
}
}
/// Disposes the instance. Releases allocated resources back to the system.
Future<void> _dispose() async {
super.dispose();
await videoParamsSubscription?.cancel();
final handle = await player.handle;
_controllers.remove(handle);
await _channel.invokeMethod(
'VideoOutputManager.Dispose',
{
'handle': handle.toString(),
},
);
}
/// Currently created [NativeVideoController]s.
/// This is used to notify about updated texture IDs & [Rect]s through [_channel].
static final _controllers = HashMap<int, NativeVideoController>();
/// [MethodChannel] for invoking platform specific native implementation.
static final _channel =
const MethodChannel('com.alexmercerind/media_kit_video')
..setMethodCallHandler(
(MethodCall call) async {
try {
debugPrint(call.method.toString());
debugPrint(call.arguments.toString());
switch (call.method) {
case 'VideoOutput.Resize':
{
// Notify about updated texture ID & [Rect].
final int handle = call.arguments['handle'];
final Rect rect = Rect.fromLTWH(
call.arguments['rect']['left'] * 1.0,
call.arguments['rect']['top'] * 1.0,
call.arguments['rect']['width'] * 1.0,
call.arguments['rect']['height'] * 1.0,
);
final int id = call.arguments['id'];
_controllers[handle]?.rect.value = rect;
_controllers[handle]?.id.value = id;
// Notify about the first frame being rendered.
if (rect.width > 0 && rect.height > 0) {
final completer = _controllers[handle]
?.waitUntilFirstFrameRenderedCompleter;
if (!(completer?.isCompleted ?? true)) {
completer?.complete();
}
}
break;
}
default:
{
break;
}
}
} catch (exception, stacktrace) {
debugPrint(exception.toString());
debugPrint(stacktrace.toString());
}
},
);
}
@@ -0,0 +1,28 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/src/video_controller/platform_video_controller.dart';
// Stub declaration for avoiding compilation errors on Dart JS using conditional imports.
class NativeVideoController extends PlatformVideoController {
static const bool supported = false;
NativeVideoController._(
super.player,
super.configuration,
);
static Future<PlatformVideoController> create(
Player player,
VideoControllerConfiguration configuration,
) =>
throw UnimplementedError();
@override
Future<void> setSize({int? width, int? height}) => throw UnimplementedError();
}
@@ -0,0 +1,157 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/src/video_controller/video_controller.dart';
/// {@template platform_video_controller}
///
/// PlatformVideoController
/// -----------------------
///
/// This class provides the interface for platform specific [VideoController] implementations.
/// The platform specific implementations are expected to implement the methods accordingly.
///
/// The subclasses are then used in composition with the [VideoController] class, based on the platform the application is running on.
///
/// {@endtemplate}
abstract class PlatformVideoController {
/// The [Player] instance associated with this instance.
final Player player;
/// User defined configuration for [VideoController].
final VideoControllerConfiguration configuration;
/// Texture ID of the video output, registered with Flutter engine by the native implementation.
final ValueNotifier<int?> id = ValueNotifier<int?>(null);
/// [Rect] of the video output, received from the native implementation.
final ValueNotifier<Rect?> rect = ValueNotifier<Rect?>(null);
/// {@macro platform_video_controller}
PlatformVideoController(
this.player,
this.configuration,
);
/// Sets the required size of the video output.
/// This may yield substantial performance improvements if a small [width] & [height] is specified.
///
/// Remember:
/// * “Premature optimization is the root of all evil”
/// * “With great power comes great responsibility”
Future<void> setSize({
int? width,
int? height,
});
/// A [Future] that completes when the first video frame has been rendered.
Future<void> get waitUntilFirstFrameRendered =>
waitUntilFirstFrameRenderedCompleter.future;
/// [Completer] used to signal the decoding & rendering of the first video frame.
/// Use [waitUntilFirstFrameRendered] to wait for the first frame to be rendered.
@protected
final waitUntilFirstFrameRenderedCompleter = Completer<void>();
void dispose() {
id.dispose();
rect.dispose();
}
}
/// {@template video_controller_configuration}
///
/// VideoControllerConfiguration
/// ----------------------------
/// Configurable options for customizing the [VideoController] behavior.
///
/// {@endtemplate}
class VideoControllerConfiguration {
/// Sets the [`--vo`](https://mpv.io/manual/stable/#options-vo) property on native backend.
///
/// Default: Platform specific.
/// * Windows, GNU/Linux, macOS & iOS: `libmpv`
/// * Android: `gpu`
final String? vo;
/// Sets the [`--hwdec`](https://mpv.io/manual/stable/#options-hwdec) property on native backend.
///
/// Default: Platform specific.
/// * Windows, GNU/Linux, macOS & iOS : `auto`
/// * Android: `auto-safe`
final String? hwdec;
/// The scale for the video output.
/// This may be used for performance reasons. Specifying this option will cause [width] & [height] to be ignored.
///
/// Default: `1.0`
final double scale;
/// The fixed width for the video output.
/// This may be used for performance reasons.
///
/// Default: `null`
final int? width;
/// The fixed height for the video output.
/// This may be used for performance reasons.
///
/// Default: `null`
final int? height;
/// Whether to enable hardware acceleration.
///
/// DO NOT DISABLE THIS OPTION MEANINGLESSLY.
/// THE BATTERY WILL DRAIN, THE DEVICE MAY HEAT UP & CPU USAGE WILL BE HIGH.
///
/// Default: `true`
final bool enableHardwareAcceleration;
/// Whether to attach `android.view.Surface` after video parameters are known.
///
/// Default:
/// * [vo] == gpu : `true`
/// * [vo] != gpu : `false`
final bool? androidAttachSurfaceAfterVideoParameters;
/// {@macro video_controller_configuration}
const VideoControllerConfiguration({
this.vo,
this.hwdec,
this.width,
this.height,
this.scale = 1.0,
this.enableHardwareAcceleration = true,
this.androidAttachSurfaceAfterVideoParameters,
});
/// Returns a copy of this class with the given fields replaced by the new values.
VideoControllerConfiguration copyWith({
String? vo,
String? hwdec,
double? scale,
int? width,
int? height,
bool? enableHardwareAcceleration,
bool? androidAttachSurfaceAfterVideoParameters,
}) =>
VideoControllerConfiguration(
vo: vo ?? this.vo,
hwdec: hwdec ?? this.hwdec,
scale: scale ?? this.scale,
width: width ?? this.width,
height: height ?? this.height,
enableHardwareAcceleration:
enableHardwareAcceleration ?? this.enableHardwareAcceleration,
androidAttachSurfaceAfterVideoParameters:
androidAttachSurfaceAfterVideoParameters ??
this.androidAttachSurfaceAfterVideoParameters,
);
}
@@ -0,0 +1,164 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/src/video_controller/platform_video_controller.dart';
import 'package:media_kit_video/src/video_controller/native_video_controller/native_video_controller.dart';
import 'package:media_kit_video/src/video_controller/android_video_controller/android_video_controller.dart';
import 'package:media_kit_video/src/video_controller/web_video_controller/web_video_controller.dart';
/// {@template video_controller}
///
/// VideoController
/// ---------------
///
/// [VideoController] is used to initialize & display video output.
/// It takes reference to existing [Player] instance from `package:media_kit`.
///
/// Passing [VideoController] to [Video] widget will cause the video output to be displayed.
///
/// ```dart
/// late final player = Player();
/// late final controller = VideoController(player);
/// ```
///
/// **Configurable options:**
///
/// 1. You can limit size of the video output by specifying [VideoControllerConfiguration.width] & [VideoControllerConfiguration.height].
/// * A smaller width & height may yield substantial performance improvements.
/// * By default, both [VideoControllerConfiguration.height] & [VideoControllerConfiguration.width] are `null` i.e. output is based on video's resolution.
/// 2. You can reduce scale of the video output by specifying [VideoControllerConfiguration.scale].
/// * A smaller scale may yield substantial performance improvements. Specifying this value will cause [VideoControllerConfiguration.width] & [VideoControllerConfiguration.height] to be ignored.
/// * By default, [VideoControllerConfiguration.scale] is `1.0` i.e. output is based on video's resolution.
/// 3. You can switch between GPU & CPU rendering by specifying [VideoControllerConfiguration.enableHardwareAcceleration].
/// * Disabling the option may improve stability on certain devices.
/// * By default, [VideoControllerConfiguration.enableHardwareAcceleration] is `true` i.e. GPU (Direct3D/OpenGL/METAL) is utilized.
///
/// **Platform specific limitations & differences:**
///
/// **Android**
/// * [VideoControllerConfiguration.width] & [VideoControllerConfiguration.height] arguments have no effect.
///
/// **Web**
/// * [VideoControllerConfiguration.width] & [VideoControllerConfiguration.height] arguments have no effect.
/// * Only single [Video] output can be displayed for a [VideoController].
/// Displaying multiple [Video] widgets with same [VideoController] will cause only last mounted [Video] to be displayed.
/// * [VideoControllerConfiguration.enableHardwareAcceleration] is ignored i.e. GPU usage is dependent upon the web browser.
///
/// {@endtemplate}
class VideoController {
/// The [Player] instance associated with this [VideoController].
final Player player;
/// Platform specific internal implementation initialized depending upon the current platform.
final platform = Completer<PlatformVideoController>();
/// Platform specific internal implementation initialized depending upon the current platform.
final notifier = ValueNotifier<PlatformVideoController?>(null);
/// Texture ID of the video output, registered with Flutter engine by the native implementation.
final ValueNotifier<int?> id = ValueNotifier<int?>(null);
/// [Rect] of the video output, received from the native implementation.
final ValueNotifier<Rect?> rect = ValueNotifier<Rect?>(null);
/// {@macro video_controller}
VideoController(
this.player, {
VideoControllerConfiguration configuration =
const VideoControllerConfiguration(),
}) {
player.platform?.isVideoControllerAttached = true;
() async {
final completer = Completer();
WidgetsBinding.instance.addPostFrameCallback((_) => completer.complete());
await completer.future;
try {
if (NativeVideoController.supported) {
final result = await NativeVideoController.create(
player,
configuration,
);
platform.complete(result);
notifier.value = result;
} else if (AndroidVideoController.supported) {
final result = await AndroidVideoController.create(
player,
configuration,
);
platform.complete(result);
notifier.value = result;
} else if (WebVideoController.supported) {
final result = await WebVideoController.create(
player,
configuration,
);
platform.complete(result);
notifier.value = result;
}
if (platform.isCompleted) {
// Populate [id] & [rect] [ValueNotifier]s with the values from [platform] implementation of [PlatformVideoController].
final controller = await platform.future;
// Add listeners.
void fn0() => id.value = controller.id.value;
void fn1() => rect.value = controller.rect.value;
fn0();
fn1();
controller.id.addListener(fn0);
controller.rect.addListener(fn1);
// Remove listeners upon [Player.dispose].
player.platform?.release.add(() async {
controller.id.removeListener(fn0);
controller.rect.removeListener(fn1);
});
} else {
platform.completeError(
UnimplementedError(
'[VideoController] is unavailable for this platform.',
),
);
}
} catch (exception, stacktrace) {
platform.completeError(exception);
debugPrint(exception.toString());
debugPrint(stacktrace.toString());
}
if (!(player.platform?.videoControllerCompleter.isCompleted ?? true)) {
player.platform?.videoControllerCompleter.complete();
}
}();
}
/// Sets the required size of the video output.
/// This may yield substantial performance improvements if a small [width] & [height] is specified.
///
/// Remember:
/// * “Premature optimization is the root of all evil”
/// * “With great power comes great responsibility”
Future<void> setSize({
int? width,
int? height,
}) async {
final instance = await platform.future;
return instance.setSize(
width: width,
height: height,
);
}
/// A [Future] that completes when the first video frame has been rendered.
Future<void> get waitUntilFirstFrameRendered async {
final instance = await platform.future;
return instance.waitUntilFirstFrameRendered;
}
}
@@ -0,0 +1,132 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
// ignore_for_file: avoid_web_libraries_in_flutter
import 'dart:async';
import 'dart:js_interop';
import 'dart:js_interop_unsafe';
import 'package:web/web.dart' as web;
import 'dart:ui_web';
import 'package:flutter/rendering.dart';
import 'package:flutter/foundation.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/src/video_controller/platform_video_controller.dart';
/// {@template web_video_controller}
///
/// WebVideoController
/// ------------------
///
/// The [WebVideoController] implementation based on HTML & JavaScript used on web.
///
/// {@endtemplate}
class WebVideoController extends PlatformVideoController {
/// Whether [WebVideoController] is supported on the current platform or not.
static const bool supported = kIsWeb;
/// {@macro web_video_controller}
WebVideoController._(
super.player,
super.configuration,
);
/// {@macro web_video_controller}
static Future<PlatformVideoController> create(
Player player,
VideoControllerConfiguration configuration,
) async {
// Retrieve the native handle of the [Player].
final handle = await player.handle;
final controller = WebVideoController._(
player,
configuration,
);
// Register [_dispose] for execution upon [Player.dispose].
player.platform?.release.add(controller._dispose);
// Retrieve the [html.VideoElement] instance from [js.context].
final instances = globalContext.getProperty(_kInstances.toJS) as JSObject;
final element = instances.getProperty(handle.toJS) as web.HTMLVideoElement;
controller._element = element;
// Register the [html.VideoElement] as platform view.
platformViewRegistry.registerViewFactory(
'com.alexmercerind.media_kit_video.$handle',
(int _) => controller._element!,
);
// On web implementation, we are having [handle] & [controller.id] same, which in itself is a simple counter based value managed within [Player].
// Since there is no texture creation or rendering involved.
controller.id.value = handle;
// Listen to the resize event of the [html.VideoElement].
controller._resizeStreamSubscription = controller._element?.onResize.listen(
(event) {
debugPrint(
'media_kit: WebVideoController: ${controller._element?.videoWidth}, ${controller._element?.videoHeight}',
);
// Update the size of the [PlatformVideoController].
controller.rect.value = Rect.fromLTWH(
0.0,
0.0,
controller._element?.videoWidth.toDouble() ?? 0.0,
controller._element?.videoHeight.toDouble() ?? 0.0,
);
},
);
// Notify about the first frame being rendered.
// Case: If some [Media] is already playing when [VideoController] is created.
if (player.state.width != null && player.state.height != null) {
if (!controller.waitUntilFirstFrameRenderedCompleter.isCompleted) {
controller.waitUntilFirstFrameRenderedCompleter.complete();
}
}
controller._element?.onCanPlayThrough.listen(
(_) {
// Notify about first frame being rendered.
if (!controller.waitUntilFirstFrameRenderedCompleter.isCompleted) {
controller.waitUntilFirstFrameRenderedCompleter.complete();
}
},
);
// Return the [PlatformVideoController].
return controller;
}
/// Sets the required size of the video output.
/// This may yield substantial performance improvements if a small [width] & [height] is specified.
///
/// Remember:
/// * “Premature optimization is the root of all evil”
/// * “With great power comes great responsibility”
@override
Future<void> setSize({
int? width,
int? height,
}) {
throw UnsupportedError(
'[AndroidVideoController.setSize] is not available on web',
);
}
/// Disposes the instance. Releases allocated resources back to the system.
Future<void> _dispose() async {
super.dispose();
await _resizeStreamSubscription?.cancel();
}
/// HTML [html.HTMLVideoElement] instance reference.
web.HTMLVideoElement? _element;
StreamSubscription<web.Event>? _resizeStreamSubscription;
/// JavaScript object attribute used to store various [VideoElement] instances in [js.context].
static const _kInstances = '\$com.alexmercerind.media_kit.instances';
}
@@ -0,0 +1,28 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/src/video_controller/platform_video_controller.dart';
// Stub declaration for avoiding compilation errors on Dart Native using conditional imports.
class WebVideoController extends PlatformVideoController {
static const bool supported = false;
WebVideoController._(
super.player,
super.configuration,
);
static Future<PlatformVideoController> create(
Player player,
VideoControllerConfiguration configuration,
) =>
throw UnimplementedError();
@override
Future<void> setSize({int? width, int? height}) => throw UnimplementedError();
}
@@ -0,0 +1,6 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
export 'stub.dart' if (dart.library.js_interop) 'real.dart';
@@ -0,0 +1,104 @@
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
///
/// Copyright © 2023 & onwards, Abdelaziz Mahdy <abdelaziz.h.mahdy@gmail.com>.
/// All rights reserved.
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:media_kit_video/src/video/video.dart';
import 'package:media_kit_video/src/subtitle/subtitle_view.dart';
/// {@template video_view_parameters}
///
/// VideoViewParameters
/// -------------------
///
/// The attributes of a [Video] widget composed into a single class.
///
/// {@endtemplate}
class VideoViewParameters {
final double? width;
final double? height;
final BoxFit fit;
final Color fill;
final Alignment alignment;
final double? aspectRatio;
final FilterQuality filterQuality;
final /* VideoControlsBuilder? */ dynamic controls;
final SubtitleViewConfiguration subtitleViewConfiguration;
final FocusNode? focusNode;
/// {@macro video_view_parameters}
VideoViewParameters({
required this.width,
required this.height,
required this.fit,
required this.fill,
required this.alignment,
required this.aspectRatio,
required this.filterQuality,
required this.controls,
required this.subtitleViewConfiguration,
this.focusNode,
});
VideoViewParameters copyWith({
double? width,
double? height,
BoxFit? fit,
Color? fill,
Alignment? alignment,
double? aspectRatio,
FilterQuality? filterQuality,
/* VideoControlsBuilder? */ dynamic controls,
bool? pauseUponEnteringBackgroundMode,
bool? resumeUponEnteringForegroundMode,
SubtitleViewConfiguration? subtitleViewConfiguration,
Future<void> Function()? onEnterFullscreen,
Future<void> Function()? onExitFullscreen,
FocusNode? focusNode,
}) {
return VideoViewParameters(
width: width ?? this.width,
height: height ?? this.height,
fit: fit ?? this.fit,
fill: fill ?? this.fill,
alignment: alignment ?? this.alignment,
aspectRatio: aspectRatio ?? this.aspectRatio,
filterQuality: filterQuality ?? this.filterQuality,
controls: controls ?? this.controls,
subtitleViewConfiguration:
subtitleViewConfiguration ?? this.subtitleViewConfiguration,
focusNode: focusNode ?? this.focusNode,
);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is VideoViewParameters &&
other.width == width &&
other.height == height &&
other.fit == fit &&
other.fill == fill &&
other.alignment == alignment &&
other.aspectRatio == aspectRatio &&
other.filterQuality == filterQuality &&
other.controls == controls &&
other.subtitleViewConfiguration == subtitleViewConfiguration &&
other.focusNode == focusNode;
@override
int get hashCode =>
width.hashCode ^
height.hashCode ^
fit.hashCode ^
fill.hashCode ^
alignment.hashCode ^
aspectRatio.hashCode ^
filterQuality.hashCode ^
controls.hashCode ^
subtitleViewConfiguration.hashCode ^
focusNode.hashCode;
}