forked from Foxyz/sync_replay_project
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:
1 parent
25b19f2d6a
commit
a87ee98faa
131 files changed
+15532
-6
No files matched your search
@@ -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>();
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
// --------------------------------------------------
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
+6
@@ -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';
|
||||
+299
@@ -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());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
+28
@@ -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();
|
||||
}
|
||||
+6
@@ -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';
|
||||
+299
@@ -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());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
+28
@@ -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();
|
||||
}
|
||||
+157
@@ -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;
|
||||
}
|
||||
}
|
||||
+132
@@ -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';
|
||||
}
|
||||
+28
@@ -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();
|
||||
}
|
||||
Vendored
+6
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user