Vendor media_kit_video 2.0.1 with the X11/EGL fix (hardware rendering on X11 with Flutter >= 3.38)
ci / core (push) Successful in 28s
ci / server (push) Successful in 51s
ci / updater-windows (push) Failing after 5m59s

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

+3 -4
View File
@@ -538,10 +538,9 @@ packages:
media_kit_video:
dependency: "direct main"
description:
name: media_kit_video
sha256: afaa509e7b7e0bf247557a3a740cde903a52c34ace9810f94500e127bd7b043d
url: "https://pub.dev"
source: hosted
path: "../../third_party/media_kit_video"
relative: true
source: path
version: "2.0.1"
meta:
dependency: transitive
+3 -1
View File
@@ -10,7 +10,9 @@ dependencies:
sdk: flutter
signals: ^7.0.0
media_kit: ^1.1.0
media_kit_video: ^2.0.1
# Vendored with Starsync's patches (third_party/media_kit_video/starsync).
media_kit_video:
path: ../../third_party/media_kit_video
media_kit_libs_video: ^1.0.0
path_provider: ^2.1.0
web_socket_channel: ^3.0.0
+1
View File
@@ -12,6 +12,7 @@ Plain overview first: [HOW-IT-WORKS.md](HOW-IT-WORKS.md). Wire format:
| `apps/desktop` | Flutter app: UI, `AppGraph` (composition root), `SyncClient`, `Connection`, `SessionCoordinator`, `Navigation`, `YourServer` (`ServerIsolate`), `MediaPipeline` (opening media), `PlaybackController` (the session player's marks and commands), the player host (`PlayerHost`, `MediaKitHost`: the only media_kit user besides the `Video` widget glue), update UI (`UpdateController`) | ships with `starsync-server` |
| `tool` | harnesses: `range_logger`, `shaper`, `fake_viewer`, `e2e_check` | dev only |
| `deploy` | systemd unit for the dedicated server | |
| `third_party/media_kit_video` | upstream media_kit_video 2.0.1 with Starsync's patches (`starsync/README.md`: Flutter ≥ 3.38 EGL display on the platform thread, the X11 GLX step-aside) | vendored; re-apply patches on update |
Guard: `packages/core/test/flutter_free_test.dart` fails if anything under
`core/lib` imports Flutter, `dart:ui` or `signals_flutter`; CI runs
+4 -1
View File
@@ -6,7 +6,10 @@ Layout and rules: [ARCHITECTURE.md](ARCHITECTURE.md).
Needs the Flutter SDK (beta channel, Dart 3.13+) and, for the Linux desktop
build, the usual GTK toolchain (clang, cmake, ninja, pkg-config, libgtk-3-dev).
mpv libraries come from `media_kit_libs_video`. `flutter_soloud` (the app's
mpv libraries come from `media_kit_libs_video`. `media_kit_video` is the patched copy in
`third_party/media_kit_video` (see its `starsync/README.md`); test video
output changes under X11 (`GDK_BACKEND=x11`, ideally a real X11 session)
and Wayland. `flutter_soloud` (the app's
sounds) compiles SoLoud and miniaudio from source in its build hook with
that C++ toolchain (MSVC on Windows); it needs no audio dev headers (miniaudio
loads ALSA / PulseAudio / JACK at run time) and, with `no_xiph_libs` set in
+245
View File
@@ -0,0 +1,245 @@
## 2.0.1
- fix: flutter 3.38.x crash
- fix(linux): flutter 3.38.x h/w render
## 2.0.0
- feat: flutter 3.38.x support
- **BREAKING CHANGE**: remove `screen_brightness` and `volume_controller` dependencies
## 1.3.1
- fix(windows): notify `VideoOutput.Resize` on platform thread
- fix(linux): notify `VideoOutput.Resize` on platform thread
- fix(android): `VideoController` implementation
- fix(android): remove deprecated API usage
## 1.3.0
- **REFACTOR**: screen_brightness -> screen_brightness_platform_interface.
- **REFACTOR**(android): simplify AndroidVideoController implementation.
- **REFACTOR**(android): VideoOutput SurfaceTextureEntry -> SurfaceProducer migration.
- **REFACTOR**: use setProperty API in NativeVideoController.
- **FIX**: improve responsiveness of showing controls on mobile.
- **FIX**: bump web to 1.1.0.
- **FIX**: cast to JSObject.
- **FIX**: not call super.didChangeAppLifecycleState(state);.
- **FIX**: not call super.didChangeAppLifecycleState(state);.
- **FIX**: wakelock print.
- **FIX**: subtitles not shifting on controls show/hide.
- **FIX**: set width/height from VideoParams in NativeVideoController.
- **FIX**: seek inside onPointerMove.
- **FIX**(windows): automatic IDXGIAdapter selection on windows 10 or greater.
- **FIX**(android): waitUntilFirstFrameRenderedCompleter.
- **FIX**(android): --hwdec=auto-safe as default.
- **FIX**: dispose ValueNotifier(s) in PlatformVideoController.
- **FIX**: fullscreen.
- **FIX**: long press video speed reset issue.
- **FIX**: wrong value in brightness builder callback.
- **FIX**: Use a Scaffold as the outermost widget on fullscreen video pages.
- **FEAT**: upgrade volume_controller dependency and refactor related code.
- **FEAT**: seek on double tap custom duration support.
## 1.2.5
- fix: subtitleView not being updated
- fix: mobile double tap areas hidden but still mounted
- fix: mobile center click is delayed
- feat: desktop video controls mouse hides with controls
- feat: desktop on tap pause and play
- feat: desktop on double tap fullscreen
- fix: windows crash by @Airyzz [#900](https://github.com/media-kit/media-kit/pull/900)
- feat: auto pass changes to fullscreen without having to call .update
## 1.2.4
- fix: web compile error
## 1.2.3
- feat: `VideoState.update` & `VideoViewParameters`
## 1.2.2
- fix: override `setState` & check `mounted` in `MaterialVideoControls` & `MaterialDesktopVideoControls`
## 1.2.1
- fix(android): clear `android.view.Surface` before playback
- feat: `MaterialVideoControlsThemeData.seekBarAlignment`
## 1.2.0
- fix: `MaterialVideoControls` layout
## 1.1.9
- fix: unmount `CircularProgressIndicator` buffering indicator if invisible
- fix: pass all video attributes to fullscreen route
- fix: prevent controls from hiding during seek
- fix: hide last video's frame upon `Player.open`
- fix: `ThemeData.copyWith` override
- fix(android): `hwdec=auto-safe` w/ `enableHardwareAcceleration=true`
- fix(windows): fullscreen for non-primary monitors
- fix(darwin): `mpv_render_context_free` call
- fix(darwin): memory leaks
- fix(ios): fix `disposeMPV`
- feat: `VideoControllerConfiguration.androidAttachSurfaceAfterVideoParameters`
- feat: center the seek-bar within its parent `Container` for improved tap area
- feat: `backdropColor` argument in `MaterialVideoControlsThemeData`
## 1.1.8
- fix: pass all `*VideoControlsTheme`(s) to fullscreen `context`
## 1.1.7
- fix: add `await` to `maybePop` when exiting fullscreen
- fix: `MaterialVideoControls`/`MaterialDesktopVideoControls` seekbar glitch
- fix(android): S/W rendering fallback
- fix(android): create fresh `android.view.Surface` for every video output
## 1.1.6
- fix: programmatic fullscreen API
- fix(android): pause upon entering fullscreen
- fix(android): `waitUntilFirstFrameRenderedNotify` in `FlutterFragmentActivity`
## 1.1.5
- fix(android): `waitUntilFirstFrameRenderedNotify` fallback & release-mode
## 1.1.4
- feat: `Video` `resumeUponEnteringForegroundMode`
- feat(android): `waitUntilFirstFrameRenderedNotify` implementation
## 1.1.3
- feat(android): `VideoControllerConfiguration.scale`
- fix(android): use `hwdec=auto`
- fix(android): `SurfaceTexture.setDefaultBufferSize` & render race
## 1.1.2
- fix(windows): memory leak in `GetVideoWidth`/`GetVideoHeight`
- fix(linux): `GThread*` leak in S/W render & `video_output_get_(width|height)`
- fix(linux): H/W support for multiple videos
- build(darwin): bump `mpv` headers to `0.36.0`
- build(darwin): use symlinks for `FRAMEWORK_SEARCH_PATHS`, `media_kit_libs_*** >= 1.1.0`
- fix(darwin): remove black screen when switching videos ([#332](https://github.com/media-kit/media-kit/issues/332))
- feat: `Video`: expose `onEnterFullscreen` & `onExitFullscreen`
- feat: feat: `visibleOnMount` `MaterialVideoControls`/`MaterialDesktopVideoControls`
- fix: display `bufferingIndicatorBuilder` even if controls are hidden
## 1.1.1
- chore: `try`/`catch` native calls to hide stray logs
- fix: `MaterialDesktopVideoControls`: do not add `onTapUp` callback if `toggleFullscreenOnDoublePress` is disabled
## 1.1.0
- feat: `SubtitleView`, `SubtitleViewConfiguration`
- feat: `shiftSubtitlesOnControlsVisibilityChange` in `MaterialVideoControls` & `MaterialDesktopVideoControls`
- feat: apply rotation from metadata to video output
- feat: improve wakelock behavior
- feat: `pauseUponEnteringBackgroundMode`
- fix: `bufferingIndicatorBuilder` padding in `MaterialVideoControls` & `MaterialDesktopVideoControls`
- fix(windows): maintain aspect ratio in s/w rendering pixel-buffer size clamping
- fix(linux): maintain aspect ratio in s/w rendering pixel-buffer size clamping
- perf(android): use `hwdec=mediacodec` w/ `enableHardwareAcceleration`
- deps: migrate [`package:wakelock_plus`](https://pub.dev/packages/wakelock_plus)
## 1.0.2
- fix(video/macos): fix fullscreen support
## 1.0.1
- fix: synchronize `VideoController` constructor
- fix: `fullscreen` video controls theme data not being applied
## 1.0.0
- feat: web support
- feat: fullscreen API
- feat: acquire wakelock
- feat: support for AGP 8.0
- feat: pre-built video controls
- feat: `controls` argument in `Video` widget
- feat: `AdaptiveVideoControls`, `MaterialVideoControls`, `MaterialDesktopVideoControls` & `NoVideoControls`
## 0.0.12
- fix(android): improve `Texture` resize handling
## 0.0.11
- fix(android): improve `Texture` resize handling
## 0.0.10
- feat: `VideoControllerConfiguration`
- feat: `VideoController.waitUntilFirstFrameRendered`
- refactor: clean-up package structure
- refactor: remove `VideoController.dispose`
- refactor: `VideoController.create` -> `VideoController` constructor
- fix(android): add `av1` to `hwdec-codecs`
- fix(android): use `--vo=gpu` + `--hwdec=mediacodec-copy` /w `enableHardwareAcceleration`
## 0.0.9
- fix(android): revert to `--vo=mediacodec_embed` in `enableHardwareAcceleration`
## 0.0.8
- fix(android): subtitle rendering
- fix(android): video rendering inside emulators (#149)
- fix(android): video rendering with `enableHardwareAcceleration: false`
## 0.0.7
- fix(linux): VAAPI hardware acceleration
- perf(windows): `VideoOutput::Resize`: delete texture objects in background
## 0.0.6
- fix(windows): synchronize texture object deletion in on unregister _v.i.z_ `VideoOutput::Resize` or `VideoOutput::~VideoOutput`
## 0.0.5
- Android support
- feat: `VideoController.setSize`
- fix: set `vo` to `libmpv` before creating render context
- refactor: `VideoController.create` takes `Player` reference instead of `handle`
## 0.0.4
- fix: use `mkdir` instead of `.gitkeep`
## 0.0.3
- fix: add `.framework` & `.xcframework` for all libs
## 0.0.2
- macOS support:
- Hardware: MPV_RENDER_API_TYPE_OPENGL + pixel buffer + METAL
- Software: MPV_RENDER_API_TYPE_SW + pixel buffer
- iOS support:
- Hardware: MPV_RENDER_API_TYPE_OPENGL + pixel buffer
- Software: MPV_RENDER_API_TYPE_SW + pixel buffer
- fix(windows): use `TextureRegistrar::UnregisterTexture` release callback to free texture resources
- fix(windows): synchronize texture unregister & release on frame dimensions change
- feat: `aspectRatio` parameter for `Video` widget
## 0.0.1
- Initial release
- Windows support:
- Hardware: MPV_RENDER_API_TYPE_OPENGL + ANGLE + DirectX 11
- Software: MPV_RENDER_API_TYPE_SW + pixel buffer
- GNU/Linux support:
- Hardware: MPV_RENDER_API_TYPE_OPENGL + GDK/GL
- Software: MPV_RENDER_API_TYPE_SW + pixel buffer
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 & onwards Hitesh Kumar Saini <saini123hitesh@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+11
View File
@@ -0,0 +1,11 @@
# [package:media_kit_video](https://github.com/media-kit/media-kit)
[![](https://img.shields.io/discord/1079685977523617792?color=33cd57&label=Discord&logo=discord&logoColor=discord)](https://discord.gg/h7qf2R9n57) [![Github Actions](https://github.com/media-kit/media-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/media-kit/media-kit/actions/workflows/ci.yml)
Native implementation for video playback in [package:media_kit](https://pub.dev/packages/media_kit).
## License
Copyright © 2021 & onwards, Hitesh Kumar Saini <<saini123hitesh@gmail.com>>
This project & the work under this repository is governed by MIT license that can be found in the [LICENSE](./LICENSE) file.
+8
View File
@@ -0,0 +1,8 @@
include: package:flutter_lints/flutter.yaml
analyzer:
errors:
dangling_library_doc_comments: ignore
unintended_html_in_doc_comment: ignore
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+38
View File
@@ -0,0 +1,38 @@
group 'com.alexmercerind.media_kit_video'
version '1.0'
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.0'
}
}
rootProject.allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: 'com.android.library'
android {
namespace 'com.alexmercerind.media_kit_video'
compileSdkVersion 36
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion 16
consumerProguardFiles 'proguard-rules.pro'
}
}
@@ -0,0 +1,3 @@
-keepclassmembers class io.flutter.embedding.engine.FlutterEngine {
private io.flutter.embedding.engine.FlutterJNI flutterJNI;
}
+1
View File
@@ -0,0 +1 @@
rootProject.name = 'media_kit_video'
@@ -0,0 +1 @@
<manifest package="com.alexmercerind.media_kit_video"></manifest>
@@ -0,0 +1,84 @@
/**
* This file is a part of media_kit (https://github.com/media-kit/media-kit).
* <p>
* 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.
*/
package com.alexmercerind.media_kit_video;
import androidx.annotation.NonNull;
import java.util.HashMap;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
/**
* MediaKitVideoPlugin
*/
public class MediaKitVideoPlugin implements FlutterPlugin, MethodCallHandler {
private MethodChannel channel;
private VideoOutputManager videoOutputManager;
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "com.alexmercerind/media_kit_video");
channel.setMethodCallHandler(this);
videoOutputManager = new VideoOutputManager(flutterPluginBinding.getTextureRegistry());
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
switch (call.method) {
case "VideoOutputManager.Create": {
final long handle = Long.parseLong(call.argument("handle"));
videoOutputManager.create(handle, (id, wid, width, height) -> channel.invokeMethod("VideoOutput.Resize", new HashMap<String, Object>() {{
put("handle", handle);
put("id", id);
put("wid", wid);
put("rect", new HashMap<String, Object>() {{
put("left", 0);
put("top", 0);
put("width", width);
put("height", height);
}});
}}));
result.success(null);
break;
}
case "VideoOutputManager.SetSurfaceSize": {
final long handle = Long.parseLong(call.argument("handle"));
final int width = Integer.parseInt(call.argument("width"));
final int height = Integer.parseInt(call.argument("height"));
videoOutputManager.setSurfaceSize(handle, width, height);
result.success(null);
break;
}
case "VideoOutputManager.Dispose": {
final long handle = Long.parseLong(call.argument("handle"));
videoOutputManager.dispose(handle);
result.success(null);
break;
}
case "Utils.IsEmulator": {
result.success(Utils.isEmulator());
break;
}
default: {
result.notImplemented();
break;
}
}
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
}
}
@@ -0,0 +1,5 @@
package com.alexmercerind.media_kit_video;
public interface TextureUpdateCallback {
void onTextureUpdate(long id, long wid, int width, int height);
}
@@ -0,0 +1,42 @@
/**
* This file is a part of media_kit (https://github.com/media-kit/media-kit).
* <p>
* 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.
*/
package com.alexmercerind.media_kit_video;
import android.os.Build;
import io.flutter.Log;
public abstract class Utils {
private static final String TAG = "Utils";
public static boolean isEmulator() {
try {
// https://github.com/fluttercommunity/plus_plugins/blob/ff54dc49230ee5f8b772a3326d4ff3758618df80/packages/device_info_plus/device_info_plus/android/src/main/kotlin/dev/fluttercommunity/plus/device_info/MethodCallHandlerImpl.kt#L110-L125
return Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")
|| Build.FINGERPRINT.startsWith("generic")
|| Build.FINGERPRINT.startsWith("unknown")
|| Build.HARDWARE.contains("goldfish")
|| Build.HARDWARE.contains("ranchu")
|| Build.MODEL.contains("google_sdk")
|| Build.MODEL.contains("Emulator")
|| Build.MODEL.contains("Android SDK built for x86")
|| Build.MANUFACTURER.contains("Genymotion")
|| Build.PRODUCT.contains("sdk_google")
|| Build.PRODUCT.contains("google_sdk")
|| Build.PRODUCT.contains("sdk")
|| Build.PRODUCT.contains("sdk_x86")
|| Build.PRODUCT.contains("vbox86p")
|| Build.PRODUCT.contains("emulator")
|| Build.PRODUCT.contains("simulator");
} catch (Throwable e) {
Log.e(TAG, "isEmulator", e);
}
return false;
}
}
@@ -0,0 +1,141 @@
/**
* This file is a part of media_kit (https://github.com/media-kit/media-kit).
* <p>
* 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.
*/
package com.alexmercerind.media_kit_video;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Locale;
import java.util.Objects;
import io.flutter.view.TextureRegistry;
public class VideoOutput implements TextureRegistry.SurfaceProducer.Callback {
private static final String TAG = "VideoOutput";
private static final Method newGlobalObjectRef;
private static final Method deleteGlobalObjectRef;
private static final HashSet<Long> deletedGlobalObjectRefs = new HashSet<>();
private static final Handler handler = new Handler(Looper.getMainLooper());
static {
try {
// com.alexmercerind.mediakitandroidhelper.MediaKitAndroidHelper is part of package:media_kit_libs_android_video & package:media_kit_libs_android_audio packages.
// Use reflection to invoke methods of com.alexmercerind.mediakitandroidhelper.MediaKitAndroidHelper.
Class<?> mediaKitAndroidHelperClass = Class.forName("com.alexmercerind.mediakitandroidhelper.MediaKitAndroidHelper");
newGlobalObjectRef = mediaKitAndroidHelperClass.getDeclaredMethod("newGlobalObjectRef", Object.class);
deleteGlobalObjectRef = mediaKitAndroidHelperClass.getDeclaredMethod("deleteGlobalObjectRef", long.class);
newGlobalObjectRef.setAccessible(true);
deleteGlobalObjectRef.setAccessible(true);
} catch (Throwable e) {
Log.i("media_kit", "package:media_kit_libs_android_video missing. Make sure you have added it to pubspec.yaml.");
throw new RuntimeException("Failed to initialize com.alexmercerind.media_kit_video.VideoOutput.");
}
}
private long id = 0;
private long wid = 0;
private final TextureUpdateCallback textureUpdateCallback;
private final TextureRegistry.SurfaceProducer surfaceProducer;
private final Object lock = new Object();
VideoOutput(TextureRegistry textureRegistryReference, TextureUpdateCallback textureUpdateCallback) {
this.textureUpdateCallback = textureUpdateCallback;
surfaceProducer = textureRegistryReference.createSurfaceProducer();
surfaceProducer.setCallback(this);
}
public void dispose() {
synchronized (lock) {
try {
surfaceProducer.getSurface().release();
} catch (Throwable e) {
Log.e(TAG, "dispose", e);
}
try {
surfaceProducer.release();
} catch (Throwable e) {
Log.e(TAG, "dispose", e);
}
onSurfaceCleanup();
}
}
public void setSurfaceSize(int width, int height) {
setSurfaceSize(width, height, false);
}
private void setSurfaceSize(int width, int height, boolean force) {
synchronized (lock) {
try {
if (!force && surfaceProducer.getWidth() == width && surfaceProducer.getHeight() == height) {
return;
}
surfaceProducer.setSize(width, height);
onSurfaceAvailable();
} catch (Throwable e) {
Log.e(TAG, "setSurfaceSize", e);
}
}
}
@Override
public void onSurfaceAvailable() {
synchronized (lock) {
Log.i(TAG, "onSurfaceAvailable");
id = surfaceProducer.id();
wid = newGlobalObjectRef(surfaceProducer.getSurface());
textureUpdateCallback.onTextureUpdate(id, wid, surfaceProducer.getWidth(), surfaceProducer.getHeight());
}
}
@Override
public void onSurfaceCleanup() {
synchronized (lock) {
Log.i(TAG, "onSurfaceCleanup");
textureUpdateCallback.onTextureUpdate(id, 0, surfaceProducer.getWidth(), surfaceProducer.getHeight());
if (wid != 0) {
final long widReference = wid;
handler.postDelayed(() -> deleteGlobalObjectRef(widReference), 5000);
}
}
}
private static long newGlobalObjectRef(Object object) {
Log.i(TAG, String.format(Locale.ENGLISH, "newGlobalRef: object = %s", object));
try {
return (long) Objects.requireNonNull(newGlobalObjectRef.invoke(null, object));
} catch (Throwable e) {
Log.e(TAG, "newGlobalRef", e);
return 0;
}
}
private static void deleteGlobalObjectRef(long ref) {
if (deletedGlobalObjectRefs.contains(ref)) {
Log.i(TAG, String.format(Locale.ENGLISH, "deleteGlobalObjectRef: ref = %d ALREADY DELETED", ref));
return;
}
if (deletedGlobalObjectRefs.size() > 100) {
deletedGlobalObjectRefs.clear();
}
deletedGlobalObjectRefs.add(ref);
Log.i(TAG, String.format(Locale.ENGLISH, "deleteGlobalObjectRef: ref = %d", ref));
try {
deleteGlobalObjectRef.invoke(null, ref);
} catch (Throwable e) {
Log.e(TAG, "deleteGlobalObjectRef", e);
}
}
}
@@ -0,0 +1,57 @@
/**
* This file is a part of media_kit (https://github.com/media-kit/media-kit).
* <p>
* 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.
*/
package com.alexmercerind.media_kit_video;
import android.util.Log;
import java.util.HashMap;
import java.util.Locale;
import java.util.Objects;
import io.flutter.view.TextureRegistry;
public class VideoOutputManager {
private static final String TAG = "VideoOutputManager";
private final HashMap<Long, VideoOutput> videoOutputs = new HashMap<>();
private final TextureRegistry textureRegistryReference;
private final Object lock = new Object();
VideoOutputManager(TextureRegistry textureRegistryReference) {
this.textureRegistryReference = textureRegistryReference;
}
public void create(long handle, TextureUpdateCallback textureUpdateCallback) {
synchronized (lock) {
Log.i(TAG, String.format(Locale.ENGLISH, "com.alexmercerind.media_kit_video.VideoOutputManager.create: %d", handle));
if (!videoOutputs.containsKey(handle)) {
final VideoOutput videoOutput = new VideoOutput(textureRegistryReference, textureUpdateCallback);
videoOutputs.put(handle, videoOutput);
}
}
}
public void dispose(long handle) {
synchronized (lock) {
Log.i(TAG, String.format(Locale.ENGLISH, "com.alexmercerind.media_kit_video.VideoOutputManager.dispose: %d", handle));
if (videoOutputs.containsKey(handle)) {
Objects.requireNonNull(videoOutputs.get(handle)).dispose();
videoOutputs.remove(handle);
}
}
}
public void setSurfaceSize(long handle, int width, int height) {
synchronized (lock) {
Log.i(TAG, String.format(Locale.ENGLISH, "com.alexmercerind.media_kit_video.VideoOutputManager.setSurfaceSize: %d %d %d", handle, width, height));
if (videoOutputs.containsKey(handle)) {
Objects.requireNonNull(videoOutputs.get(handle)).setSurfaceSize(width, height);
}
}
}
}
@@ -0,0 +1,30 @@
public enum MPVHelpers {
public static func checkError(_ status: CInt) {
if status < 0 {
NSLog("MPVHelpers: error: \(String(cString: mpv_error_string(status)))")
exit(1)
}
}
public static func getVideoOutParams(
_ handle: OpaquePointer
) -> MPVVideoOutParams {
var node = mpv_node()
defer {
mpv_free_node_contents(&node)
}
mpv_get_property(handle, "video-out-params", MPV_FORMAT_NODE, &node)
if node.format != MPV_FORMAT_NODE_MAP {
return MPVVideoOutParams.empty
}
let map: mpv_node_list = node.u.list!.pointee
if map.num == 0 {
return MPVVideoOutParams.empty
}
return MPVVideoOutParams.fromMPVNodeList(map)
}
}
@@ -0,0 +1,55 @@
public class MPVVideoOutParams {
public let dw: Int64
public let dh: Int64
public let rotate: Int64
init(
dw: Int64,
dh: Int64,
rotate: Int64
) {
self.dw = dw
self.dh = dh
self.rotate = rotate
}
public static let empty = MPVVideoOutParams(
dw: 0,
dh: 0,
rotate: 0
)
public static func fromMPVNodeList(
_ map: mpv_node_list
) -> MPVVideoOutParams {
var dw: Int64 = 0
var dh: Int64 = 0
var rotate: Int64 = 0
var kptr = map.keys!
var vptr = map.values!
for _ in 0 ..< map.num {
let key = String(cString: kptr.pointee!)
let value: mpv_node = vptr.pointee
kptr = kptr.successor()
vptr = vptr.successor()
if value.format == MPV_FORMAT_INT64 {
if key == "dw" {
dw = value.u.int64
} else if key == "dh" {
dh = value.u.int64
} else if key == "rotate" {
rotate = value.u.int64
}
}
}
return MPVVideoOutParams(
dw: dw,
dh: dh,
rotate: rotate
)
}
}
@@ -0,0 +1,168 @@
#if canImport(Flutter)
import Flutter
#elseif canImport(FlutterMacOS)
import FlutterMacOS
#endif
public class MediaKitVideoPlugin: NSObject, FlutterPlugin {
private static let CHANNEL_NAME = "com.alexmercerind/media_kit_video"
public static func register(with registrar: FlutterPluginRegistrar) {
#if canImport(Flutter)
let binaryMessenger = registrar.messenger()
let registry = registrar.textures()
let utils: UtilsProtocol? = nil
#elseif canImport(FlutterMacOS)
let binaryMessenger = registrar.messenger
let registry = registrar.textures
let utils: UtilsProtocol? = Utils(registrar)
#endif
let channel = FlutterMethodChannel(
name: CHANNEL_NAME,
binaryMessenger: binaryMessenger
)
let instance = MediaKitVideoPlugin(
registry: registry,
channel: channel,
utils: utils
)
registrar.addMethodCallDelegate(instance, channel: channel)
}
private let channel: FlutterMethodChannel
private let videoOutputManager: VideoOutputManager
private let utils: UtilsProtocol?
init(
registry: FlutterTextureRegistry,
channel: FlutterMethodChannel,
utils: UtilsProtocol?
) {
self.channel = channel
videoOutputManager = VideoOutputManager(
registry: registry
)
self.utils = utils
}
public func handle(
_ call: FlutterMethodCall,
result: @escaping FlutterResult
) {
switch call.method {
case "VideoOutputManager.Create":
handleCreateMethodCall(call.arguments, result)
case "VideoOutputManager.SetSize":
handleSetSizeMethodCall(call.arguments, result)
case "VideoOutputManager.Dispose":
handleDisposeMethodCall(call.arguments, result)
case "Utils.EnterNativeFullscreen":
handleEnterNativeFullscreenMethodCall(call.arguments, result)
case "Utils.ExitNativeFullscreen":
handleExitNativeFullscreenMethodCall(call.arguments, result)
default:
result(FlutterMethodNotImplemented)
}
}
private func handleCreateMethodCall(
_ arguments: Any?,
_ result: FlutterResult
) {
let args = arguments as? [String: Any]
let handleStr = args?["handle"] as! String
let handle: Int64? = Int64(handleStr)
let configDict = args?["configuration"] as! [String: Any]
let configuration = VideoOutputConfiguration.fromDict(configDict)
assert(handle != nil, "handle must be an Int64")
videoOutputManager.create(
handle: handle!,
configuration: configuration,
textureUpdateCallback: { (_ textureId: Int64, _ size: CGSize) in
self.channel.invokeMethod(
"VideoOutput.Resize",
arguments: [
"handle": handle!,
"id": textureId,
"rect": [
"top": 0,
"left": 0,
"width": size.width,
"height": size.height,
],
] as [String: Any]
)
}
)
result(nil)
}
private func handleSetSizeMethodCall(
_ arguments: Any?,
_ result: FlutterResult
) {
let args = arguments as? [String: Any]
let handleStr = args?["handle"] as! String
let widthStr = args?["width"] as! String
let heightStr = args?["height"] as! String
let handle: Int64? = Int64(handleStr)
let width: Int64? = Int64(widthStr)
let height: Int64? = Int64(heightStr)
assert(handle != nil, "handle must be an Int64")
self.videoOutputManager.setSize(
handle: handle!,
width: width,
height: height
)
result(nil)
}
private func handleDisposeMethodCall(
_ arguments: Any?,
_ result: FlutterResult
) {
let args = arguments as? [String: Any]
let handleStr = args?["handle"] as! String
let handle: Int64? = Int64(handleStr)
assert(handle != nil, "handle must be an Int64")
videoOutputManager.destroy(
handle: handle!
)
result(nil)
}
private func handleEnterNativeFullscreenMethodCall(
_: Any?,
_ result: FlutterResult
) {
if utils == nil {
return result(FlutterMethodNotImplemented)
}
utils?.enterNativeFullscreen()
result(nil)
}
private func handleExitNativeFullscreenMethodCall(
_: Any?,
_ result: FlutterResult
) {
if utils == nil {
return result(FlutterMethodNotImplemented)
}
utils?.exitNativeFullscreen()
result(nil)
}
}
@@ -0,0 +1,10 @@
#if canImport(Flutter)
import Flutter
#elseif canImport(FlutterMacOS)
import FlutterMacOS
#endif
public protocol ResizableTextureProtocol: NSObject, FlutterTexture {
func resize(_ size: CGSize)
func render(_ size: CGSize)
}
@@ -0,0 +1,44 @@
#if canImport(Flutter)
import Flutter
#elseif canImport(FlutterMacOS)
import FlutterMacOS
#endif
// This class avoids data race when called from a thread
public class SafeResizableTexture:
NSObject,
FlutterTexture,
ResizableTextureProtocol
{
private let lock = NSRecursiveLock()
private let child: ResizableTextureProtocol
init(_ child: ResizableTextureProtocol) {
self.child = child
}
public func resize(_ size: CGSize) {
return locked {
return child.resize(size)
}
}
public func render(_ size: CGSize) {
return locked {
return child.render(size)
}
}
public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
return child.copyPixelBuffer()
}
private func locked<T>(do block: () -> T) -> T {
lock.lock()
defer {
lock.unlock()
}
return block()
}
}
@@ -0,0 +1,106 @@
// This class was created to prevent a frameBuffer read by Flutter from being
// concurrently modified by a write method (mpv's renderer).
//
// To do this, two pools are set up, one for frameBuffers available for writing,
// the other for frameBuffers ready to be read by Flutter.
//
// When a frameBuffer has finished being modified and is "pushed", the oldest
// ready frameBuffer is marked as the current frameBuffer, and the oldest
// current frameBuffer is placed in the pool of frameBuffers available for
// writing.
//
// The use of at least three frameBuffers ensures that the read and write phases
// do not overlap, thus eliminating flicker.
public class SwappableObjectManager<T> {
private let lock: NSRecursiveLock = NSRecursiveLock()
private var available: [T]
private var ready: [T] = []
private var _current: T?
init(objects: [T], skipCheckArgs: Bool = false) {
if !skipCheckArgs {
SwappableObjectManager.checkArgs(objects)
}
available = objects
}
public func reinit(objects: [T], skipCheckArgs: Bool = false) {
if !skipCheckArgs {
SwappableObjectManager.checkArgs(objects)
}
lock.lock()
defer {
lock.unlock()
}
available = objects
ready = []
_current = nil
}
public func nextAvailable() -> T? {
lock.lock()
defer {
lock.unlock()
}
let object: T? =
available.count > 0
? available.removeFirst()
: nil
return object
}
public func pushAsReady(_ object: T) {
lock.lock()
defer {
lock.unlock()
}
ready.append(object)
updateCurrent()
}
public var current: T? {
lock.lock()
defer {
lock.unlock()
}
return _current
}
private func updateCurrent() {
lock.lock()
defer {
lock.unlock()
}
let next: T? =
ready.count > 0
? ready.removeFirst()
: nil
if next == nil {
return
}
let old: T? = _current
_current = next
if old == nil {
return
}
available.append(old!)
}
static private func checkArgs(_ objects: [T]) {
if objects.count < 2 {
NSLog("SwappableObjectManager: require at least two objects to work")
}
}
}
@@ -0,0 +1,146 @@
#if canImport(Flutter)
import Flutter
#elseif canImport(FlutterMacOS)
import FlutterMacOS
#endif
public class TextureSW: NSObject, FlutterTexture, ResizableTextureProtocol {
public typealias UpdateCallback = () -> Void
private let handle: OpaquePointer
private let updateCallback: UpdateCallback
private var renderContext: OpaquePointer?
private var textureContexts = SwappableObjectManager<TextureSWContext>(
objects: [],
skipCheckArgs: true
)
init(
handle: OpaquePointer,
updateCallback: @escaping UpdateCallback
) {
self.handle = handle
self.updateCallback = updateCallback
super.init()
DispatchQueue.main.async {
self.initMPV()
}
}
deinit {
disposePixelBuffer()
disposeMPV()
}
public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
let textureContext = textureContexts.current
if textureContext == nil {
return nil
}
return Unmanaged.passRetained(textureContext!.pixelBuffer)
}
private func initMPV() {
let api = UnsafeMutableRawPointer(
mutating: (MPV_RENDER_API_TYPE_SW as NSString).utf8String
)
var params: [mpv_render_param] = [
mpv_render_param(type: MPV_RENDER_PARAM_API_TYPE, data: api),
mpv_render_param(type: MPV_RENDER_PARAM_INVALID, data: nil),
]
MPVHelpers.checkError(
mpv_render_context_create(&renderContext, handle, &params)
)
mpv_render_context_set_update_callback(
renderContext,
{ (ctx) in
let that = unsafeBitCast(ctx, to: TextureSW.self)
DispatchQueue.main.async {
that.updateCallback()
}
},
UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
)
}
private func disposeMPV() {
mpv_render_context_set_update_callback(renderContext, nil, nil)
mpv_render_context_free(renderContext)
}
public func resize(_ size: CGSize) {
if size.width == 0 || size.height == 0 {
return
}
NSLog("TextureSW: resize: \(size.width)x\(size.height)")
createPixelBuffer(size)
}
private func createPixelBuffer(_ size: CGSize) {
disposePixelBuffer()
textureContexts.reinit(
objects: [
TextureSWContext(size: size),
TextureSWContext(size: size),
TextureSWContext(size: size),
],
skipCheckArgs: true
)
}
private func disposePixelBuffer() {
textureContexts.reinit(objects: [], skipCheckArgs: true)
}
public func render(_ size: CGSize) {
let textureContext = textureContexts.nextAvailable()
if textureContext == nil {
return
}
CVPixelBufferLockBaseAddress(
textureContext!.pixelBuffer,
CVPixelBufferLockFlags(rawValue: 0)
)
defer {
CVPixelBufferUnlockBaseAddress(
textureContext!.pixelBuffer,
CVPixelBufferLockFlags(rawValue: 0)
)
}
var ssize: [Int32] = [Int32(size.width), Int32(size.height)]
let format: String = "bgr0"
var pitch: Int = CVPixelBufferGetBytesPerRow(textureContext!.pixelBuffer)
let buffer = CVPixelBufferGetBaseAddress(textureContext!.pixelBuffer)
// pointers
let ssizePtr = ssize.withUnsafeMutableBytes {
$0.baseAddress?.assumingMemoryBound(to: Int32.self)
}
let formatPtr = UnsafeMutablePointer(
mutating: (format as NSString).utf8String
)
let pitchPtr = withUnsafeMutablePointer(to: &pitch) { $0 }
let bufferPtr = buffer!.assumingMemoryBound(to: UInt8.self)
var params: [mpv_render_param] = [
mpv_render_param(type: MPV_RENDER_PARAM_SW_SIZE, data: ssizePtr),
mpv_render_param(type: MPV_RENDER_PARAM_SW_FORMAT, data: formatPtr),
mpv_render_param(type: MPV_RENDER_PARAM_SW_STRIDE, data: pitchPtr),
mpv_render_param(type: MPV_RENDER_PARAM_SW_POINTER, data: bufferPtr),
mpv_render_param(type: MPV_RENDER_PARAM_INVALID, data: nil),
]
mpv_render_context_render(renderContext, &params)
textureContexts.pushAsReady(textureContext!)
}
}
@@ -0,0 +1,36 @@
public class TextureSWContext {
public let pixelBuffer: CVPixelBuffer
init(size: CGSize) {
self.pixelBuffer = TextureSWContext.createPixelBuffer(size)
}
deinit {
TextureSWContext.disposePixelBuffer(pixelBuffer)
}
private static func createPixelBuffer(_ size: CGSize) -> CVPixelBuffer {
let attrs =
[
kCVPixelBufferMetalCompatibilityKey: true
] as CFDictionary
var pixelBuffer: CVPixelBuffer?
let cvret = CVPixelBufferCreate(
kCFAllocatorDefault,
Int(size.width),
Int(size.height),
kCVPixelFormatType_32BGRA,
attrs,
&pixelBuffer
)
assert(cvret == kCVReturnSuccess, "CVPixelBufferCreate")
return pixelBuffer!
}
private static func disposePixelBuffer(_ pixelBuffer: CVPixelBuffer) {
// 'CVPixelBufferRelease' is unavailable: Core Foundation objects are
// automatically memory managed
}
}
@@ -0,0 +1,4 @@
public protocol UtilsProtocol: NSObject {
func enterNativeFullscreen()
func exitNativeFullscreen()
}
@@ -0,0 +1,199 @@
#if canImport(Flutter)
import Flutter
#elseif canImport(FlutterMacOS)
import FlutterMacOS
#endif
// This class creates and manipulates the different types of FlutterTexture,
// handles resizing, rendering calls, and notify Flutter when a new frame is
// available to render.
//
// To improve the user experience, a worker is used to execute heavy tasks on a
// dedicated thread.
public class VideoOutput: NSObject {
// Will be called on the main thread
public typealias TextureUpdateCallback = (Int64, CGSize) -> Void
private static let isSimulator: Bool = {
let isSim: Bool
#if targetEnvironment(simulator)
isSim = true
#else
isSim = false
#endif
return isSim
}()
private let handle: OpaquePointer
private let enableHardwareAcceleration: Bool
private let registry: FlutterTextureRegistry
private let textureUpdateCallback: TextureUpdateCallback
private let worker: Worker = .init()
private var width: Int64?
private var height: Int64?
private var texture: ResizableTextureProtocol!
private var textureId: Int64 = -1
private var currentSize: CGSize = CGSize.zero
private var disposed: Bool = false
init(
handle: Int64,
configuration: VideoOutputConfiguration,
registry: FlutterTextureRegistry,
textureUpdateCallback: @escaping TextureUpdateCallback
) {
let handle = OpaquePointer(bitPattern: Int(handle))
assert(handle != nil, "handle casting")
self.handle = handle!
width = configuration.width
height = configuration.height
enableHardwareAcceleration = configuration.enableHardwareAcceleration
self.registry = registry
self.textureUpdateCallback = textureUpdateCallback
super.init()
worker.enqueue {
self._init()
}
}
deinit {
worker.cancel()
disposed = true
disposeTextureId()
}
public func setSize(width: Int64?, height: Int64?) {
worker.enqueue {
self.width = width
self.height = height
}
}
private func _init() {
let enableHardwareAcceleration =
VideoOutput.isSimulator ? false : enableHardwareAcceleration
NSLog(
"VideoOutput: enableHardwareAcceleration: \(enableHardwareAcceleration)"
)
if VideoOutput.isSimulator {
NSLog(
"VideoOutput: warning: hardware rendering is disabled in the iOS simulator, due to an incompatibility with OpenGL ES"
)
}
if enableHardwareAcceleration {
texture = SafeResizableTexture(
TextureHW(
handle: handle,
// Use `weak self` to prevent memory leaks
updateCallback: { [weak self]() in
guard let that = self else {
return
}
that.updateCallback()
}
)
)
} else {
texture = SafeResizableTexture(
TextureSW(
handle: handle,
// Use `weak self` to prevent memory leaks
updateCallback: { [weak self]() in
guard let that = self else {
return
}
that.updateCallback()
}
)
)
}
DispatchQueue.main.sync { [weak self]() in
guard let that = self else {
return
}
that.registerTextureId()
}
}
// Must be run on the main thread
private func registerTextureId() {
// Textures must be registered on the platform thread.
textureId = registry.register(texture)
// textureUpdateCallback must run on the main thread
textureUpdateCallback(textureId, CGSize(width: 0, height: 0))
}
private func disposeTextureId() {
let registry_ = self.registry
let textureId_ = self.textureId
textureId = -1
DispatchQueue.main.async {
// Textures must be unregistered on the platform thread
registry_.unregisterTexture(textureId_)
}
}
public func updateCallback() {
worker.enqueue {
self._updateCallback()
}
}
private func _updateCallback() {
let size = videoSize
if size.width == 0 || size.height == 0 {
return
}
if currentSize != size {
currentSize = size
texture.resize(size)
DispatchQueue.main.sync { [weak self] in
guard let that = self else { return }
// textureUpdateCallback must run on the main thread
that.textureUpdateCallback(that.textureId, size)
}
}
if disposed {
return
}
texture.render(size)
DispatchQueue.main.sync { [weak self] in
guard let that = self else { return }
// Textures must be marked as available from the main thread
that.registry.textureFrameAvailable(that.textureId)
}
}
private var videoSize: CGSize {
// fixed size
if width != nil && height != nil {
return CGSize(
width: Double(width!),
height: Double(height!)
)
}
let params = MPVHelpers.getVideoOutParams(handle)
return CGSize(
width: Double(width ?? (params.rotate == 0 || params.rotate == 180
? params.dw
: params.dh)),
height: Double(height ?? (params.rotate == 0 || params.rotate == 180
? params.dh
: params.dw))
)
}
}
@@ -0,0 +1,33 @@
public class VideoOutputConfiguration {
public let width: Int64?
public let height: Int64?
public let enableHardwareAcceleration: Bool
init(
width: Int64?,
height: Int64?,
enableHardwareAcceleration: Bool
) {
self.width = width
self.height = height
self.enableHardwareAcceleration = enableHardwareAcceleration
}
public static func fromDict(_ dict: [String: Any])
-> VideoOutputConfiguration
{
let widthStr = dict["width"] as! String
let heightStr = dict["height"] as! String
let enableHardwareAcceleration =
dict["enableHardwareAcceleration"] as! Bool
let width: Int64? = Int64(widthStr)
let height: Int64? = Int64(heightStr)
return VideoOutputConfiguration(
width: width,
height: height,
enableHardwareAcceleration: enableHardwareAcceleration
)
}
}
@@ -0,0 +1,56 @@
#if canImport(Flutter)
import Flutter
#elseif canImport(FlutterMacOS)
import FlutterMacOS
#endif
public class VideoOutputManager: NSObject {
private let registry: FlutterTextureRegistry
private var videoOutputs = [Int64: VideoOutput]()
init(registry: FlutterTextureRegistry) {
self.registry = registry
}
public func create(
handle: Int64,
configuration: VideoOutputConfiguration,
textureUpdateCallback: @escaping VideoOutput.TextureUpdateCallback
) {
let videoOutput = VideoOutput(
handle: handle,
configuration: configuration,
registry: self.registry,
textureUpdateCallback: textureUpdateCallback
)
self.videoOutputs[handle] = videoOutput
}
public func setSize(
handle: Int64,
width: Int64?,
height: Int64?
) {
let videoOutput = self.videoOutputs[handle]
if videoOutput == nil {
return
}
videoOutput!.setSize(
width: width,
height: height
)
}
public func destroy(
handle: Int64
) {
let videoOutput = self.videoOutputs[handle]
if videoOutput == nil {
return
}
self.videoOutputs[handle] = nil
}
}
@@ -0,0 +1,74 @@
// https://stackoverflow.com/questions/49043257/how-to-ensure-to-run-some-code-on-same-background-thread/49075382#49075382
class Worker {
public typealias Job = () -> Void
private let semaphore = DispatchSemaphore(value: 0)
private let lock = NSRecursiveLock()
private var thread: Thread!
private var queue = [Job]()
private var canceled: Bool = false
init() {
thread = Thread(block: loop)
thread.start()
}
public func cancel() {
signalCancel()
thread.cancel()
}
public func enqueue(_ job: @escaping Job) {
locked {
queue.append(job)
}
semaphore.signal()
}
private func loop() {
while true {
semaphore.wait()
if isCanceled() {
return
}
let job = getFirstJob()
job()
}
}
private func signalCancel() {
locked {
canceled = true
}
semaphore.signal()
}
private func isCanceled() -> Bool {
let c = locked {
canceled
}
return c
}
private func getFirstJob() -> Job {
let job = locked {
queue.removeFirst()
}
return job
}
private func locked<T>(do block: () -> T) -> T {
lock.lock()
defer {
lock.unlock()
}
return block()
}
}
@@ -0,0 +1,9 @@
#if canImport(Flutter)
import Flutter
#elseif canImport(FlutterMacOS)
import FlutterMacOS
#endif
public class MediaKitVideoPlugin: NSObject, FlutterPlugin {
public static func register(with _: FlutterPluginRegistrar) {}
}
+29
View File
@@ -0,0 +1,29 @@
SPACE := $(subst ,, )
HEADERS_DESTDIR_ESCAPED=$(subst $(SPACE),\ ,${HEADERS_DESTDIR})
all: ${HEADERS_DESTDIR_ESCAPED}/mpv/*.h
MPV_HEADERS_VERSION=v0.36.0
MPV_HEADERS_SHA256SUM=29abc44f8ebee013bb2f9fe14d80b30db19b534c679056e4851ceadf5a5e8bf6
TAR=tar
TAR_WILDCARDS_FLAG := $(shell ${TAR} --version 2>&1 | grep -q 'GNU' && echo "--wildcards" || echo "")
.cache/headers/mpv-${MPV_HEADERS_VERSION}.tar.gz:
mkdir -p .cache/headers
rm -f .cache/headers/*.tmp .cache/headers/*.tar.gz
curl -L \
https://github.com/mpv-player/mpv/archive/refs/tags/${MPV_HEADERS_VERSION}.tar.gz \
-o .cache/headers/mpv.tar.gz.tmp
shasum -a 256 -c <<< '${MPV_HEADERS_SHA256SUM} .cache/headers/mpv.tar.gz.tmp'
mv .cache/headers/mpv.tar.gz.tmp .cache/headers/mpv-${MPV_HEADERS_VERSION}.tar.gz
touch .cache/headers/mpv-${MPV_HEADERS_VERSION}.tar.gz
.cache/headers/mpv.tar.gz: .cache/headers/mpv-${MPV_HEADERS_VERSION}.tar.gz
rm -f .cache/headers/mpv.tar.gz
ln -s mpv-${MPV_HEADERS_VERSION}.tar.gz .cache/headers/mpv.tar.gz
${HEADERS_DESTDIR_ESCAPED}/mpv/*.h: .cache/headers/mpv.tar.gz
mkdir -p ${HEADERS_DESTDIR_ESCAPED}/mpv
${TAR} ${TAR_WILDCARDS_FLAG} -xvf .cache/headers/mpv.tar.gz --strip-components 2 -C ${HEADERS_DESTDIR_ESCAPED}/mpv/ 'mpv-*/libmpv/*.h'
touch ${HEADERS_DESTDIR_ESCAPED}/mpv/*.h
@@ -0,0 +1,95 @@
class MediaKitUtils
attr_accessor :libs_found, :libs_package
module Platform
IOS ||= 'ios'
MACOS ||= 'macos'
end
module Type
AUDIO ||= 'audio'
VIDEO ||= 'video'
end
def initialize(platform)
raise "Expecting '#{Platform::IOS}' or '#{Platform::MACOS}': platform = #{platform}" if
!(platform == Platform::IOS || platform == Platform::MACOS)
# Find the nearest path to `pubspec.lock` using `PWD` env var set by
# `cocoapods`
current_dir = ENV['PWD'] || '/'
pubspec_lock_path = MediaKitUtils::find_nearest_pubspec_lock(current_dir)
# If not found try again using `PWD_FALLBACK` env var manually set,
# `PWD_FALLBACK` must point to app folder or any child folder
if pubspec_lock_path == ''
current_dir = ENV['PWD_FALLBACK'] || '/'
pubspec_lock_path = MediaKitUtils::find_nearest_pubspec_lock(current_dir)
end
# Abort if no `pubspec.lock` was found
if pubspec_lock_path == ''
abort(
sprintf(
'media_kit: ERROR: No pubspec.lock was found: ENV["PWD"] = "%s"',
ENV["PWD"]
)
)
end
# Load packages from `pubspec.lock`
pubspec_lock = YAML.load_file(pubspec_lock_path)
packages = pubspec_lock['packages']
# Checks for `media_kit_libs_***` in `pubspec.lock`
libs_count = 0
@libs_package = ''
Type.constants.each do |constant|
type = Type.const_get(constant)
package = sprintf('media_kit_libs_%s_%s', platform, type)
if packages.keys.include?(package)
libs_count += 1
@libs_package = package
puts sprintf('media_kit: INFO: package:%s found', package)
end
end
# Abort if multiple `media_kit_libs_***` was found
if libs_count > 1
abort(
sprintf(
'media_kit: ERROR: package:media_kit_libs_%s_*** must be uniq',
platform,
)
)
end
@libs_found = libs_count > 0
# Warn if no `media_kit_libs_*` was found
if !@libs_found
warn(
sprintf(
'media_kit: WARNING: package:media_kit_libs_%s_*** not found',
platform
)
)
end
end
# Looks for the nearest `pubspec.lock` by recursively ascending through the
# parent folders
def self.find_nearest_pubspec_lock(current_dir)
while current_dir != '/'
path = File.join(current_dir, 'pubspec.lock')
if File.exist?(path)
return path
else
current_dir = File.expand_path('..', current_dir)
end
end
return ''
end
end
@@ -0,0 +1,205 @@
import Flutter
import OpenGLES
public class TextureHW: NSObject, FlutterTexture, ResizableTextureProtocol {
public typealias UpdateCallback = () -> Void
private let handle: OpaquePointer
private let updateCallback: UpdateCallback
private let context: EAGLContext
private let textureCache: CVOpenGLESTextureCache
private var renderContext: OpaquePointer?
private var textureContexts = SwappableObjectManager<TextureGLESContext>(
objects: [],
skipCheckArgs: true
)
init(
handle: OpaquePointer,
updateCallback: @escaping UpdateCallback
) {
self.handle = handle
self.updateCallback = updateCallback
self.context = OpenGLESHelpers.createContext()
self.textureCache = OpenGLESHelpers.createTextureCache(context)
super.init()
self.initMPV()
}
deinit {
disposePixelBuffer()
disposeMPV()
OpenGLESHelpers.deleteTextureCache(textureCache)
// Deleting the context may cause potential RAM or VRAM memory leaks, as it
// is used in the `deinit` method of the `TextureGLESContext`.
// Potential fix: use a counter, and delete it only when the counter reaches
// zero
OpenGLESHelpers.deleteContext(context)
}
public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
let textureContext = textureContexts.current
if textureContext == nil {
return nil
}
return Unmanaged.passRetained(textureContext!.pixelBuffer)
}
private func initMPV() {
EAGLContext.setCurrent(context)
defer {
OpenGLESHelpers.checkError("initMPV")
EAGLContext.setCurrent(nil)
}
let api = UnsafeMutableRawPointer(
mutating: (MPV_RENDER_API_TYPE_OPENGL as NSString).utf8String
)
var procAddress = mpv_opengl_init_params(
get_proc_address: {
(ctx, name) in
return TextureHW.getProcAddress(ctx, name)
},
get_proc_address_ctx: nil
)
var params: [mpv_render_param] = withUnsafeMutableBytes(of: &procAddress) {
procAddress in
return [
mpv_render_param(type: MPV_RENDER_PARAM_API_TYPE, data: api),
mpv_render_param(
type: MPV_RENDER_PARAM_OPENGL_INIT_PARAMS,
data: procAddress.baseAddress.map {
UnsafeMutableRawPointer($0)
}
),
mpv_render_param(),
]
}
MPVHelpers.checkError(
mpv_render_context_create(&renderContext, handle, &params)
)
mpv_render_context_set_update_callback(
renderContext,
{ (ctx) in
let that = unsafeBitCast(ctx, to: TextureHW.self)
DispatchQueue.main.async {
that.updateCallback()
}
},
UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
)
}
private func disposeMPV() {
EAGLContext.setCurrent(context)
defer {
OpenGLESHelpers.checkError("disposeMPV")
EAGLContext.setCurrent(nil)
}
mpv_render_context_set_update_callback(renderContext, nil, nil)
mpv_render_context_free(renderContext)
}
public func resize(_ size: CGSize) {
if size.width == 0 || size.height == 0 {
return
}
NSLog("TextureGL: resize: \(size.width)x\(size.height)")
createPixelBuffer(size)
}
private func createPixelBuffer(_ size: CGSize) {
disposePixelBuffer()
textureContexts.reinit(
objects: [
TextureGLESContext(
context: context,
textureCache: textureCache,
size: size
),
TextureGLESContext(
context: context,
textureCache: textureCache,
size: size
),
TextureGLESContext(
context: context,
textureCache: textureCache,
size: size
),
],
skipCheckArgs: true
)
}
private func disposePixelBuffer() {
textureContexts.reinit(objects: [], skipCheckArgs: true)
}
public func render(_ size: CGSize) {
let textureContext = textureContexts.nextAvailable()
if textureContext == nil {
return
}
EAGLContext.setCurrent(context)
defer {
OpenGLESHelpers.checkError("render")
EAGLContext.setCurrent(nil)
}
glBindFramebuffer(GLenum(GL_FRAMEBUFFER), textureContext!.frameBuffer)
defer {
glBindFramebuffer(GLenum(GL_FRAMEBUFFER), 0)
}
var fbo = mpv_opengl_fbo(
fbo: Int32(textureContext!.frameBuffer),
w: Int32(size.width),
h: Int32(size.height),
internal_format: 0
)
let fboPtr = withUnsafeMutablePointer(to: &fbo) { $0 }
var params: [mpv_render_param] = [
mpv_render_param(type: MPV_RENDER_PARAM_OPENGL_FBO, data: fboPtr),
mpv_render_param(type: MPV_RENDER_PARAM_INVALID, data: nil),
]
mpv_render_context_render(renderContext, &params)
glFlush()
textureContexts.pushAsReady(textureContext!)
}
static private func getProcAddress(
_ ctx: UnsafeMutableRawPointer?,
_ name: UnsafePointer<Int8>?
) -> UnsafeMutableRawPointer? {
let symbol: CFString = CFStringCreateWithCString(
kCFAllocatorDefault,
name,
kCFStringEncodingASCII
)
let indentifier = CFBundleGetBundleWithIdentifier(
"com.apple.opengles" as CFString
)
let addr = CFBundleGetFunctionPointerForName(indentifier, symbol)
if addr == nil {
NSLog("Cannot get OpenGLES function pointer!")
}
return addr
}
}
@@ -0,0 +1,30 @@
public enum MPVHelpers {
public static func checkError(_ status: CInt) {
if status < 0 {
NSLog("MPVHelpers: error: \(String(cString: mpv_error_string(status)))")
exit(1)
}
}
public static func getVideoOutParams(
_ handle: OpaquePointer
) -> MPVVideoOutParams {
var node = mpv_node()
defer {
mpv_free_node_contents(&node)
}
mpv_get_property(handle, "video-out-params", MPV_FORMAT_NODE, &node)
if node.format != MPV_FORMAT_NODE_MAP {
return MPVVideoOutParams.empty
}
let map: mpv_node_list = node.u.list!.pointee
if map.num == 0 {
return MPVVideoOutParams.empty
}
return MPVVideoOutParams.fromMPVNodeList(map)
}
}
@@ -0,0 +1,55 @@
public class MPVVideoOutParams {
public let dw: Int64
public let dh: Int64
public let rotate: Int64
init(
dw: Int64,
dh: Int64,
rotate: Int64
) {
self.dw = dw
self.dh = dh
self.rotate = rotate
}
public static let empty = MPVVideoOutParams(
dw: 0,
dh: 0,
rotate: 0
)
public static func fromMPVNodeList(
_ map: mpv_node_list
) -> MPVVideoOutParams {
var dw: Int64 = 0
var dh: Int64 = 0
var rotate: Int64 = 0
var kptr = map.keys!
var vptr = map.values!
for _ in 0 ..< map.num {
let key = String(cString: kptr.pointee!)
let value: mpv_node = vptr.pointee
kptr = kptr.successor()
vptr = vptr.successor()
if value.format == MPV_FORMAT_INT64 {
if key == "dw" {
dw = value.u.int64
} else if key == "dh" {
dh = value.u.int64
} else if key == "rotate" {
rotate = value.u.int64
}
}
}
return MPVVideoOutParams(
dw: dw,
dh: dh,
rotate: rotate
)
}
}
@@ -0,0 +1,168 @@
#if canImport(Flutter)
import Flutter
#elseif canImport(FlutterMacOS)
import FlutterMacOS
#endif
public class MediaKitVideoPlugin: NSObject, FlutterPlugin {
private static let CHANNEL_NAME = "com.alexmercerind/media_kit_video"
public static func register(with registrar: FlutterPluginRegistrar) {
#if canImport(Flutter)
let binaryMessenger = registrar.messenger()
let registry = registrar.textures()
let utils: UtilsProtocol? = nil
#elseif canImport(FlutterMacOS)
let binaryMessenger = registrar.messenger
let registry = registrar.textures
let utils: UtilsProtocol? = Utils(registrar)
#endif
let channel = FlutterMethodChannel(
name: CHANNEL_NAME,
binaryMessenger: binaryMessenger
)
let instance = MediaKitVideoPlugin(
registry: registry,
channel: channel,
utils: utils
)
registrar.addMethodCallDelegate(instance, channel: channel)
}
private let channel: FlutterMethodChannel
private let videoOutputManager: VideoOutputManager
private let utils: UtilsProtocol?
init(
registry: FlutterTextureRegistry,
channel: FlutterMethodChannel,
utils: UtilsProtocol?
) {
self.channel = channel
videoOutputManager = VideoOutputManager(
registry: registry
)
self.utils = utils
}
public func handle(
_ call: FlutterMethodCall,
result: @escaping FlutterResult
) {
switch call.method {
case "VideoOutputManager.Create":
handleCreateMethodCall(call.arguments, result)
case "VideoOutputManager.SetSize":
handleSetSizeMethodCall(call.arguments, result)
case "VideoOutputManager.Dispose":
handleDisposeMethodCall(call.arguments, result)
case "Utils.EnterNativeFullscreen":
handleEnterNativeFullscreenMethodCall(call.arguments, result)
case "Utils.ExitNativeFullscreen":
handleExitNativeFullscreenMethodCall(call.arguments, result)
default:
result(FlutterMethodNotImplemented)
}
}
private func handleCreateMethodCall(
_ arguments: Any?,
_ result: FlutterResult
) {
let args = arguments as? [String: Any]
let handleStr = args?["handle"] as! String
let handle: Int64? = Int64(handleStr)
let configDict = args?["configuration"] as! [String: Any]
let configuration = VideoOutputConfiguration.fromDict(configDict)
assert(handle != nil, "handle must be an Int64")
videoOutputManager.create(
handle: handle!,
configuration: configuration,
textureUpdateCallback: { (_ textureId: Int64, _ size: CGSize) in
self.channel.invokeMethod(
"VideoOutput.Resize",
arguments: [
"handle": handle!,
"id": textureId,
"rect": [
"top": 0,
"left": 0,
"width": size.width,
"height": size.height,
],
] as [String: Any]
)
}
)
result(nil)
}
private func handleSetSizeMethodCall(
_ arguments: Any?,
_ result: FlutterResult
) {
let args = arguments as? [String: Any]
let handleStr = args?["handle"] as! String
let widthStr = args?["width"] as! String
let heightStr = args?["height"] as! String
let handle: Int64? = Int64(handleStr)
let width: Int64? = Int64(widthStr)
let height: Int64? = Int64(heightStr)
assert(handle != nil, "handle must be an Int64")
self.videoOutputManager.setSize(
handle: handle!,
width: width,
height: height
)
result(nil)
}
private func handleDisposeMethodCall(
_ arguments: Any?,
_ result: FlutterResult
) {
let args = arguments as? [String: Any]
let handleStr = args?["handle"] as! String
let handle: Int64? = Int64(handleStr)
assert(handle != nil, "handle must be an Int64")
videoOutputManager.destroy(
handle: handle!
)
result(nil)
}
private func handleEnterNativeFullscreenMethodCall(
_: Any?,
_ result: FlutterResult
) {
if utils == nil {
return result(FlutterMethodNotImplemented)
}
utils?.enterNativeFullscreen()
result(nil)
}
private func handleExitNativeFullscreenMethodCall(
_: Any?,
_ result: FlutterResult
) {
if utils == nil {
return result(FlutterMethodNotImplemented)
}
utils?.exitNativeFullscreen()
result(nil)
}
}
@@ -0,0 +1,10 @@
#if canImport(Flutter)
import Flutter
#elseif canImport(FlutterMacOS)
import FlutterMacOS
#endif
public protocol ResizableTextureProtocol: NSObject, FlutterTexture {
func resize(_ size: CGSize)
func render(_ size: CGSize)
}
@@ -0,0 +1,44 @@
#if canImport(Flutter)
import Flutter
#elseif canImport(FlutterMacOS)
import FlutterMacOS
#endif
// This class avoids data race when called from a thread
public class SafeResizableTexture:
NSObject,
FlutterTexture,
ResizableTextureProtocol
{
private let lock = NSRecursiveLock()
private let child: ResizableTextureProtocol
init(_ child: ResizableTextureProtocol) {
self.child = child
}
public func resize(_ size: CGSize) {
return locked {
return child.resize(size)
}
}
public func render(_ size: CGSize) {
return locked {
return child.render(size)
}
}
public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
return child.copyPixelBuffer()
}
private func locked<T>(do block: () -> T) -> T {
lock.lock()
defer {
lock.unlock()
}
return block()
}
}
@@ -0,0 +1,106 @@
// This class was created to prevent a frameBuffer read by Flutter from being
// concurrently modified by a write method (mpv's renderer).
//
// To do this, two pools are set up, one for frameBuffers available for writing,
// the other for frameBuffers ready to be read by Flutter.
//
// When a frameBuffer has finished being modified and is "pushed", the oldest
// ready frameBuffer is marked as the current frameBuffer, and the oldest
// current frameBuffer is placed in the pool of frameBuffers available for
// writing.
//
// The use of at least three frameBuffers ensures that the read and write phases
// do not overlap, thus eliminating flicker.
public class SwappableObjectManager<T> {
private let lock: NSRecursiveLock = NSRecursiveLock()
private var available: [T]
private var ready: [T] = []
private var _current: T?
init(objects: [T], skipCheckArgs: Bool = false) {
if !skipCheckArgs {
SwappableObjectManager.checkArgs(objects)
}
available = objects
}
public func reinit(objects: [T], skipCheckArgs: Bool = false) {
if !skipCheckArgs {
SwappableObjectManager.checkArgs(objects)
}
lock.lock()
defer {
lock.unlock()
}
available = objects
ready = []
_current = nil
}
public func nextAvailable() -> T? {
lock.lock()
defer {
lock.unlock()
}
let object: T? =
available.count > 0
? available.removeFirst()
: nil
return object
}
public func pushAsReady(_ object: T) {
lock.lock()
defer {
lock.unlock()
}
ready.append(object)
updateCurrent()
}
public var current: T? {
lock.lock()
defer {
lock.unlock()
}
return _current
}
private func updateCurrent() {
lock.lock()
defer {
lock.unlock()
}
let next: T? =
ready.count > 0
? ready.removeFirst()
: nil
if next == nil {
return
}
let old: T? = _current
_current = next
if old == nil {
return
}
available.append(old!)
}
static private func checkArgs(_ objects: [T]) {
if objects.count < 2 {
NSLog("SwappableObjectManager: require at least two objects to work")
}
}
}
@@ -0,0 +1,146 @@
#if canImport(Flutter)
import Flutter
#elseif canImport(FlutterMacOS)
import FlutterMacOS
#endif
public class TextureSW: NSObject, FlutterTexture, ResizableTextureProtocol {
public typealias UpdateCallback = () -> Void
private let handle: OpaquePointer
private let updateCallback: UpdateCallback
private var renderContext: OpaquePointer?
private var textureContexts = SwappableObjectManager<TextureSWContext>(
objects: [],
skipCheckArgs: true
)
init(
handle: OpaquePointer,
updateCallback: @escaping UpdateCallback
) {
self.handle = handle
self.updateCallback = updateCallback
super.init()
DispatchQueue.main.async {
self.initMPV()
}
}
deinit {
disposePixelBuffer()
disposeMPV()
}
public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
let textureContext = textureContexts.current
if textureContext == nil {
return nil
}
return Unmanaged.passRetained(textureContext!.pixelBuffer)
}
private func initMPV() {
let api = UnsafeMutableRawPointer(
mutating: (MPV_RENDER_API_TYPE_SW as NSString).utf8String
)
var params: [mpv_render_param] = [
mpv_render_param(type: MPV_RENDER_PARAM_API_TYPE, data: api),
mpv_render_param(type: MPV_RENDER_PARAM_INVALID, data: nil),
]
MPVHelpers.checkError(
mpv_render_context_create(&renderContext, handle, &params)
)
mpv_render_context_set_update_callback(
renderContext,
{ (ctx) in
let that = unsafeBitCast(ctx, to: TextureSW.self)
DispatchQueue.main.async {
that.updateCallback()
}
},
UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
)
}
private func disposeMPV() {
mpv_render_context_set_update_callback(renderContext, nil, nil)
mpv_render_context_free(renderContext)
}
public func resize(_ size: CGSize) {
if size.width == 0 || size.height == 0 {
return
}
NSLog("TextureSW: resize: \(size.width)x\(size.height)")
createPixelBuffer(size)
}
private func createPixelBuffer(_ size: CGSize) {
disposePixelBuffer()
textureContexts.reinit(
objects: [
TextureSWContext(size: size),
TextureSWContext(size: size),
TextureSWContext(size: size),
],
skipCheckArgs: true
)
}
private func disposePixelBuffer() {
textureContexts.reinit(objects: [], skipCheckArgs: true)
}
public func render(_ size: CGSize) {
let textureContext = textureContexts.nextAvailable()
if textureContext == nil {
return
}
CVPixelBufferLockBaseAddress(
textureContext!.pixelBuffer,
CVPixelBufferLockFlags(rawValue: 0)
)
defer {
CVPixelBufferUnlockBaseAddress(
textureContext!.pixelBuffer,
CVPixelBufferLockFlags(rawValue: 0)
)
}
var ssize: [Int32] = [Int32(size.width), Int32(size.height)]
let format: String = "bgr0"
var pitch: Int = CVPixelBufferGetBytesPerRow(textureContext!.pixelBuffer)
let buffer = CVPixelBufferGetBaseAddress(textureContext!.pixelBuffer)
// pointers
let ssizePtr = ssize.withUnsafeMutableBytes {
$0.baseAddress?.assumingMemoryBound(to: Int32.self)
}
let formatPtr = UnsafeMutablePointer(
mutating: (format as NSString).utf8String
)
let pitchPtr = withUnsafeMutablePointer(to: &pitch) { $0 }
let bufferPtr = buffer!.assumingMemoryBound(to: UInt8.self)
var params: [mpv_render_param] = [
mpv_render_param(type: MPV_RENDER_PARAM_SW_SIZE, data: ssizePtr),
mpv_render_param(type: MPV_RENDER_PARAM_SW_FORMAT, data: formatPtr),
mpv_render_param(type: MPV_RENDER_PARAM_SW_STRIDE, data: pitchPtr),
mpv_render_param(type: MPV_RENDER_PARAM_SW_POINTER, data: bufferPtr),
mpv_render_param(type: MPV_RENDER_PARAM_INVALID, data: nil),
]
mpv_render_context_render(renderContext, &params)
textureContexts.pushAsReady(textureContext!)
}
}
@@ -0,0 +1,36 @@
public class TextureSWContext {
public let pixelBuffer: CVPixelBuffer
init(size: CGSize) {
self.pixelBuffer = TextureSWContext.createPixelBuffer(size)
}
deinit {
TextureSWContext.disposePixelBuffer(pixelBuffer)
}
private static func createPixelBuffer(_ size: CGSize) -> CVPixelBuffer {
let attrs =
[
kCVPixelBufferMetalCompatibilityKey: true
] as CFDictionary
var pixelBuffer: CVPixelBuffer?
let cvret = CVPixelBufferCreate(
kCFAllocatorDefault,
Int(size.width),
Int(size.height),
kCVPixelFormatType_32BGRA,
attrs,
&pixelBuffer
)
assert(cvret == kCVReturnSuccess, "CVPixelBufferCreate")
return pixelBuffer!
}
private static func disposePixelBuffer(_ pixelBuffer: CVPixelBuffer) {
// 'CVPixelBufferRelease' is unavailable: Core Foundation objects are
// automatically memory managed
}
}
@@ -0,0 +1,4 @@
public protocol UtilsProtocol: NSObject {
func enterNativeFullscreen()
func exitNativeFullscreen()
}
@@ -0,0 +1,199 @@
#if canImport(Flutter)
import Flutter
#elseif canImport(FlutterMacOS)
import FlutterMacOS
#endif
// This class creates and manipulates the different types of FlutterTexture,
// handles resizing, rendering calls, and notify Flutter when a new frame is
// available to render.
//
// To improve the user experience, a worker is used to execute heavy tasks on a
// dedicated thread.
public class VideoOutput: NSObject {
// Will be called on the main thread
public typealias TextureUpdateCallback = (Int64, CGSize) -> Void
private static let isSimulator: Bool = {
let isSim: Bool
#if targetEnvironment(simulator)
isSim = true
#else
isSim = false
#endif
return isSim
}()
private let handle: OpaquePointer
private let enableHardwareAcceleration: Bool
private let registry: FlutterTextureRegistry
private let textureUpdateCallback: TextureUpdateCallback
private let worker: Worker = .init()
private var width: Int64?
private var height: Int64?
private var texture: ResizableTextureProtocol!
private var textureId: Int64 = -1
private var currentSize: CGSize = CGSize.zero
private var disposed: Bool = false
init(
handle: Int64,
configuration: VideoOutputConfiguration,
registry: FlutterTextureRegistry,
textureUpdateCallback: @escaping TextureUpdateCallback
) {
let handle = OpaquePointer(bitPattern: Int(handle))
assert(handle != nil, "handle casting")
self.handle = handle!
width = configuration.width
height = configuration.height
enableHardwareAcceleration = configuration.enableHardwareAcceleration
self.registry = registry
self.textureUpdateCallback = textureUpdateCallback
super.init()
worker.enqueue {
self._init()
}
}
deinit {
worker.cancel()
disposed = true
disposeTextureId()
}
public func setSize(width: Int64?, height: Int64?) {
worker.enqueue {
self.width = width
self.height = height
}
}
private func _init() {
let enableHardwareAcceleration =
VideoOutput.isSimulator ? false : enableHardwareAcceleration
NSLog(
"VideoOutput: enableHardwareAcceleration: \(enableHardwareAcceleration)"
)
if VideoOutput.isSimulator {
NSLog(
"VideoOutput: warning: hardware rendering is disabled in the iOS simulator, due to an incompatibility with OpenGL ES"
)
}
if enableHardwareAcceleration {
texture = SafeResizableTexture(
TextureHW(
handle: handle,
// Use `weak self` to prevent memory leaks
updateCallback: { [weak self]() in
guard let that = self else {
return
}
that.updateCallback()
}
)
)
} else {
texture = SafeResizableTexture(
TextureSW(
handle: handle,
// Use `weak self` to prevent memory leaks
updateCallback: { [weak self]() in
guard let that = self else {
return
}
that.updateCallback()
}
)
)
}
DispatchQueue.main.sync { [weak self]() in
guard let that = self else {
return
}
that.registerTextureId()
}
}
// Must be run on the main thread
private func registerTextureId() {
// Textures must be registered on the platform thread.
textureId = registry.register(texture)
// textureUpdateCallback must run on the main thread
textureUpdateCallback(textureId, CGSize(width: 0, height: 0))
}
private func disposeTextureId() {
let registry_ = self.registry
let textureId_ = self.textureId
textureId = -1
DispatchQueue.main.async {
// Textures must be unregistered on the platform thread
registry_.unregisterTexture(textureId_)
}
}
public func updateCallback() {
worker.enqueue {
self._updateCallback()
}
}
private func _updateCallback() {
let size = videoSize
if size.width == 0 || size.height == 0 {
return
}
if currentSize != size {
currentSize = size
texture.resize(size)
DispatchQueue.main.sync { [weak self] in
guard let that = self else { return }
// textureUpdateCallback must run on the main thread
that.textureUpdateCallback(that.textureId, size)
}
}
if disposed {
return
}
texture.render(size)
DispatchQueue.main.sync { [weak self] in
guard let that = self else { return }
// Textures must be marked as available from the main thread
that.registry.textureFrameAvailable(that.textureId)
}
}
private var videoSize: CGSize {
// fixed size
if width != nil && height != nil {
return CGSize(
width: Double(width!),
height: Double(height!)
)
}
let params = MPVHelpers.getVideoOutParams(handle)
return CGSize(
width: Double(width ?? (params.rotate == 0 || params.rotate == 180
? params.dw
: params.dh)),
height: Double(height ?? (params.rotate == 0 || params.rotate == 180
? params.dh
: params.dw))
)
}
}
@@ -0,0 +1,33 @@
public class VideoOutputConfiguration {
public let width: Int64?
public let height: Int64?
public let enableHardwareAcceleration: Bool
init(
width: Int64?,
height: Int64?,
enableHardwareAcceleration: Bool
) {
self.width = width
self.height = height
self.enableHardwareAcceleration = enableHardwareAcceleration
}
public static func fromDict(_ dict: [String: Any])
-> VideoOutputConfiguration
{
let widthStr = dict["width"] as! String
let heightStr = dict["height"] as! String
let enableHardwareAcceleration =
dict["enableHardwareAcceleration"] as! Bool
let width: Int64? = Int64(widthStr)
let height: Int64? = Int64(heightStr)
return VideoOutputConfiguration(
width: width,
height: height,
enableHardwareAcceleration: enableHardwareAcceleration
)
}
}
@@ -0,0 +1,56 @@
#if canImport(Flutter)
import Flutter
#elseif canImport(FlutterMacOS)
import FlutterMacOS
#endif
public class VideoOutputManager: NSObject {
private let registry: FlutterTextureRegistry
private var videoOutputs = [Int64: VideoOutput]()
init(registry: FlutterTextureRegistry) {
self.registry = registry
}
public func create(
handle: Int64,
configuration: VideoOutputConfiguration,
textureUpdateCallback: @escaping VideoOutput.TextureUpdateCallback
) {
let videoOutput = VideoOutput(
handle: handle,
configuration: configuration,
registry: self.registry,
textureUpdateCallback: textureUpdateCallback
)
self.videoOutputs[handle] = videoOutput
}
public func setSize(
handle: Int64,
width: Int64?,
height: Int64?
) {
let videoOutput = self.videoOutputs[handle]
if videoOutput == nil {
return
}
videoOutput!.setSize(
width: width,
height: height
)
}
public func destroy(
handle: Int64
) {
let videoOutput = self.videoOutputs[handle]
if videoOutput == nil {
return
}
self.videoOutputs[handle] = nil
}
}
@@ -0,0 +1,74 @@
// https://stackoverflow.com/questions/49043257/how-to-ensure-to-run-some-code-on-same-background-thread/49075382#49075382
class Worker {
public typealias Job = () -> Void
private let semaphore = DispatchSemaphore(value: 0)
private let lock = NSRecursiveLock()
private var thread: Thread!
private var queue = [Job]()
private var canceled: Bool = false
init() {
thread = Thread(block: loop)
thread.start()
}
public func cancel() {
signalCancel()
thread.cancel()
}
public func enqueue(_ job: @escaping Job) {
locked {
queue.append(job)
}
semaphore.signal()
}
private func loop() {
while true {
semaphore.wait()
if isCanceled() {
return
}
let job = getFirstJob()
job()
}
}
private func signalCancel() {
locked {
canceled = true
}
semaphore.signal()
}
private func isCanceled() -> Bool {
let c = locked {
canceled
}
return c
}
private func getFirstJob() -> Job {
let job = locked {
queue.removeFirst()
}
return job
}
private func locked<T>(do block: () -> T) -> T {
lock.lock()
defer {
lock.unlock()
}
return block()
}
}
@@ -0,0 +1,181 @@
import OpenGLES
import UIKit
public class OpenGLESHelpers {
static public func createContext() -> EAGLContext {
let context = EAGLContext(api: .openGLES3)
return context!
}
static public func createTextureCache(
_ context: EAGLContext
) -> CVOpenGLESTextureCache {
var textureCache: CVOpenGLESTextureCache?
let cvret: CVReturn = CVOpenGLESTextureCacheCreate(
kCFAllocatorDefault,
nil,
context,
nil,
&textureCache
)
assert(cvret == kCVReturnSuccess, "CVOpenGLESTextureCacheCreate")
return textureCache!
}
static public func createPixelBuffer(_ size: CGSize) -> CVPixelBuffer {
var pixelBuffer: CVPixelBuffer?
let attrs =
[
kCVPixelBufferMetalCompatibilityKey: true
] as CFDictionary
let cvret: CVReturn = CVPixelBufferCreate(
kCFAllocatorDefault,
Int(size.width),
Int(size.height),
kCVPixelFormatType_32BGRA,
attrs,
&pixelBuffer
)
assert(cvret == kCVReturnSuccess, "CVPixelBufferCreate")
return pixelBuffer!
}
static public func createTexture(
_ textureCache: CVOpenGLESTextureCache,
_ pixelBuffer: CVPixelBuffer,
_ size: CGSize
) -> CVOpenGLESTexture {
var texture: CVOpenGLESTexture?
let cvret: CVReturn = CVOpenGLESTextureCacheCreateTextureFromImage(
kCFAllocatorDefault,
textureCache,
pixelBuffer,
nil,
GLenum(GL_TEXTURE_2D),
GL_RGBA,
GLsizei(size.width),
GLsizei(size.height),
GLenum(GL_BGRA),
GLenum(GL_UNSIGNED_BYTE),
0,
&texture
)
assert(
cvret == kCVReturnSuccess,
"CVOpenGLESTextureCacheCreateTextureFromImage"
)
return texture!
}
public enum FrameBufferError: Error {
case status(status: GLuint)
}
static public func createFrameBuffer(
context: EAGLContext,
texture: CVOpenGLESTexture,
size: CGSize
) throws -> GLuint {
EAGLContext.setCurrent(context)
defer {
OpenGLESHelpers.checkError("createFrameBuffer")
EAGLContext.setCurrent(nil)
}
let textureName: GLuint = CVOpenGLESTextureGetName(texture)
glBindTexture(GLenum(GL_TEXTURE_2D), textureName)
defer {
glBindTexture(GLenum(GL_TEXTURE_2D), 0)
}
var frameBuffer: GLuint = 0
glGenFramebuffers(1, &frameBuffer)
glBindFramebuffer(GLenum(GL_FRAMEBUFFER), frameBuffer)
defer {
glBindFramebuffer(GLenum(GL_FRAMEBUFFER), 0)
}
glFramebufferTexture2D(
GLenum(GL_FRAMEBUFFER),
GLenum(GL_COLOR_ATTACHMENT0),
GLenum(GL_TEXTURE_2D),
textureName,
0
)
let status = glCheckFramebufferStatus(GLenum(GL_FRAMEBUFFER))
if status != GLenum(GL_FRAMEBUFFER_COMPLETE) {
glDeleteFramebuffers(1, &frameBuffer)
throw OpenGLESHelpers.FrameBufferError.status(status: status)
}
return frameBuffer
}
static public func deleteContext(_ context: EAGLContext) {
// Release function for EAGLContext not found
}
static public func deleteTextureCache(_ textureCache: CVOpenGLESTextureCache) {
CVOpenGLESTextureCacheFlush(textureCache, 0)
// 'CVOpenGLESTextureCacheRelease' is unavailable: Core Foundation objects are
// automatically memory managed
}
static public func deletePixeBuffer(
_ context: EAGLContext,
_ pixelBuffer: CVPixelBuffer
) {
// 'CVPixelBufferRelease' is unavailable: Core Foundation objects are
// automatically memory managed
}
// BUG: `glDeleteTextures` does not release `CVOpenGLESTexture`.
// `CVOpenGLESTextureCache` retains a direct or indirect reference to
// `IOSurface`, which causes a memory leak until `CVOpenGLESTextureCache` is
// released.
static public func deleteTexture(
_ context: EAGLContext,
_ texture: CVOpenGLESTexture
) {
EAGLContext.setCurrent(context)
defer {
OpenGLESHelpers.checkError("deleteTexture")
EAGLContext.setCurrent(nil)
}
var textureName: GLuint = CVOpenGLESTextureGetName(texture)
glDeleteTextures(1, &textureName)
}
static public func deleteFrameBuffer(
_ context: EAGLContext,
_ frameBuffer: GLuint
) {
EAGLContext.setCurrent(context)
defer {
OpenGLESHelpers.checkError("deleteFrameBuffer")
EAGLContext.setCurrent(nil)
}
var frameBuffer = frameBuffer
glDeleteFramebuffers(1, &frameBuffer)
}
static public func checkError(_ message: String) {
let error = glGetError()
if error == GL_NO_ERROR {
return
}
NSLog("OpenGLESHelpers: error: \(message): \(error)")
}
}
@@ -0,0 +1,54 @@
public class TextureGLESContext {
private let context: EAGLContext
public let frameBuffer: GLuint
public let texture: CVOpenGLESTexture
public let pixelBuffer: CVPixelBuffer
init(
context: EAGLContext,
textureCache: CVOpenGLESTextureCache,
size: CGSize
) {
self.context = context
// Sometimes the frameBuffer created by `createFrameBuffer` has the status
// `GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT`, which makes it unusable.
// This seems to be because the pixelBuffer created by `createPixelBuffer`
// appears to be invalid, with no way to prevent or detect this.
// So we have to try several times to create a frameBuffer valid.
// This is usually the case after the second attempt.
for index in 0...3 {
let pixelBuffer = OpenGLESHelpers.createPixelBuffer(size)
let texture = OpenGLESHelpers.createTexture(
textureCache,
pixelBuffer,
size
)
let frameBuffer = try? OpenGLESHelpers.createFrameBuffer(
context: context,
texture: texture,
size: size
)
if frameBuffer != nil {
self.pixelBuffer = pixelBuffer
self.texture = texture
self.frameBuffer = frameBuffer!
return
}
OpenGLESHelpers.deletePixeBuffer(context, pixelBuffer)
OpenGLESHelpers.deleteTexture(context, texture)
}
NSLog("TextureGLESContext: init: unable to create a valid frameBuffer")
exit(1)
}
deinit {
OpenGLESHelpers.deletePixeBuffer(context, pixelBuffer)
OpenGLESHelpers.deleteTexture(context, texture)
OpenGLESHelpers.deleteFrameBuffer(context, frameBuffer)
}
}
@@ -0,0 +1,9 @@
#if canImport(Flutter)
import Flutter
#elseif canImport(FlutterMacOS)
import FlutterMacOS
#endif
public class MediaKitVideoPlugin: NSObject, FlutterPlugin {
public static func register(with _: FlutterPluginRegistrar) {}
}
+50
View File
@@ -0,0 +1,50 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint media_kit_video.podspec` to validate before publishing.
#
require_relative '../common/darwin/Podspec/media_kit_utils.rb'
Pod::Spec.new do |s|
# Setup required files
system("make -C ../common/darwin HEADERS_DESTDIR=\"$(pwd)/Headers\"")
# Initialize `MediaKitUtils`
mku = MediaKitUtils.new(MediaKitUtils::Platform::IOS)
s.name = 'media_kit_video'
s.version = '0.0.1'
s.summary = 'Native implementation for video playback in package:media_kit'
s.description = <<-DESC
Native implementation for video playback in package:media_kit.
DESC
s.homepage = 'https://github.com/media-kit/media-kit.git'
s.license = { :file => '../LICENSE' }
s.author = { 'Hitesh Kumar Saini' => 'saini123hitesh@gmail.com' }
s.source = { :path => '.' }
s.platform = :ios, '9.0'
s.swift_version = '5.0'
s.dependency 'Flutter'
if mku.libs_found
# Define paths to frameworks dir
framework_search_paths_iphoneos = sprintf('$(PROJECT_DIR)/../.symlinks/plugins/%s/ios/Frameworks/.symlinks/mpv/ios', mku.libs_package)
framework_search_paths_iphonesimulator = sprintf('$(PROJECT_DIR)/../.symlinks/plugins/%s/ios/Frameworks/.symlinks/mpv/ios-simulator', mku.libs_package)
s.source_files = 'Classes/plugin/**/*.swift', 'Headers/**/*.h'
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
'GCC_WARN_INHIBIT_ALL_WARNINGS' => 'YES',
'GCC_PREPROCESSOR_DEFINITIONS' => '"$(inherited)" GL_SILENCE_DEPRECATION COREVIDEO_SILENCE_GL_DEPRECATION',
'FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]' => sprintf('"$(inherited)" "%s"', framework_search_paths_iphoneos),
'FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]' => sprintf('"$(inherited)" "%s"', framework_search_paths_iphonesimulator),
'OTHER_LDFLAGS' => '"$(inherited)" -framework Mpv',
# Flutter.framework does not contain a i386 slice.
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386',
}
else
s.source_files = 'Classes/stub/**/*.swift'
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
end
end
+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;
}
+99
View File
@@ -0,0 +1,99 @@
# 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.
cmake_minimum_required(VERSION 3.10)
set(PROJECT_NAME "media_kit_video")
set(CMAKE_CXX_STANDARD 17)
project(${PROJECT_NAME} LANGUAGES CXX)
set(PLUGIN_NAME "media_kit_video_plugin")
if(MEDIA_KIT_LIBS_AVAILABLE)
add_library(
${PLUGIN_NAME} SHARED
"media_kit_video_plugin.cc"
"texture_gl.cc"
"texture_sw.cc"
"video_output_manager.cc"
"video_output.cc"
"utils.cc"
)
apply_standard_settings(${PLUGIN_NAME})
# Check for libmpv & epoxy headers & libraries.
find_package(PkgConfig REQUIRED)
pkg_check_modules(mpv IMPORTED_TARGET mpv)
pkg_check_modules(epoxy IMPORTED_TARGET epoxy)
set_target_properties(
${PLUGIN_NAME} PROPERTIES
CXX_VISIBILITY_PRESET hidden
)
target_compile_definitions(
${PLUGIN_NAME} PRIVATE
FLUTTER_PLUGIN_IMPL
)
target_compile_options(${PLUGIN_NAME} PRIVATE "${mpv_CFLAGS_OTHER}")
target_compile_options(${PLUGIN_NAME} PRIVATE "${epoxy_CFLAGS_OTHER}")
target_include_directories(
${PLUGIN_NAME} INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/include"
"${mpv_INCLUDE_DIRS}"
"${epoxy_INCLUDE_DIRS}"
)
target_link_libraries(
${PLUGIN_NAME} PRIVATE
flutter
PkgConfig::GTK
PkgConfig::mpv
PkgConfig::epoxy
)
else()
message(NOTICE "media_kit: WARNING: package:media_kit_libs_*** not found.")
add_library(
${PLUGIN_NAME} SHARED
"media_kit_video_plugin.cc"
)
apply_standard_settings(${PLUGIN_NAME})
set_target_properties(
${PLUGIN_NAME} PROPERTIES
CXX_VISIBILITY_PRESET hidden
)
target_compile_definitions(
${PLUGIN_NAME} PRIVATE
FLUTTER_PLUGIN_IMPL
# Add macro to indicate that package:media_kit_libs_*** is not available.
"MEDIA_KIT_LIBS_NOT_FOUND=1"
)
target_include_directories(
${PLUGIN_NAME} INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/include"
)
target_link_libraries(
${PLUGIN_NAME} PRIVATE
flutter
PkgConfig::GTK
)
endif()
set(
media_kit_video_bundled_libraries
""
PARENT_SCOPE
)
@@ -0,0 +1,34 @@
// 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.
#ifndef MEDIA_KIT_VIDEO_PLUGIN_H_
#define MEDIA_KIT_VIDEO_PLUGIN_H_
#include <flutter_linux/flutter_linux.h>
G_BEGIN_DECLS
#ifdef FLUTTER_PLUGIN_IMPL
#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default")))
#else
#define FLUTTER_PLUGIN_EXPORT
#endif
typedef struct _MediaKitVideoPlugin MediaKitVideoPlugin;
typedef struct {
GObjectClass parent_class;
} MediaKitVideoPluginClass;
FLUTTER_PLUGIN_EXPORT GType media_kit_video_plugin_get_type();
FLUTTER_PLUGIN_EXPORT void media_kit_video_plugin_register_with_registrar(
FlPluginRegistrar* registrar);
G_END_DECLS
#endif // FLUTTER_PLUGIN_MEDIA_KIT_VIDEO_PLUGIN_H_
@@ -0,0 +1,35 @@
// 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.
#ifndef TEXTURE_GL_H_
#define TEXTURE_GL_H_
#include <flutter_linux/flutter_linux.h>
#include "video_output.h"
#define TEXTURE_GL_TYPE (texture_gl_get_type())
G_DECLARE_FINAL_TYPE(TextureGL, texture_gl, TEXTURE_GL, TEXTURE_GL, FlTextureGL)
#define TEXTURE_GL(obj) \
(G_TYPE_CHECK_INSTANCE_CAST((obj), texture_gl_get_type(), TextureGL))
TextureGL* texture_gl_new(VideoOutput* video_output);
/**
* @brief Populates texture with video frame.
*/
gboolean texture_gl_populate_texture(FlTextureGL* texture,
guint32* target,
guint32* name,
guint32* width,
guint32* height,
GError** error);
#endif // TEXTURE_GL_H_
@@ -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.
#ifndef TEXTURE_SW_H_
#define TEXTURE_SW_H_
#include <flutter_linux/flutter_linux.h>
#include "video_output.h"
#define TEXTURE_SW_TYPE (texture_sw_get_type())
G_DECLARE_FINAL_TYPE(TextureSW,
texture_sw,
TEXTURE_SW,
TEXTURE_SW,
FlPixelBufferTexture)
#define TEXTURE_SW(obj) \
(G_TYPE_CHECK_INSTANCE_CAST((obj), texture_sw_get_type(), TextureSW))
TextureSW* texture_sw_new(VideoOutput* video_output);
gboolean texture_sw_copy_pixels(FlPixelBufferTexture* texture,
const guint8** buffer,
guint32* width,
guint32* height,
GError** error);
// Limit the frame size to 1080p in software rendering.
// This is for performance reasons & to avoid allocating too much memory.
#define SW_RENDERING_MAX_WIDTH 1920
#define SW_RENDERING_MAX_HEIGHT 1080
#define SW_RENDERING_PIXEL_BUFFER_SIZE \
(SW_RENDERING_MAX_WIDTH) * (SW_RENDERING_MAX_HEIGHT) * (4)
#endif // TEXTURE_SW_H_
@@ -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.
#include <gtk/gtk.h>
void utils_enter_native_fullscreen(GtkWidget* window);
void utils_exit_native_fullscreen(GtkWidget* window);
@@ -0,0 +1,133 @@
// 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.
#ifndef VIDEO_OUTPUT_H_
#define VIDEO_OUTPUT_H_
#include <flutter_linux/flutter_linux.h>
#include <epoxy/egl.h>
#include <epoxy/glx.h>
#include "mpv/client.h"
#include "mpv/render.h"
#include "mpv/render_gl.h"
typedef struct _VideoOutputConfiguration {
gint64 width;
gint64 height;
bool enable_hardware_acceleration;
_VideoOutputConfiguration(gint64 width = NULL,
gint64 height = NULL,
bool enable_hardware_acceleration = true)
: width(width),
height(height),
enable_hardware_acceleration(enable_hardware_acceleration) {}
} VideoOutputConfiguration;
// Callback invoked when the texture ID updates i.e. video dimensions changes.
typedef void (*TextureUpdateCallback)(gint64 id,
gint64 width,
gint64 height,
gpointer context);
#define VIDEO_OUTPUT_TYPE (video_output_get_type())
G_DECLARE_FINAL_TYPE(VideoOutput,
video_output,
VIDEO_OUTPUT,
VIDEO_OUTPUT,
GObject)
#define VIDEO_OUTPUT(obj) \
(G_TYPE_CHECK_INSTANCE_CAST((obj), video_output_get_type(), VideoOutput))
/**
* @brief Creates a new |VideoOutput| instance for given |handle|.
*
* @param texture_registrar |FlTextureRegistrar| reference.
* @param handle |mpv_handle| reference casted to gint64.
* @param width Preferred width of the video. Pass `NULL` for using texture
* dimensions based on video's resolution.
* @param height Preferred height of the video. Pass `NULL` for using texture
* dimensions based on video's resolution.
* @param enable_hardware_acceleration Whether to enable hardware acceleration.
* @return VideoOutput*
*/
VideoOutput* video_output_new(FlTextureRegistrar* texture_registrar,
FlView* view,
gint64 handle,
VideoOutputConfiguration configuration);
/**
* @brief Sets the callback invoked when the texture ID updates i.e. video
* dimensions changes.
*
* @param self |VideoOutput| reference.
* @param texture_update_callback Callback.
* @param texture_update_callback_context Callback context.
*/
void video_output_set_texture_update_callback(
VideoOutput* self,
TextureUpdateCallback texture_update_callback,
gpointer texture_update_callback_context);
/**
* @brief Sets the required video output size. This forces |VideoOutput| to
* resize the internal OpenGL surface / texture.
*
* @param texture_registrar |FlTextureRegistrar| reference.
* @param width Preferred width of the video. Pass `NULL` for using texture
* dimensions based on video's resolution.
* @param height Preferred height of the video. Pass `NULL` for using texture
* dimensions based on video's resolution.
*/
void video_output_set_size(VideoOutput* self, gint64 width, gint64 height);
mpv_render_context* video_output_get_render_context(VideoOutput* self);
GdkGLContext* video_output_get_gdk_gl_context(VideoOutput* self);
EGLDisplay video_output_get_egl_display(VideoOutput* self);
// GDK's GLX context of the calling thread, set aside while an EGL context
// is used there.
typedef struct {
Display* display;
GLXDrawable draw;
GLXDrawable read;
GLXContext context;
} VideoOutputGlxState;
void video_output_glx_release(VideoOutputGlxState* state);
void video_output_glx_restore(VideoOutputGlxState* state);
// Makes |context| current again, or, when it is EGL_NO_CONTEXT, releases
// whatever context of |own_display| this thread holds.
void video_output_restore_egl_context(EGLDisplay own_display,
EGLDisplay display,
EGLSurface draw,
EGLSurface read,
EGLContext context);
EGLContext video_output_get_egl_context(VideoOutput* self);
EGLSurface video_output_get_egl_surface(VideoOutput* self);
guint8* video_output_get_pixel_buffer(VideoOutput* self);
gint64 video_output_get_width(VideoOutput* self);
gint64 video_output_get_height(VideoOutput* self);
gint64 video_output_get_texture_id(VideoOutput* self);
void video_output_notify_texture_update(VideoOutput* self);
#endif // VIDEO_OUTPUT_H_
@@ -0,0 +1,75 @@
// 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.
#ifndef VIDEO_OUTPUT_MANAGER_H_
#define VIDEO_OUTPUT_MANAGER_H_
#include "video_output.h"
#define VIDEO_OUTPUT_MANAGER_TYPE (video_output_manager_get_type())
// Creates & disposes |VideoOutput| instances for video embedding.
G_DECLARE_FINAL_TYPE(VideoOutputManager,
video_output_manager,
VIDEO_OUTPUT_MANAGER,
VIDEO_OUTPUT_MANAGER,
GObject)
#define VIDEO_OUTPUT_MANAGER(obj) \
(G_TYPE_CHECK_INSTANCE_CAST((obj), video_output_manager_get_type(), \
VideoOutputManager))
VideoOutputManager* video_output_manager_new(
FlTextureRegistrar* texture_registrar,
FlView* view);
/**
* @brief Creates a new |VideoOutput| instance for given |handle|.
*
* @param self |VideoOutputManager| reference.
* @param handle |mpv_handle| reference casted to gint64.
* @param width Preferred width of the video. Pass `NULL` for using texture
* dimensions based on video's resolution.
* @param height Preferred height of the video. Pass `NULL` for using texture
* dimensions based on video's resolution.
* @param enable_hardware_acceleration Whether to enable hardware acceleration.
* @param texture_update_callback Callback invoked when the texture ID updates
* i.e. video dimensions changes.
* @param texture_update_callback_context Context passed to
* |texture_update_callback|.
*/
void video_output_manager_create(VideoOutputManager* self,
gint64 handle,
VideoOutputConfiguration configuration,
TextureUpdateCallback texture_update_callback,
gpointer texture_update_callback_context);
/**
* @brief Sets the required video output size. This forces |VideoOutput| to
* resize the internal OpenGL surface / texture.
*
* @param texture_registrar |FlTextureRegistrar| reference.
* @param width Preferred width of the video. Pass `NULL` for using texture
* dimensions based on video's resolution.
* @param height Preferred height of the video. Pass `NULL` for using texture
* dimensions based on video's resolution.
*/
void video_output_manager_set_size(VideoOutputManager* self,
gint64 handle,
gint64 width,
gint64 height);
/**
* @brief Disposes |VideoOutput| instance for given |handle|.
*
* @param self |VideoOutputManager| reference.
* @param handle |mpv_handle| reference casted to gint64.
*/
void video_output_manager_dispose(VideoOutputManager* self, gint64 handle);
#endif
@@ -0,0 +1,208 @@
// 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.
#include "include/media_kit_video/media_kit_video_plugin.h"
#ifndef MEDIA_KIT_LIBS_NOT_FOUND
#include <gtk/gtk.h>
#include "include/media_kit_video/utils.h"
#include "include/media_kit_video/video_output_manager.h"
#define MEDIA_KIT_VIDEO_PLUGIN(obj) \
(G_TYPE_CHECK_INSTANCE_CAST((obj), media_kit_video_plugin_get_type(), \
MediaKitVideoPlugin))
struct _MediaKitVideoPlugin {
GObject parent_instance;
FlMethodChannel* channel;
FlView* view;
VideoOutputManager* video_output_manager;
};
G_DEFINE_TYPE(MediaKitVideoPlugin, media_kit_video_plugin, g_object_get_type())
static void media_kit_video_plugin_handle_method_call(
MediaKitVideoPlugin* self,
FlMethodCall* method_call) {
g_autoptr(FlMethodResponse) response = NULL;
const gchar* method = fl_method_call_get_name(method_call);
if (g_strcmp0(method, "VideoOutputManager.Create") == 0) {
FlValue* arguments = fl_method_call_get_args(method_call);
FlValue* handle = fl_value_lookup_string(arguments, "handle");
FlValue* configuration = fl_value_lookup_string(arguments, "configuration");
gint64 handle_value =
g_ascii_strtoll(fl_value_get_string(handle), NULL, 10);
VideoOutputConfiguration configuration_value = {};
const gchar* configuration_width =
fl_value_get_string(fl_value_lookup_string(configuration, "width"));
const gchar* configuration_height =
fl_value_get_string(fl_value_lookup_string(configuration, "height"));
const bool configuration_enable_hardware_acceleration = fl_value_get_bool(
fl_value_lookup_string(configuration, "enableHardwareAcceleration"));
if (g_strcmp0(configuration_width, "null") != 0) {
configuration_value.width =
g_ascii_strtoll(configuration_width, NULL, 10);
}
if (g_strcmp0(configuration_height, "null") != 0) {
configuration_value.height =
g_ascii_strtoll(configuration_height, NULL, 10);
}
configuration_value.enable_hardware_acceleration =
configuration_enable_hardware_acceleration;
typedef struct _VideoOutputTextureUpdateCallbackData {
FlMethodChannel* channel;
gint64 handle;
} VideoOutputTextureUpdateCallbackData;
// TODO(@alexmercerind): Fix memory leak.
VideoOutputTextureUpdateCallbackData* data =
g_new0(VideoOutputTextureUpdateCallbackData, 1);
data->channel = self->channel;
data->handle = handle_value;
video_output_manager_create(
self->video_output_manager, handle_value, configuration_value,
[](gint64 id, gint64 width, gint64 height, gpointer context) {
auto data = (VideoOutputTextureUpdateCallbackData*)context;
FlMethodChannel* channel = data->channel;
gint64 handle = data->handle;
FlValue* rect = fl_value_new_map();
fl_value_set_string_take(rect, "left", fl_value_new_int(0));
fl_value_set_string_take(rect, "top", fl_value_new_int(0));
fl_value_set_string_take(rect, "width", fl_value_new_int(width));
fl_value_set_string_take(rect, "height", fl_value_new_int(height));
FlValue* result = fl_value_new_map();
fl_value_set_string_take(result, "handle", fl_value_new_int(handle));
fl_value_set_string_take(result, "id", fl_value_new_int(id));
fl_value_set_string_take(result, "rect", rect);
typedef struct {
FlMethodChannel* channel;
FlValue* result;
} IdleCallbackData;
IdleCallbackData* idle_data = g_new0(IdleCallbackData, 1);
idle_data->channel = channel;
idle_data->result = result;
// `fl_method_channel_invoke_method` should be called from PlatformThread.
g_idle_add([](gpointer user_data) -> gboolean {
IdleCallbackData* idle_data = (IdleCallbackData*)user_data;
fl_method_channel_invoke_method(idle_data->channel, "VideoOutput.Resize",
idle_data->result, NULL, NULL, NULL);
g_free(idle_data);
return G_SOURCE_REMOVE;
}, idle_data);
},
data);
FlValue* result = fl_value_new_null();
response = FL_METHOD_RESPONSE(fl_method_success_response_new(result));
} else if (g_strcmp0(method, "VideoOutputManager.SetSize") == 0) {
FlValue* arguments = fl_method_call_get_args(method_call);
FlValue* handle = fl_value_lookup_string(arguments, "handle");
FlValue* width = fl_value_lookup_string(arguments, "width");
FlValue* height = fl_value_lookup_string(arguments, "height");
gint64 handle_value =
g_ascii_strtoll(fl_value_get_string(handle), NULL, 10);
gint64 width_value = 0;
gint64 height_value = 0;
if (g_strcmp0(fl_value_get_string(width), "null") != 0) {
width_value = g_ascii_strtoll(fl_value_get_string(width), NULL, 10);
}
if (g_strcmp0(fl_value_get_string(height), "null") != 0) {
height_value = g_ascii_strtoll(fl_value_get_string(height), NULL, 10);
}
video_output_manager_set_size(self->video_output_manager, handle_value,
width_value, height_value);
FlValue* result = fl_value_new_null();
response = FL_METHOD_RESPONSE(fl_method_success_response_new(result));
} else if (g_strcmp0(method, "VideoOutputManager.Dispose") == 0) {
FlValue* arguments = fl_method_call_get_args(method_call);
FlValue* handle = fl_value_lookup_string(arguments, "handle");
gint64 handle_value =
g_ascii_strtoll(fl_value_get_string(handle), NULL, 10);
video_output_manager_dispose(self->video_output_manager, handle_value);
FlValue* result = fl_value_new_null();
response = FL_METHOD_RESPONSE(fl_method_success_response_new(result));
} else if (g_strcmp0(method, "Utils.EnterNativeFullscreen") == 0) {
utils_enter_native_fullscreen(
gtk_widget_get_toplevel(GTK_WIDGET(self->view)));
FlValue* result = fl_value_new_null();
response = FL_METHOD_RESPONSE(fl_method_success_response_new(result));
} else if (g_strcmp0(method, "Utils.ExitNativeFullscreen") == 0) {
utils_exit_native_fullscreen(
gtk_widget_get_toplevel(GTK_WIDGET(self->view)));
FlValue* result = fl_value_new_null();
response = FL_METHOD_RESPONSE(fl_method_success_response_new(result));
} else {
response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
}
fl_method_call_respond(method_call, response, nullptr);
}
static void media_kit_video_plugin_dispose(GObject* object) {
G_OBJECT_CLASS(media_kit_video_plugin_parent_class)->dispose(object);
}
static void media_kit_video_plugin_class_init(MediaKitVideoPluginClass* klass) {
G_OBJECT_CLASS(klass)->dispose = media_kit_video_plugin_dispose;
}
static void media_kit_video_plugin_init(MediaKitVideoPlugin* self) {
self->channel = NULL;
self->video_output_manager = NULL;
}
static void method_call_cb(FlMethodChannel* channel,
FlMethodCall* method_call,
gpointer user_data) {
MediaKitVideoPlugin* plugin = MEDIA_KIT_VIDEO_PLUGIN(user_data);
media_kit_video_plugin_handle_method_call(plugin, method_call);
}
static MediaKitVideoPlugin* media_kit_video_plugin_new(
FlPluginRegistrar* registrar) {
MediaKitVideoPlugin* self = MEDIA_KIT_VIDEO_PLUGIN(
g_object_new(media_kit_video_plugin_get_type(), nullptr));
g_autoptr(FlMethodCodec) codec =
FL_METHOD_CODEC(fl_standard_method_codec_new());
self->channel =
fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar),
"com.alexmercerind/media_kit_video", codec);
fl_method_channel_set_method_call_handler(self->channel, method_call_cb, self,
g_object_unref);
FlTextureRegistrar* texture_registrar =
fl_plugin_registrar_get_texture_registrar(registrar);
FlView* view = fl_plugin_registrar_get_view(registrar);
self->view = view;
self->video_output_manager =
video_output_manager_new(texture_registrar, view);
return self;
}
void media_kit_video_plugin_register_with_registrar(
FlPluginRegistrar* registrar) {
media_kit_video_plugin_new(registrar);
}
#else
#include <iostream>
void media_kit_video_plugin_register_with_registrar(
FlPluginRegistrar* registrar) {
std::cout << "media_kit: WARNING: package:media_kit_libs_*** not found."
<< std::endl;
}
#endif
+284
View File
@@ -0,0 +1,284 @@
// 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.
#include "include/media_kit_video/texture_gl.h"
#include <epoxy/gl.h>
#include <epoxy/egl.h>
// EGLImage extension function pointers
typedef EGLImageKHR (*PFNEGLCREATEIMAGEKHRPROC)(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLint *attrib_list);
typedef EGLBoolean (*PFNEGLDESTROYIMAGEKHRPROC)(EGLDisplay dpy, EGLImageKHR image);
typedef void (*PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)(GLenum target, GLeglImageOES image);
// Define the extension functions
#ifndef eglCreateImageKHR
static PFNEGLCREATEIMAGEKHRPROC eglCreateImageKHR = NULL;
#endif
#ifndef eglDestroyImageKHR
static PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR = NULL;
#endif
#ifndef glEGLImageTargetTexture2DOES
static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES = NULL;
#endif
static void init_egl_image_extensions() {
static gboolean initialized = FALSE;
if (!initialized) {
eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
glEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
initialized = TRUE;
}
}
struct _TextureGL {
FlTextureGL parent_instance;
guint32 name; // Flutter's texture name
guint32 fbo; // mpv's FBO
guint32 mpv_texture; // mpv's texture
EGLImageKHR egl_image; // EGLImage for sharing between contexts
guint32 current_width;
guint32 current_height;
VideoOutput* video_output;
};
G_DEFINE_TYPE(TextureGL, texture_gl, fl_texture_gl_get_type())
static void texture_gl_init(TextureGL* self) {
self->name = 0;
self->fbo = 0;
self->mpv_texture = 0;
self->egl_image = EGL_NO_IMAGE_KHR;
self->current_width = 1;
self->current_height = 1;
self->video_output = NULL;
}
static void texture_gl_dispose(GObject* object) {
TextureGL* self = TEXTURE_GL(object);
VideoOutput* video_output = self->video_output;
VideoOutputGlxState glx_state;
video_output_glx_release(&glx_state);
// Save current context
EGLDisplay current_display = eglGetCurrentDisplay();
EGLContext current_context = eglGetCurrentContext();
EGLSurface current_draw = eglGetCurrentSurface(EGL_DRAW);
EGLSurface current_read = eglGetCurrentSurface(EGL_READ);
// Clean up Flutter's texture (in Flutter's context)
if (self->name != 0) {
glDeleteTextures(1, &self->name);
self->name = 0;
}
// Clean up EGLImage
if (self->egl_image != EGL_NO_IMAGE_KHR && video_output != NULL) {
EGLDisplay egl_display = video_output_get_egl_display(video_output);
eglDestroyImageKHR(egl_display, self->egl_image);
self->egl_image = EGL_NO_IMAGE_KHR;
}
// Clean up mpv's OpenGL resources (in mpv's isolated context)
if (video_output != NULL) {
EGLDisplay egl_display = video_output_get_egl_display(video_output);
EGLContext egl_context = video_output_get_egl_context(video_output);
if (egl_context != EGL_NO_CONTEXT) {
eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, egl_context);
if (self->mpv_texture != 0) {
glDeleteTextures(1, &self->mpv_texture);
self->mpv_texture = 0;
}
if (self->fbo != 0) {
glDeleteFramebuffers(1, &self->fbo);
self->fbo = 0;
}
// Restore previous context
video_output_restore_egl_context(egl_display, current_display,
current_draw, current_read,
current_context);
}
}
video_output_glx_restore(&glx_state);
self->current_width = 1;
self->current_height = 1;
self->video_output = NULL;
G_OBJECT_CLASS(texture_gl_parent_class)->dispose(object);
}
static void texture_gl_class_init(TextureGLClass* klass) {
FL_TEXTURE_GL_CLASS(klass)->populate = texture_gl_populate_texture;
G_OBJECT_CLASS(klass)->dispose = texture_gl_dispose;
}
TextureGL* texture_gl_new(VideoOutput* video_output) {
init_egl_image_extensions();
TextureGL* self = TEXTURE_GL(g_object_new(texture_gl_get_type(), NULL));
self->video_output = video_output;
return self;
}
gboolean texture_gl_populate_texture(FlTextureGL* texture,
guint32* target,
guint32* name,
guint32* width,
guint32* height,
GError** error) {
TextureGL* self = TEXTURE_GL(texture);
VideoOutput* video_output = self->video_output;
gint32 required_width = (guint32)video_output_get_width(video_output);
gint32 required_height = (guint32)video_output_get_height(video_output);
if (required_width > 0 && required_height > 0) {
gboolean first_frame = self->name == 0 || self->fbo == 0 || self->mpv_texture == 0;
gboolean resize = self->current_width != required_width ||
self->current_height != required_height;
if (first_frame || resize) {
// Save Flutter's current EGL context
EGLDisplay flutter_display = eglGetCurrentDisplay();
EGLContext flutter_context = eglGetCurrentContext();
EGLSurface flutter_draw = eglGetCurrentSurface(EGL_DRAW);
EGLSurface flutter_read = eglGetCurrentSurface(EGL_READ);
EGLDisplay egl_display = video_output_get_egl_display(video_output);
EGLContext egl_context = video_output_get_egl_context(video_output);
// Switch to mpv's isolated context to create/resize mpv's texture and FBO
eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, egl_context);
// Free previous resources in mpv's context
if (!first_frame) {
glDeleteTextures(1, &self->mpv_texture);
glDeleteFramebuffers(1, &self->fbo);
if (self->egl_image != EGL_NO_IMAGE_KHR) {
eglDestroyImageKHR(egl_display, self->egl_image);
}
}
// Create mpv's FBO and texture
glGenFramebuffers(1, &self->fbo);
glBindFramebuffer(GL_FRAMEBUFFER, self->fbo);
glGenTextures(1, &self->mpv_texture);
glBindTexture(GL_TEXTURE_2D, self->mpv_texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, required_width, required_height,
0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
// Attach mpv's texture to FBO
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D, self->mpv_texture, 0);
// Create EGLImage from mpv's texture
EGLint egl_image_attribs[] = { EGL_NONE };
self->egl_image = eglCreateImageKHR(
egl_display,
egl_context,
EGL_GL_TEXTURE_2D_KHR,
(EGLClientBuffer)(guintptr)self->mpv_texture,
egl_image_attribs);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindTexture(GL_TEXTURE_2D, 0);
// Flush to ensure mpv's texture is ready
glFlush();
// Switch back to Flutter's context to create/update Flutter's texture
eglMakeCurrent(flutter_display, flutter_draw, flutter_read, flutter_context);
// Free previous Flutter texture
if (!first_frame && self->name != 0) {
glDeleteTextures(1, &self->name);
}
// Create Flutter's texture from EGLImage
glGenTextures(1, &self->name);
glBindTexture(GL_TEXTURE_2D, self->name);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, self->egl_image);
glBindTexture(GL_TEXTURE_2D, 0);
self->current_width = required_width;
self->current_height = required_height;
// Notify Flutter about dimension change
video_output_notify_texture_update(video_output);
// Flutter's context is already current, so we're ready to render
}
// Save Flutter's current context
EGLDisplay flutter_display = eglGetCurrentDisplay();
EGLContext flutter_context = eglGetCurrentContext();
EGLSurface flutter_draw = eglGetCurrentSurface(EGL_DRAW);
EGLSurface flutter_read = eglGetCurrentSurface(EGL_READ);
EGLDisplay egl_display = video_output_get_egl_display(video_output);
EGLContext egl_context = video_output_get_egl_context(video_output);
mpv_render_context* render_context = video_output_get_render_context(video_output);
// Switch to mpv's isolated context for rendering
eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, egl_context);
// Bind mpv's FBO
glBindFramebuffer(GL_FRAMEBUFFER, self->fbo);
// Render mpv frame to mpv's texture
mpv_opengl_fbo fbo{(gint32)self->fbo, required_width, required_height, 0};
int flip_y = 0;
mpv_render_param params[] = {
{MPV_RENDER_PARAM_OPENGL_FBO, &fbo},
{MPV_RENDER_PARAM_FLIP_Y, &flip_y},
{MPV_RENDER_PARAM_INVALID, NULL},
};
mpv_render_context_render(render_context, params);
// Unbind FBO
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// Flush to ensure rendering is complete
glFlush();
// Restore Flutter's context
eglMakeCurrent(flutter_display, flutter_draw, flutter_read, flutter_context);
}
*target = GL_TEXTURE_2D;
*name = self->name;
*width = self->current_width;
*height = self->current_height;
if (self->name == 0) {
// First frame not yet available - create dummy texture in Flutter's context
glGenTextures(1, &self->name);
glBindTexture(GL_TEXTURE_2D, self->name);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
glBindTexture(GL_TEXTURE_2D, 0);
*name = self->name;
*width = 1;
*height = 1;
}
return TRUE;
}
+64
View File
@@ -0,0 +1,64 @@
// 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.
#include "include/media_kit_video/texture_sw.h"
struct _TextureSW {
FlPixelBufferTexture parent_instance;
guint32 current_width;
guint32 current_height;
VideoOutput* video_output;
};
G_DEFINE_TYPE(TextureSW, texture_sw, fl_pixel_buffer_texture_get_type())
static void texture_sw_init(TextureSW* self) {
self->current_width = 1;
self->current_height = 1;
self->video_output = NULL;
}
static void texture_sw_dispose(GObject* object) {
G_OBJECT_CLASS(texture_sw_parent_class)->dispose(object);
}
static void texture_sw_class_init(TextureSWClass* klass) {
FL_PIXEL_BUFFER_TEXTURE_CLASS(klass)->copy_pixels = texture_sw_copy_pixels;
G_OBJECT_CLASS(klass)->dispose = texture_sw_dispose;
}
TextureSW* texture_sw_new(VideoOutput* video_output) {
TextureSW* self = TEXTURE_SW(g_object_new(texture_sw_get_type(), NULL));
self->video_output = video_output;
return self;
}
gboolean texture_sw_copy_pixels(FlPixelBufferTexture* texture,
const guint8** buffer,
guint32* width,
guint32* height,
GError** error) {
TextureSW* self = TEXTURE_SW(texture);
VideoOutput* video_output = self->video_output;
gint32 required_width = (guint32)video_output_get_width(video_output);
gint32 required_height = (guint32)video_output_get_height(video_output);
if (required_width > 0 && required_height > 0) {
guint8* pixel_buffer = video_output_get_pixel_buffer(video_output);
if (self->current_width != required_width ||
self->current_height != required_height) {
self->current_width = required_width;
self->current_height = required_height;
// Notify Flutter about the change in texture's dimensions.
video_output_notify_texture_update(video_output);
}
*buffer = pixel_buffer;
*width = required_width;
*height = required_height;
}
return TRUE;
}
+18
View File
@@ -0,0 +1,18 @@
// // 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.
#include "include/media_kit_video/utils.h"
void utils_enter_native_fullscreen(GtkWidget* window) {
gtk_window_fullscreen(GTK_WINDOW(window));
}
void utils_exit_native_fullscreen(GtkWidget* window) {
gtk_window_unfullscreen(GTK_WINDOW(window));
}
+586
View File
@@ -0,0 +1,586 @@
// 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.
#include "include/media_kit_video/video_output.h"
#include "include/media_kit_video/texture_gl.h"
#include "include/media_kit_video/texture_sw.h"
#include <epoxy/egl.h>
#include <epoxy/glx.h>
#include <gdk/gdkwayland.h>
#include <gdk/gdkx.h>
// Flutter >= 3.38 keeps its EGL contexts on the raster thread, so nothing is
// current on the platform thread (where |video_output_new| runs). Open the
// display the way Flutter's FlOpenGLManager does: EGL hands back the same
// EGLDisplay for the same platform and native display, which the EGLImage
// shared in |texture_gl_populate_texture| needs. eglGetPlatformDisplayEXT is
// looked up by hand: epoxy's dispatch for it needs a current display.
static EGLDisplay flutter_egl_display() {
typedef EGLDisplay (*GetPlatformDisplay)(EGLenum, void*, const EGLint*);
GetPlatformDisplay get_platform_display =
(GetPlatformDisplay)eglGetProcAddress("eglGetPlatformDisplayEXT");
if (get_platform_display == NULL) {
return EGL_NO_DISPLAY;
}
EGLDisplay egl_display = EGL_NO_DISPLAY;
GdkDisplay* display = gdk_display_get_default();
if (GDK_IS_WAYLAND_DISPLAY(display)) {
egl_display = get_platform_display(
EGL_PLATFORM_WAYLAND_EXT, gdk_wayland_display_get_wl_display(display),
NULL);
} else if (GDK_IS_X11_DISPLAY(display)) {
egl_display = get_platform_display(
EGL_PLATFORM_X11_EXT, gdk_x11_display_get_xdisplay(display), NULL);
}
if (egl_display != EGL_NO_DISPLAY &&
!eglInitialize(egl_display, NULL, NULL)) {
egl_display = EGL_NO_DISPLAY;
}
return egl_display;
}
void video_output_glx_release(VideoOutputGlxState* state) {
*state = VideoOutputGlxState{};
// On X11, GDK draws Flutter's view with GLX on the platform thread, and
// libglvnd refuses an EGL context (EGL_BAD_ACCESS) on a thread holding a
// GLX one: step it aside while ours is in use.
if (!GDK_IS_X11_DISPLAY(gdk_display_get_default()) ||
glXGetCurrentContext() == NULL) {
return;
}
state->display = glXGetCurrentDisplay();
state->draw = glXGetCurrentDrawable();
state->read = glXGetCurrentReadDrawable();
state->context = glXGetCurrentContext();
glXMakeContextCurrent(state->display, None, None, NULL);
}
void video_output_glx_restore(VideoOutputGlxState* state) {
if (state->context != NULL) {
glXMakeContextCurrent(state->display, state->draw, state->read,
state->context);
}
}
void video_output_restore_egl_context(EGLDisplay own_display,
EGLDisplay display,
EGLSurface draw,
EGLSurface read,
EGLContext context) {
// Nothing was current (the platform thread): release ours, or it stays
// current here and the raster thread cannot make it current (BadAccess).
if (context == EGL_NO_CONTEXT) {
eglMakeCurrent(own_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
EGL_NO_CONTEXT);
} else {
eglMakeCurrent(display, draw, read, context);
}
}
struct _VideoOutput {
GObject parent_instance;
TextureGL* texture_gl;
EGLDisplay egl_display; /* EGL display for mpv rendering (shared with flutter). */
EGLContext egl_context; /* Isolated EGL context (non-shared). */
EGLSurface egl_surface; /* Place holder surface for activating egl context */
guint8* pixel_buffer;
TextureSW* texture_sw;
GMutex mutex; /* Only used in S/W rendering. */
mpv_handle* handle;
mpv_render_context* render_context;
gint64 width;
gint64 height;
VideoOutputConfiguration configuration;
TextureUpdateCallback texture_update_callback;
gpointer texture_update_callback_context;
FlTextureRegistrar* texture_registrar;
gboolean destroyed;
};
G_DEFINE_TYPE(VideoOutput, video_output, G_TYPE_OBJECT)
static void video_output_dispose(GObject* object) {
VideoOutput* self = VIDEO_OUTPUT(object);
self->destroyed = TRUE;
// Make sure that no more callbacks are invoked from mpv.
if (self->render_context) {
mpv_render_context_set_update_callback(self->render_context, NULL, NULL);
}
// H/W
if (self->texture_gl) {
fl_texture_registrar_unregister_texture(self->texture_registrar,
FL_TEXTURE(self->texture_gl));
// Save Flutter's current context before cleanup
EGLDisplay current_display = eglGetCurrentDisplay();
EGLContext flutter_context = eglGetCurrentContext();
EGLSurface flutter_draw_surface = eglGetCurrentSurface(EGL_DRAW);
EGLSurface flutter_read_surface = eglGetCurrentSurface(EGL_READ);
// Free mpv_render_context with our own isolated EGL context
if (self->render_context != NULL) {
VideoOutputGlxState glx_state;
video_output_glx_release(&glx_state);
if (self->egl_context != EGL_NO_CONTEXT) {
eglMakeCurrent(self->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, self->egl_context);
}
mpv_render_context_free(self->render_context);
self->render_context = NULL;
// Restore Flutter's context
video_output_restore_egl_context(self->egl_display, current_display,
flutter_draw_surface,
flutter_read_surface, flutter_context);
video_output_glx_restore(&glx_state);
}
// Clean up EGL resources
if (self->egl_context != EGL_NO_CONTEXT) {
eglDestroyContext(self->egl_display, self->egl_context);
self->egl_context = EGL_NO_CONTEXT;
}
g_object_unref(self->texture_gl);
}
// S/W
if (self->texture_sw) {
fl_texture_registrar_unregister_texture(self->texture_registrar,
FL_TEXTURE(self->texture_sw));
g_free(self->pixel_buffer);
g_object_unref(self->texture_sw);
if (self->render_context != NULL) {
mpv_render_context_free(self->render_context);
self->render_context = NULL;
}
}
g_mutex_clear(&self->mutex);
G_OBJECT_CLASS(video_output_parent_class)->dispose(object);
}
static void video_output_class_init(VideoOutputClass* klass) {
G_OBJECT_CLASS(klass)->dispose = video_output_dispose;
}
static void video_output_init(VideoOutput* self) {
self->texture_gl = NULL;
self->egl_display = EGL_NO_DISPLAY;
self->egl_context = EGL_NO_CONTEXT;
self->egl_surface = EGL_NO_SURFACE;
self->texture_sw = NULL;
self->pixel_buffer = NULL;
self->handle = NULL;
self->render_context = NULL;
self->width = 0;
self->height = 0;
self->configuration = VideoOutputConfiguration{};
self->texture_update_callback = NULL;
self->texture_update_callback_context = NULL;
self->texture_registrar = NULL;
self->destroyed = FALSE;
g_mutex_init(&self->mutex);
}
VideoOutput* video_output_new(FlTextureRegistrar* texture_registrar,
FlView* view,
gint64 handle,
VideoOutputConfiguration configuration) {
VideoOutput* self = VIDEO_OUTPUT(g_object_new(video_output_get_type(), NULL));
self->texture_registrar = texture_registrar;
self->handle = (mpv_handle*)handle;
self->width = configuration.width;
self->height = configuration.height;
self->configuration = configuration;
#ifndef MPV_RENDER_API_TYPE_SW
// MPV_RENDER_API_TYPE_SW must be available for S/W rendering.
if (!self->configuration.enable_hardware_acceleration) {
g_printerr("media_kit: VideoOutput: S/W rendering is not supported.\n");
}
self->configuration.enable_hardware_acceleration = TRUE;
#endif
mpv_set_option_string(self->handle, "video-sync", "audio");
// Causes frame drops with `pulse` audio output. (SlotSun/dart_simple_live#42)
// mpv_set_option_string(self->handle, "video-timing-offset", "0");
gboolean hardware_acceleration_supported = FALSE;
if (self->configuration.enable_hardware_acceleration) {
VideoOutputGlxState glx_state;
video_output_glx_release(&glx_state);
// Get Flutter's current EGL display (DO NOT share context)
EGLDisplay flutter_display = eglGetCurrentDisplay();
EGLContext flutter_context = eglGetCurrentContext();
EGLSurface flutter_draw_surface = eglGetCurrentSurface(EGL_DRAW);
EGLSurface flutter_read_surface = eglGetCurrentSurface(EGL_READ);
EGLDisplay egl_display = flutter_context != EGL_NO_CONTEXT
? flutter_display
: flutter_egl_display();
if (egl_display != EGL_NO_DISPLAY) {
self->egl_display = egl_display;
// Bind OpenGL ES API (Flutter uses OpenGL ES on Linux)
eglBindAPI(EGL_OPENGL_ES_API);
// Query Flutter's EGL config and reuse it for compatibility
EGLConfig config = NULL;
EGLint config_id = 0;
if (flutter_context == EGL_NO_CONTEXT) {
// The attributes FlOpenGLManager chooses Flutter's config with.
EGLint num_configs = 0;
const EGLint config_attribs[] = {
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, 8, EGL_DEPTH_SIZE, 8, EGL_STENCIL_SIZE, 8,
EGL_NONE,
};
if (!eglChooseConfig(self->egl_display, config_attribs, &config, 1,
&num_configs) ||
num_configs == 0) {
g_printerr("media_kit: VideoOutput: Failed to choose an EGL config.\n");
config = NULL;
}
} else if (eglQueryContext(self->egl_display, flutter_context, EGL_CONFIG_ID, &config_id)) {
g_print("media_kit: VideoOutput: Flutter's EGL config ID: %d\n", config_id);
// Get Flutter's exact config
EGLint num_configs = 0;
EGLint config_attribs[] = { EGL_CONFIG_ID, config_id, EGL_NONE };
if (eglChooseConfig(self->egl_display, config_attribs, &config, 1, &num_configs) && num_configs > 0) {
g_print("media_kit: VideoOutput: Using Flutter's EGL config.\n");
} else {
g_printerr("media_kit: VideoOutput: Failed to get Flutter's EGL config by ID.\n");
config = NULL;
}
} else {
g_printerr("media_kit: VideoOutput: Failed to query Flutter's EGL config ID.\n");
}
if (config != NULL) {
// Create an isolated EGL context (NOT shared with Flutter)
// This prevents OpenGL state pollution and resource contention
EGLint context_attribs[] = {
EGL_CONTEXT_CLIENT_VERSION, 2,
EGL_NONE,
};
self->egl_context = eglCreateContext(self->egl_display, config,
EGL_NO_CONTEXT, context_attribs);
if (self->egl_context != EGL_NO_CONTEXT) {
// Make our isolated context current for initialization (surfaceless)
if (eglMakeCurrent(self->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, self->egl_context)) {
// Create texture with our isolated context
self->texture_gl = texture_gl_new(self);
if (fl_texture_registrar_register_texture(
texture_registrar, FL_TEXTURE(self->texture_gl))) {
// Initialize mpv with our isolated EGL context
mpv_opengl_init_params gl_init_params{
[](auto, auto name) {
return (void*)eglGetProcAddress(name);
},
NULL,
};
mpv_render_param params[] = {
{MPV_RENDER_PARAM_API_TYPE, (void*)MPV_RENDER_API_TYPE_OPENGL},
{MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, (void*)&gl_init_params},
{MPV_RENDER_PARAM_INVALID, (void*)0},
{MPV_RENDER_PARAM_INVALID, (void*)0},
};
// VAAPI acceleration requires passing X11/Wayland display
GdkDisplay* display = gdk_display_get_default();
if (GDK_IS_WAYLAND_DISPLAY(display)) {
params[2].type = MPV_RENDER_PARAM_WL_DISPLAY;
params[2].data = gdk_wayland_display_get_wl_display(display);
} else if (GDK_IS_X11_DISPLAY(display)) {
params[2].type = MPV_RENDER_PARAM_X11_DISPLAY;
params[2].data = gdk_x11_display_get_xdisplay(display);
}
if (mpv_render_context_create(&self->render_context, self->handle, params) == 0) {
mpv_render_context_set_update_callback(
self->render_context,
[](void* data) {
VideoOutput* self = (VideoOutput*)data;
if (self->destroyed) {
return;
}
fl_texture_registrar_mark_texture_frame_available(
self->texture_registrar, FL_TEXTURE(self->texture_gl));
},
self);
hardware_acceleration_supported = TRUE;
g_print("media_kit: VideoOutput: H/W rendering with isolated EGL context.\n");
} else {
g_printerr("media_kit: VideoOutput: Failed to create mpv_render_context.\n");
}
} else {
g_printerr("media_kit: VideoOutput: Failed to register texture.\n");
}
// Restore Flutter's context
video_output_restore_egl_context(
self->egl_display, flutter_display, flutter_draw_surface,
flutter_read_surface, flutter_context);
} else {
g_printerr("media_kit: VideoOutput: Failed to make isolated EGL context current. Error: 0x%x\n", eglGetError());
}
} else {
g_printerr("media_kit: VideoOutput: Failed to create isolated EGL context. Error: 0x%x\n", eglGetError());
}
} else {
g_printerr("media_kit: VideoOutput: Could not obtain Flutter's EGL config.\n");
}
} else {
g_printerr("media_kit: VideoOutput: EGL display is invalid.\n");
}
video_output_glx_restore(&glx_state);
}
#ifdef MPV_RENDER_API_TYPE_SW
if (!hardware_acceleration_supported) {
g_printerr("media_kit: VideoOutput: S/W rendering.\n");
// H/W rendering failed. Fallback to S/W rendering.
self->pixel_buffer = g_new0(guint8, SW_RENDERING_PIXEL_BUFFER_SIZE);
self->texture_gl = NULL;
self->texture_sw = texture_sw_new(self);
if (fl_texture_registrar_register_texture(texture_registrar,
FL_TEXTURE(self->texture_sw))) {
mpv_render_param params[] = {
{MPV_RENDER_PARAM_API_TYPE, (void*)MPV_RENDER_API_TYPE_SW},
{MPV_RENDER_PARAM_INVALID, (void*)0},
};
if (mpv_render_context_create(&self->render_context, self->handle,
params) == 0) {
mpv_render_context_set_update_callback(
self->render_context,
[](void* data) {
gdk_threads_add_idle(
[](gpointer data) -> gboolean {
VideoOutput* self = (VideoOutput*)data;
if (self->destroyed) {
return FALSE;
}
g_mutex_lock(&self->mutex);
gint64 width = video_output_get_width(self);
gint64 height = video_output_get_height(self);
if (width > 0 && height > 0) {
gint32 size[]{(gint32)width, (gint32)height};
gint32 pitch = 4 * (gint32)width;
mpv_render_param params[]{
{MPV_RENDER_PARAM_SW_SIZE, size},
{MPV_RENDER_PARAM_SW_FORMAT, (void*)"rgb0"},
{MPV_RENDER_PARAM_SW_STRIDE, &pitch},
{MPV_RENDER_PARAM_SW_POINTER, self->pixel_buffer},
{MPV_RENDER_PARAM_INVALID, (void*)0},
};
mpv_render_context_render(self->render_context, params);
fl_texture_registrar_mark_texture_frame_available(
self->texture_registrar,
FL_TEXTURE(self->texture_sw));
}
g_mutex_unlock(&self->mutex);
return FALSE;
},
data);
},
self);
}
}
}
#endif
return self;
}
void video_output_set_texture_update_callback(
VideoOutput* self,
TextureUpdateCallback texture_update_callback,
gpointer texture_update_callback_context) {
self->texture_update_callback = texture_update_callback;
self->texture_update_callback_context = texture_update_callback_context;
// Notify initial dimensions as (1, 1) if |width| & |height| are 0 i.e.
// texture & video frame size is based on playing file's resolution. This
// will make sure that `Texture` widget on Flutter's widget tree is actually
// mounted & |fl_texture_registrar_mark_texture_frame_available| actually
// invokes the |TextureGL| or |TextureSW| callbacks. Otherwise it will be a
// never ending deadlock where no video frames are ever rendered.
gint64 texture_id = video_output_get_texture_id(self);
if (self->width == 0 || self->height == 0) {
self->texture_update_callback(texture_id, 1, 1,
self->texture_update_callback_context);
} else {
self->texture_update_callback(texture_id, self->width, self->height,
self->texture_update_callback_context);
}
}
void video_output_set_size(VideoOutput* self, gint64 width, gint64 height) {
// Ideally, a mutex should be used here & |video_output_get_width| +
// |video_output_get_height|. However, that is throwing everything into a
// deadlock. Flutter itself seems to have some synchronization mechanism in
// rendering & platform channels AFAIK.
// H/W
if (self->texture_gl) {
self->width = width;
self->height = height;
}
// S/W
if (self->texture_sw) {
self->width = CLAMP(width, 0, SW_RENDERING_MAX_WIDTH);
self->height = CLAMP(height, 0, SW_RENDERING_MAX_HEIGHT);
}
}
mpv_render_context* video_output_get_render_context(VideoOutput* self) {
return self->render_context;
}
EGLDisplay video_output_get_egl_display(VideoOutput* self) {
return self->egl_display;
}
EGLContext video_output_get_egl_context(VideoOutput* self) {
return self->egl_context;
}
EGLSurface video_output_get_egl_surface(VideoOutput* self) {
return self->egl_surface;
}
guint8* video_output_get_pixel_buffer(VideoOutput* self) {
return self->pixel_buffer;
}
gint64 video_output_get_width(VideoOutput* self) {
// Fixed width.
if (self->width) {
return self->width;
}
// Video resolution dependent width.
gint64 width = 0;
gint64 height = 0;
mpv_node params;
mpv_get_property(self->handle, "video-out-params", MPV_FORMAT_NODE, &params);
int64_t dw = 0, dh = 0, rotate = 0;
if (params.format == MPV_FORMAT_NODE_MAP) {
for (int32_t i = 0; i < params.u.list->num; i++) {
char* key = params.u.list->keys[i];
auto value = params.u.list->values[i];
if (value.format == MPV_FORMAT_INT64) {
if (strcmp(key, "dw") == 0) {
dw = value.u.int64;
}
if (strcmp(key, "dh") == 0) {
dh = value.u.int64;
}
if (strcmp(key, "rotate") == 0) {
rotate = value.u.int64;
}
}
}
mpv_free_node_contents(&params);
}
width = rotate == 0 || rotate == 180 ? dw : dh;
height = rotate == 0 || rotate == 180 ? dh : dw;
if (self->texture_sw != NULL) {
// Make sure |width| & |height| fit between |SW_RENDERING_MAX_WIDTH| &
// |SW_RENDERING_MAX_HEIGHT| while maintaining aspect ratio.
if (width >= SW_RENDERING_MAX_WIDTH) {
return SW_RENDERING_MAX_WIDTH;
}
if (height >= SW_RENDERING_MAX_HEIGHT) {
return width / height * SW_RENDERING_MAX_HEIGHT;
}
}
return width;
}
gint64 video_output_get_height(VideoOutput* self) {
// Fixed height.
if (self->width) {
return self->height;
}
// Video resolution dependent height.
gint64 width = 0;
gint64 height = 0;
mpv_node params;
mpv_get_property(self->handle, "video-out-params", MPV_FORMAT_NODE, &params);
int64_t dw = 0, dh = 0, rotate = 0;
if (params.format == MPV_FORMAT_NODE_MAP) {
for (int32_t i = 0; i < params.u.list->num; i++) {
char* key = params.u.list->keys[i];
auto value = params.u.list->values[i];
if (value.format == MPV_FORMAT_INT64) {
if (strcmp(key, "dw") == 0) {
dw = value.u.int64;
}
if (strcmp(key, "dh") == 0) {
dh = value.u.int64;
}
if (strcmp(key, "rotate") == 0) {
rotate = value.u.int64;
}
}
}
mpv_free_node_contents(&params);
}
width = rotate == 0 || rotate == 180 ? dw : dh;
height = rotate == 0 || rotate == 180 ? dh : dw;
if (self->texture_sw != NULL) {
// Make sure |width| & |height| fit between |SW_RENDERING_MAX_WIDTH| &
// |SW_RENDERING_MAX_HEIGHT| while maintaining aspect ratio.
if (height >= SW_RENDERING_MAX_HEIGHT) {
return SW_RENDERING_MAX_HEIGHT;
}
if (width >= SW_RENDERING_MAX_WIDTH) {
return height / width * SW_RENDERING_MAX_WIDTH;
}
}
return height;
}
gint64 video_output_get_texture_id(VideoOutput* self) {
// H/W
if (self->texture_gl) {
return (gint64)self->texture_gl;
}
// S/W
if (self->texture_sw) {
return (gint64)self->texture_sw;
}
g_assert_not_reached();
return -1;
}
void video_output_notify_texture_update(VideoOutput* self) {
gint64 id = video_output_get_texture_id(self);
gint64 width = video_output_get_width(self);
gint64 height = video_output_get_height(self);
gpointer context = self->texture_update_callback_context;
if (self->texture_update_callback != NULL) {
self->texture_update_callback(id, width, height, context);
}
}
@@ -0,0 +1,75 @@
// 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.
#include "include/media_kit_video/video_output_manager.h"
struct _VideoOutputManager {
GObject parent_instance;
GHashTable* video_outputs;
FlTextureRegistrar* texture_registrar;
FlView* view;
};
G_DEFINE_TYPE(VideoOutputManager, video_output_manager, G_TYPE_OBJECT)
static void video_output_manager_init(VideoOutputManager* self) {
self->video_outputs = g_hash_table_new_full(g_direct_hash, g_direct_equal,
nullptr, g_object_unref);
}
static void video_output_manager_dispose(GObject* object) {
VideoOutputManager* self = VIDEO_OUTPUT_MANAGER(object);
g_hash_table_unref(self->video_outputs);
G_OBJECT_CLASS(video_output_manager_parent_class)->dispose(object);
}
static void video_output_manager_class_init(VideoOutputManagerClass* klass) {
G_OBJECT_CLASS(klass)->dispose = video_output_manager_dispose;
}
VideoOutputManager* video_output_manager_new(
FlTextureRegistrar* texture_registrar,
FlView* view) {
VideoOutputManager* video_output_manager = VIDEO_OUTPUT_MANAGER(
g_object_new(video_output_manager_get_type(), nullptr));
video_output_manager->texture_registrar = texture_registrar;
video_output_manager->view = view;
return video_output_manager;
}
void video_output_manager_create(VideoOutputManager* self,
gint64 handle,
VideoOutputConfiguration configuration,
TextureUpdateCallback texture_update_callback,
gpointer texture_update_callback_context) {
if (!g_hash_table_contains(self->video_outputs, GINT_TO_POINTER(handle))) {
g_autoptr(VideoOutput) video_output = video_output_new(
self->texture_registrar, self->view, handle, configuration);
video_output_set_texture_update_callback(
video_output, texture_update_callback, texture_update_callback_context);
g_hash_table_insert(self->video_outputs, GINT_TO_POINTER(handle),
g_object_ref(video_output));
}
}
void video_output_manager_set_size(VideoOutputManager* self,
gint64 handle,
gint64 width,
gint64 height) {
if (g_hash_table_contains(self->video_outputs, GINT_TO_POINTER(handle))) {
VideoOutput* video_output = VIDEO_OUTPUT(
g_hash_table_lookup(self->video_outputs, GINT_TO_POINTER(handle)));
video_output_set_size(video_output, width, height);
}
}
void video_output_manager_dispose(VideoOutputManager* self, gint64 handle) {
if (g_hash_table_contains(self->video_outputs, GINT_TO_POINTER(handle))) {
g_hash_table_remove(self->video_outputs, GINT_TO_POINTER(handle));
}
}
@@ -0,0 +1,208 @@
import FlutterMacOS
import OpenGL.GL
import OpenGL.GL3
public class TextureHW: NSObject, FlutterTexture, ResizableTextureProtocol {
public typealias UpdateCallback = () -> Void
private let handle: OpaquePointer
private let updateCallback: UpdateCallback
private let pixelFormat: CGLPixelFormatObj
private let context: CGLContextObj
private let textureCache: CVOpenGLTextureCache
private var renderContext: OpaquePointer?
private var textureContexts = SwappableObjectManager<TextureGLContext>(
objects: [],
skipCheckArgs: true
)
init(
handle: OpaquePointer,
updateCallback: @escaping UpdateCallback
) {
self.handle = handle
self.updateCallback = updateCallback
self.pixelFormat = OpenGLHelpers.createPixelFormat()
self.context = OpenGLHelpers.createContext(pixelFormat)
self.textureCache = OpenGLHelpers.createTextureCache(context, pixelFormat)
super.init()
self.initMPV()
}
deinit {
disposePixelBuffer()
disposeMPV()
OpenGLHelpers.deleteTextureCache(textureCache)
OpenGLHelpers.deletePixelFormat(pixelFormat)
// Deleting the context may cause potential RAM or VRAM memory leaks, as it
// is used in the `deinit` method of the `TextureGLContext`.
// Potential fix: use a counter, and delete it only when the counter reaches
// zero
OpenGLHelpers.deleteContext(context)
}
public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
let textureContext = textureContexts.current
if textureContext == nil {
return nil
}
return Unmanaged.passRetained(textureContext!.pixelBuffer)
}
private func initMPV() {
CGLSetCurrentContext(context)
defer {
OpenGLHelpers.checkError("initMPV")
CGLSetCurrentContext(nil)
}
let api = UnsafeMutableRawPointer(
mutating: (MPV_RENDER_API_TYPE_OPENGL as NSString).utf8String
)
var procAddress = mpv_opengl_init_params(
get_proc_address: {
(ctx, name) in
return TextureHW.getProcAddress(ctx, name)
},
get_proc_address_ctx: nil
)
var params: [mpv_render_param] = withUnsafeMutableBytes(of: &procAddress) {
procAddress in
return [
mpv_render_param(type: MPV_RENDER_PARAM_API_TYPE, data: api),
mpv_render_param(
type: MPV_RENDER_PARAM_OPENGL_INIT_PARAMS,
data: procAddress.baseAddress.map {
UnsafeMutableRawPointer($0)
}
),
mpv_render_param(),
]
}
MPVHelpers.checkError(
mpv_render_context_create(&renderContext, handle, &params)
)
mpv_render_context_set_update_callback(
renderContext,
{ (ctx) in
let that = unsafeBitCast(ctx, to: TextureHW.self)
DispatchQueue.main.async {
that.updateCallback()
}
},
UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
)
}
private func disposeMPV() {
CGLSetCurrentContext(context)
defer {
OpenGLHelpers.checkError("disposeMPV")
CGLSetCurrentContext(nil)
}
mpv_render_context_set_update_callback(renderContext, nil, nil)
mpv_render_context_free(renderContext)
}
public func resize(_ size: CGSize) {
if size.width == 0 || size.height == 0 {
return
}
NSLog("TextureGL: resize: \(size.width)x\(size.height)")
createPixelBuffer(size)
}
private func createPixelBuffer(_ size: CGSize) {
disposePixelBuffer()
textureContexts.reinit(
objects: [
TextureGLContext(
context: context,
textureCache: textureCache,
size: size
),
TextureGLContext(
context: context,
textureCache: textureCache,
size: size
),
TextureGLContext(
context: context,
textureCache: textureCache,
size: size
),
],
skipCheckArgs: true
)
}
private func disposePixelBuffer() {
textureContexts.reinit(objects: [], skipCheckArgs: true)
}
public func render(_ size: CGSize) {
let textureContext = textureContexts.nextAvailable()
if textureContext == nil {
return
}
CGLSetCurrentContext(context)
defer {
OpenGLHelpers.checkError("render")
CGLSetCurrentContext(nil)
}
glBindFramebuffer(GLenum(GL_FRAMEBUFFER), textureContext!.frameBuffer)
defer {
glBindFramebuffer(GLenum(GL_FRAMEBUFFER), 0)
}
var fbo = mpv_opengl_fbo(
fbo: Int32(textureContext!.frameBuffer),
w: Int32(size.width),
h: Int32(size.height),
internal_format: 0
)
let fboPtr = withUnsafeMutablePointer(to: &fbo) { $0 }
var params: [mpv_render_param] = [
mpv_render_param(type: MPV_RENDER_PARAM_OPENGL_FBO, data: fboPtr),
mpv_render_param(type: MPV_RENDER_PARAM_INVALID, data: nil),
]
mpv_render_context_render(renderContext, &params)
glFlush()
textureContexts.pushAsReady(textureContext!)
}
static private func getProcAddress(
_ ctx: UnsafeMutableRawPointer?,
_ name: UnsafePointer<Int8>?
) -> UnsafeMutableRawPointer? {
let symbol: CFString = CFStringCreateWithCString(
kCFAllocatorDefault,
name,
kCFStringEncodingASCII
)
let indentifier = CFBundleGetBundleWithIdentifier(
"com.apple.opengl" as CFString
)
let addr = CFBundleGetFunctionPointerForName(indentifier, symbol)
if addr == nil {
NSLog("Cannot get OpenGL function pointer!")
}
return addr
}
}
@@ -0,0 +1,37 @@
import FlutterMacOS
public class Utils: NSObject, UtilsProtocol {
private let registrar: FlutterPluginRegistrar
init(_ registrar: FlutterPluginRegistrar) {
self.registrar = registrar
}
private var window: NSWindow? {
registrar.view?.window
}
public func enterNativeFullscreen() {
guard let window = window else {
return printWarning()
}
if !window.styleMask.contains(.fullScreen) {
window.toggleFullScreen(nil)
}
}
public func exitNativeFullscreen() {
guard let window = window else {
return printWarning()
}
if window.styleMask.contains(.fullScreen) {
window.toggleFullScreen(nil)
}
}
private func printWarning() {
NSLog("Utils: warning: unable to find the window")
}
}
@@ -0,0 +1,30 @@
public enum MPVHelpers {
public static func checkError(_ status: CInt) {
if status < 0 {
NSLog("MPVHelpers: error: \(String(cString: mpv_error_string(status)))")
exit(1)
}
}
public static func getVideoOutParams(
_ handle: OpaquePointer
) -> MPVVideoOutParams {
var node = mpv_node()
defer {
mpv_free_node_contents(&node)
}
mpv_get_property(handle, "video-out-params", MPV_FORMAT_NODE, &node)
if node.format != MPV_FORMAT_NODE_MAP {
return MPVVideoOutParams.empty
}
let map: mpv_node_list = node.u.list!.pointee
if map.num == 0 {
return MPVVideoOutParams.empty
}
return MPVVideoOutParams.fromMPVNodeList(map)
}
}
@@ -0,0 +1,55 @@
public class MPVVideoOutParams {
public let dw: Int64
public let dh: Int64
public let rotate: Int64
init(
dw: Int64,
dh: Int64,
rotate: Int64
) {
self.dw = dw
self.dh = dh
self.rotate = rotate
}
public static let empty = MPVVideoOutParams(
dw: 0,
dh: 0,
rotate: 0
)
public static func fromMPVNodeList(
_ map: mpv_node_list
) -> MPVVideoOutParams {
var dw: Int64 = 0
var dh: Int64 = 0
var rotate: Int64 = 0
var kptr = map.keys!
var vptr = map.values!
for _ in 0 ..< map.num {
let key = String(cString: kptr.pointee!)
let value: mpv_node = vptr.pointee
kptr = kptr.successor()
vptr = vptr.successor()
if value.format == MPV_FORMAT_INT64 {
if key == "dw" {
dw = value.u.int64
} else if key == "dh" {
dh = value.u.int64
} else if key == "rotate" {
rotate = value.u.int64
}
}
}
return MPVVideoOutParams(
dw: dw,
dh: dh,
rotate: rotate
)
}
}
@@ -0,0 +1,168 @@
#if canImport(Flutter)
import Flutter
#elseif canImport(FlutterMacOS)
import FlutterMacOS
#endif
public class MediaKitVideoPlugin: NSObject, FlutterPlugin {
private static let CHANNEL_NAME = "com.alexmercerind/media_kit_video"
public static func register(with registrar: FlutterPluginRegistrar) {
#if canImport(Flutter)
let binaryMessenger = registrar.messenger()
let registry = registrar.textures()
let utils: UtilsProtocol? = nil
#elseif canImport(FlutterMacOS)
let binaryMessenger = registrar.messenger
let registry = registrar.textures
let utils: UtilsProtocol? = Utils(registrar)
#endif
let channel = FlutterMethodChannel(
name: CHANNEL_NAME,
binaryMessenger: binaryMessenger
)
let instance = MediaKitVideoPlugin(
registry: registry,
channel: channel,
utils: utils
)
registrar.addMethodCallDelegate(instance, channel: channel)
}
private let channel: FlutterMethodChannel
private let videoOutputManager: VideoOutputManager
private let utils: UtilsProtocol?
init(
registry: FlutterTextureRegistry,
channel: FlutterMethodChannel,
utils: UtilsProtocol?
) {
self.channel = channel
videoOutputManager = VideoOutputManager(
registry: registry
)
self.utils = utils
}
public func handle(
_ call: FlutterMethodCall,
result: @escaping FlutterResult
) {
switch call.method {
case "VideoOutputManager.Create":
handleCreateMethodCall(call.arguments, result)
case "VideoOutputManager.SetSize":
handleSetSizeMethodCall(call.arguments, result)
case "VideoOutputManager.Dispose":
handleDisposeMethodCall(call.arguments, result)
case "Utils.EnterNativeFullscreen":
handleEnterNativeFullscreenMethodCall(call.arguments, result)
case "Utils.ExitNativeFullscreen":
handleExitNativeFullscreenMethodCall(call.arguments, result)
default:
result(FlutterMethodNotImplemented)
}
}
private func handleCreateMethodCall(
_ arguments: Any?,
_ result: FlutterResult
) {
let args = arguments as? [String: Any]
let handleStr = args?["handle"] as! String
let handle: Int64? = Int64(handleStr)
let configDict = args?["configuration"] as! [String: Any]
let configuration = VideoOutputConfiguration.fromDict(configDict)
assert(handle != nil, "handle must be an Int64")
videoOutputManager.create(
handle: handle!,
configuration: configuration,
textureUpdateCallback: { (_ textureId: Int64, _ size: CGSize) in
self.channel.invokeMethod(
"VideoOutput.Resize",
arguments: [
"handle": handle!,
"id": textureId,
"rect": [
"top": 0,
"left": 0,
"width": size.width,
"height": size.height,
],
] as [String: Any]
)
}
)
result(nil)
}
private func handleSetSizeMethodCall(
_ arguments: Any?,
_ result: FlutterResult
) {
let args = arguments as? [String: Any]
let handleStr = args?["handle"] as! String
let widthStr = args?["width"] as! String
let heightStr = args?["height"] as! String
let handle: Int64? = Int64(handleStr)
let width: Int64? = Int64(widthStr)
let height: Int64? = Int64(heightStr)
assert(handle != nil, "handle must be an Int64")
self.videoOutputManager.setSize(
handle: handle!,
width: width,
height: height
)
result(nil)
}
private func handleDisposeMethodCall(
_ arguments: Any?,
_ result: FlutterResult
) {
let args = arguments as? [String: Any]
let handleStr = args?["handle"] as! String
let handle: Int64? = Int64(handleStr)
assert(handle != nil, "handle must be an Int64")
videoOutputManager.destroy(
handle: handle!
)
result(nil)
}
private func handleEnterNativeFullscreenMethodCall(
_: Any?,
_ result: FlutterResult
) {
if utils == nil {
return result(FlutterMethodNotImplemented)
}
utils?.enterNativeFullscreen()
result(nil)
}
private func handleExitNativeFullscreenMethodCall(
_: Any?,
_ result: FlutterResult
) {
if utils == nil {
return result(FlutterMethodNotImplemented)
}
utils?.exitNativeFullscreen()
result(nil)
}
}
Loaded 100 of 131 files, more files were not shown because too many files have changed in this diff. Show more