Vendor media_kit_video 2.0.1 with the X11/EGL fix (hardware rendering on X11 with Flutter >= 3.38)
Flutter >= 3.38 keeps its EGL contexts on the raster thread, so media_kit's video output found none on the platform thread and fell back to software rendering (no shaders, ~5 fps) on X11. The patched output opens Flutter's EGL display itself, steps GDK's GLX context aside on X11 and releases its own context. Patch from a friend's agent, kept in third_party/media_kit_video/starsync/. Wayland/NVIDIA still renders on the GPU (checked); the X11 path was verified on the friend's machine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
25b19f2d6a
commit
a87ee98faa
131 files changed
+15532
-6
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
Vendored
+21
@@ -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
@@ -0,0 +1,11 @@
|
||||
# [package:media_kit_video](https://github.com/media-kit/media-kit)
|
||||
|
||||
[](https://discord.gg/h7qf2R9n57) [](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.
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
rootProject.name = 'media_kit_video'
|
||||
@@ -0,0 +1 @@
|
||||
<manifest package="com.alexmercerind.media_kit_video"></manifest>
|
||||
+84
@@ -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);
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package com.alexmercerind.media_kit_video;
|
||||
|
||||
public interface TextureUpdateCallback {
|
||||
void onTextureUpdate(long id, long wid, int width, int height);
|
||||
}
|
||||
Vendored
+42
@@ -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;
|
||||
}
|
||||
}
|
||||
third_party/media_kit_video/android/src/main/java/com/alexmercerind/media_kit_video/VideoOutput.java
Vendored
+141
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+57
@@ -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)
|
||||
}
|
||||
}
|
||||
+55
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+168
@@ -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)
|
||||
}
|
||||
}
|
||||
+10
@@ -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)
|
||||
}
|
||||
+44
@@ -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()
|
||||
}
|
||||
}
|
||||
+106
@@ -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, ¶ms)
|
||||
)
|
||||
|
||||
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, ¶ms)
|
||||
|
||||
textureContexts.pushAsReady(textureContext!)
|
||||
}
|
||||
}
|
||||
+36
@@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
+33
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+56
@@ -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()
|
||||
}
|
||||
}
|
||||
+9
@@ -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) {}
|
||||
}
|
||||
@@ -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, ¶ms)
|
||||
)
|
||||
|
||||
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, ¶ms)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
+55
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+168
@@ -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)
|
||||
}
|
||||
}
|
||||
+10
@@ -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)
|
||||
}
|
||||
+44
@@ -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()
|
||||
}
|
||||
}
|
||||
+106
@@ -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, ¶ms)
|
||||
)
|
||||
|
||||
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, ¶ms)
|
||||
|
||||
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))
|
||||
)
|
||||
}
|
||||
}
|
||||
+33
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+56
@@ -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) {}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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';
|
||||
+13
@@ -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';
|
||||
+27
@@ -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);
|
||||
}
|
||||
}
|
||||
Vendored
+42
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
+2198
File diff suppressed because it is too large.
Load diff
Vendored
+1625
File diff suppressed because it is too large.
Load diff
Vendored
+119
@@ -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();
|
||||
Vendored
+33
@@ -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;
|
||||
+14
@@ -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;
|
||||
+72
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+83
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
+110
@@ -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>();
|
||||
@@ -0,0 +1,51 @@
|
||||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Julien Muret <birros@protonmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
/// {@template wakelock}
|
||||
/// Wakelock
|
||||
/// --------
|
||||
///
|
||||
/// This class acquires & releases the wakelock based on reference count.
|
||||
///
|
||||
/// {@endtemplate}
|
||||
class Wakelock {
|
||||
/// {@macro wakelock}
|
||||
Wakelock();
|
||||
|
||||
/// Marks the wakelock as enabled for this instance.
|
||||
void enable() {
|
||||
if (!_enabled) {
|
||||
_enabled = true;
|
||||
_count++;
|
||||
_update();
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks the wakelock as disabled for this instance.
|
||||
void disable() {
|
||||
if (_enabled) {
|
||||
_enabled = false;
|
||||
_count--;
|
||||
_update();
|
||||
}
|
||||
}
|
||||
|
||||
/// Acquires the wakelock if enabled count is greater than 0.
|
||||
void _update() {
|
||||
if (_count > 0) {
|
||||
WakelockPlus.enable().catchError((_) {});
|
||||
} else {
|
||||
WakelockPlus.disable().catchError((_) {});
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the wakelock is enabled for this instance.
|
||||
bool _enabled = false;
|
||||
|
||||
/// The number of [Wakelock] instances in enabled state.
|
||||
static int _count = 0;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
|
||||
export 'video_texture.dart' if (dart.library.js_interop) 'video_web.dart';
|
||||
@@ -0,0 +1,534 @@
|
||||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:media_kit_video/media_kit_video_controls/media_kit_video_controls.dart';
|
||||
|
||||
import 'package:media_kit_video/src/subtitle/subtitle_view.dart';
|
||||
import 'package:media_kit_video/media_kit_video_controls/media_kit_video_controls.dart'
|
||||
as media_kit_video_controls;
|
||||
import 'package:media_kit_video/src/utils/dispose_safe_notifer.dart';
|
||||
|
||||
import 'package:media_kit_video/src/utils/wakelock.dart';
|
||||
import 'package:media_kit_video/src/video_view_parameters.dart';
|
||||
import 'package:media_kit_video/src/video_controller/video_controller.dart';
|
||||
import 'package:media_kit_video/src/video_controller/platform_video_controller.dart';
|
||||
|
||||
/// {@template video}
|
||||
///
|
||||
/// Video
|
||||
/// -----
|
||||
/// [Video] widget is used to display video output.
|
||||
///
|
||||
/// Use [VideoController] to initialize & handle the video rendering.
|
||||
///
|
||||
/// **Example:**
|
||||
///
|
||||
/// ```dart
|
||||
/// class MyScreen extends StatefulWidget {
|
||||
/// const MyScreen({Key? key}) : super(key: key);
|
||||
/// @override
|
||||
/// State<MyScreen> createState() => MyScreenState();
|
||||
/// }
|
||||
///
|
||||
/// class MyScreenState extends State<MyScreen> {
|
||||
/// late final player = Player();
|
||||
/// late final controller = VideoController(player);
|
||||
///
|
||||
/// @override
|
||||
/// void initState() {
|
||||
/// super.initState();
|
||||
/// player.open(Media('https://user-images.githubusercontent.com/28951144/229373695-22f88f13-d18f-4288-9bf1-c3e078d83722.mp4'));
|
||||
/// }
|
||||
///
|
||||
/// @override
|
||||
/// void dispose() {
|
||||
/// player.dispose();
|
||||
/// super.dispose();
|
||||
/// }
|
||||
///
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return Scaffold(
|
||||
/// body: Video(
|
||||
/// controller: controller,
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// {@endtemplate}
|
||||
class Video extends StatefulWidget {
|
||||
/// The [VideoController] reference to control this [Video] output.
|
||||
final VideoController controller;
|
||||
|
||||
/// Width of this viewport.
|
||||
final double? width;
|
||||
|
||||
/// Height of this viewport.
|
||||
final double? height;
|
||||
|
||||
/// Fit of the viewport.
|
||||
final BoxFit fit;
|
||||
|
||||
/// Background color to fill the video background.
|
||||
final Color fill;
|
||||
|
||||
/// Alignment of the viewport.
|
||||
final Alignment alignment;
|
||||
|
||||
/// Preferred aspect ratio of the viewport.
|
||||
final double? aspectRatio;
|
||||
|
||||
/// Filter quality of the [Texture] widget displaying the video output.
|
||||
final FilterQuality filterQuality;
|
||||
|
||||
/// Video controls builder.
|
||||
final VideoControlsBuilder? controls;
|
||||
|
||||
/// Whether to acquire wake lock while playing the video.
|
||||
final bool wakelock;
|
||||
|
||||
/// Whether to pause the video when application enters background mode.
|
||||
final bool pauseUponEnteringBackgroundMode;
|
||||
|
||||
/// Whether to resume the video when application enters foreground mode.
|
||||
///
|
||||
/// This attribute is only applicable if [pauseUponEnteringBackgroundMode] is `true`.
|
||||
///
|
||||
final bool resumeUponEnteringForegroundMode;
|
||||
|
||||
/// The configuration for subtitles e.g. [TextStyle] & padding etc.
|
||||
final SubtitleViewConfiguration subtitleViewConfiguration;
|
||||
|
||||
/// The callback invoked when the [Video] enters fullscreen.
|
||||
final Future<void> Function() onEnterFullscreen;
|
||||
|
||||
/// The callback invoked when the [Video] exits fullscreen.
|
||||
final Future<void> Function() onExitFullscreen;
|
||||
|
||||
/// FocusNode for keyboard input.
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// {@macro video}
|
||||
const Video({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.width,
|
||||
this.height,
|
||||
this.fit = BoxFit.contain,
|
||||
this.fill = const Color(0xFF000000),
|
||||
this.alignment = Alignment.center,
|
||||
this.aspectRatio,
|
||||
this.filterQuality = FilterQuality.low,
|
||||
this.controls = media_kit_video_controls.AdaptiveVideoControls,
|
||||
this.wakelock = true,
|
||||
this.pauseUponEnteringBackgroundMode = true,
|
||||
this.resumeUponEnteringForegroundMode = false,
|
||||
this.subtitleViewConfiguration = const SubtitleViewConfiguration(),
|
||||
this.onEnterFullscreen = defaultEnterNativeFullscreen,
|
||||
this.onExitFullscreen = defaultExitNativeFullscreen,
|
||||
this.focusNode,
|
||||
});
|
||||
|
||||
@override
|
||||
State<Video> createState() => VideoState();
|
||||
}
|
||||
|
||||
class VideoState extends State<Video> with WidgetsBindingObserver {
|
||||
late final _contextNotifier = DisposeSafeNotifier<BuildContext?>(null);
|
||||
late ValueNotifier<VideoViewParameters> videoViewParametersNotifier;
|
||||
late bool _disposeNotifiers;
|
||||
final _subtitleViewKey = GlobalKey<SubtitleViewState>();
|
||||
final _wakelock = Wakelock();
|
||||
final _subscriptions = <StreamSubscription>[];
|
||||
late int? _width = widget.controller.player.state.width;
|
||||
late int? _height = widget.controller.player.state.height;
|
||||
late bool _visible = (_width ?? 0) > 0 && (_height ?? 0) > 0;
|
||||
|
||||
bool _pauseDueToPauseUponEnteringBackgroundMode = false;
|
||||
// Public API:
|
||||
bool isFullscreen() {
|
||||
return media_kit_video_controls.isFullscreen(_contextNotifier.value!);
|
||||
}
|
||||
|
||||
Future<void> enterFullscreen() {
|
||||
return media_kit_video_controls.enterFullscreen(_contextNotifier.value!);
|
||||
}
|
||||
|
||||
Future<void> exitFullscreen() {
|
||||
return media_kit_video_controls.exitFullscreen(_contextNotifier.value!);
|
||||
}
|
||||
|
||||
Future<void> toggleFullscreen() {
|
||||
return media_kit_video_controls.toggleFullscreen(_contextNotifier.value!);
|
||||
}
|
||||
|
||||
void setSubtitleViewPadding(
|
||||
EdgeInsets padding, {
|
||||
Duration duration = const Duration(milliseconds: 100),
|
||||
}) {
|
||||
return _subtitleViewKey.currentState?.setPadding(
|
||||
padding,
|
||||
duration: duration,
|
||||
);
|
||||
}
|
||||
|
||||
void update({
|
||||
double? width,
|
||||
double? height,
|
||||
BoxFit? fit,
|
||||
Color? fill,
|
||||
Alignment? alignment,
|
||||
double? aspectRatio,
|
||||
FilterQuality? filterQuality,
|
||||
VideoControlsBuilder? controls,
|
||||
SubtitleViewConfiguration? subtitleViewConfiguration,
|
||||
FocusNode? focusNode,
|
||||
}) {
|
||||
videoViewParametersNotifier.value =
|
||||
videoViewParametersNotifier.value.copyWith(
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
fill: fill,
|
||||
alignment: alignment,
|
||||
aspectRatio: aspectRatio,
|
||||
filterQuality: filterQuality,
|
||||
controls: controls,
|
||||
subtitleViewConfiguration: subtitleViewConfiguration,
|
||||
focusNode: focusNode,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
videoViewParametersNotifier =
|
||||
media_kit_video_controls.VideoStateInheritedWidget.maybeOf(
|
||||
context,
|
||||
)?.videoViewParametersNotifier ??
|
||||
ValueNotifier<VideoViewParameters>(
|
||||
VideoViewParameters(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
fit: widget.fit,
|
||||
fill: widget.fill,
|
||||
alignment: widget.alignment,
|
||||
aspectRatio: widget.aspectRatio,
|
||||
filterQuality: widget.filterQuality,
|
||||
controls: widget.controls,
|
||||
subtitleViewConfiguration: widget.subtitleViewConfiguration,
|
||||
focusNode: widget.focusNode,
|
||||
),
|
||||
);
|
||||
_disposeNotifiers =
|
||||
media_kit_video_controls.VideoStateInheritedWidget.maybeOf(
|
||||
context,
|
||||
)?.disposeNotifiers ??
|
||||
true;
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(Video oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
final currentParams = videoViewParametersNotifier.value;
|
||||
|
||||
final newParams = currentParams.copyWith(
|
||||
width:
|
||||
widget.width != oldWidget.width ? widget.width : currentParams.width,
|
||||
height: widget.height != oldWidget.height
|
||||
? widget.height
|
||||
: currentParams.height,
|
||||
fit: widget.fit != oldWidget.fit ? widget.fit : currentParams.fit,
|
||||
fill: widget.fill != oldWidget.fill ? widget.fill : currentParams.fill,
|
||||
alignment: widget.alignment != oldWidget.alignment
|
||||
? widget.alignment
|
||||
: currentParams.alignment,
|
||||
aspectRatio: widget.aspectRatio != oldWidget.aspectRatio
|
||||
? widget.aspectRatio
|
||||
: currentParams.aspectRatio,
|
||||
filterQuality: widget.filterQuality != oldWidget.filterQuality
|
||||
? widget.filterQuality
|
||||
: currentParams.filterQuality,
|
||||
controls: widget.controls != oldWidget.controls
|
||||
? widget.controls
|
||||
: currentParams.controls,
|
||||
subtitleViewConfiguration: widget.subtitleViewConfiguration !=
|
||||
oldWidget.subtitleViewConfiguration
|
||||
? widget.subtitleViewConfiguration
|
||||
: currentParams.subtitleViewConfiguration,
|
||||
focusNode: widget.focusNode != oldWidget.focusNode
|
||||
? widget.focusNode
|
||||
: currentParams.focusNode,
|
||||
);
|
||||
|
||||
if (newParams != currentParams) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
videoViewParametersNotifier.value = newParams;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (widget.pauseUponEnteringBackgroundMode) {
|
||||
if ([
|
||||
AppLifecycleState.paused,
|
||||
AppLifecycleState.detached,
|
||||
].contains(state)) {
|
||||
if (widget.controller.player.state.playing) {
|
||||
_pauseDueToPauseUponEnteringBackgroundMode = true;
|
||||
widget.controller.player.pause();
|
||||
}
|
||||
} else {
|
||||
if (widget.resumeUponEnteringForegroundMode &&
|
||||
_pauseDueToPauseUponEnteringBackgroundMode) {
|
||||
_pauseDueToPauseUponEnteringBackgroundMode = false;
|
||||
widget.controller.player.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
super.didChangeAppLifecycleState(state);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
// --------------------------------------------------
|
||||
// Do not show the video frame until width & height are available.
|
||||
// Since [ValueNotifier<Rect?>] inside [VideoController] only gets updated by the render loop (i.e. it will not fire when video's width & height are not available etc.), it's important to handle this separately here.
|
||||
_subscriptions.addAll(
|
||||
[
|
||||
widget.controller.player.stream.width.listen(
|
||||
(value) {
|
||||
_width = value;
|
||||
final visible = (_width ?? 0) > 0 && (_height ?? 0) > 0;
|
||||
if (_visible != visible) {
|
||||
setState(() {
|
||||
_visible = visible;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
widget.controller.player.stream.height.listen(
|
||||
(value) {
|
||||
_height = value;
|
||||
final visible = (_width ?? 0) > 0 && (_height ?? 0) > 0;
|
||||
if (_visible != visible) {
|
||||
setState(() {
|
||||
_visible = visible;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
// --------------------------------------------------
|
||||
if (widget.wakelock) {
|
||||
if (widget.controller.player.state.playing) {
|
||||
_wakelock.enable();
|
||||
}
|
||||
_subscriptions.add(
|
||||
widget.controller.player.stream.playing.listen(
|
||||
(value) {
|
||||
if (value) {
|
||||
_wakelock.enable();
|
||||
} else {
|
||||
_wakelock.disable();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_wakelock.disable();
|
||||
for (final subscription in _subscriptions) {
|
||||
subscription.cancel();
|
||||
}
|
||||
if (_disposeNotifiers) {
|
||||
videoViewParametersNotifier.dispose();
|
||||
_contextNotifier.dispose();
|
||||
VideoStateInheritedWidgetContextNotifierState.fallback.remove(this);
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void refreshView() {}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return media_kit_video_controls.VideoStateInheritedWidget(
|
||||
state: this as dynamic,
|
||||
contextNotifier: _contextNotifier,
|
||||
videoViewParametersNotifier: videoViewParametersNotifier,
|
||||
child: ValueListenableBuilder<VideoViewParameters>(
|
||||
valueListenable: videoViewParametersNotifier,
|
||||
builder: (context, videoViewParameters, _) {
|
||||
return Container(
|
||||
clipBehavior: Clip.none,
|
||||
width: videoViewParameters.width,
|
||||
height: videoViewParameters.height,
|
||||
color: videoViewParameters.fill,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
ClipRect(
|
||||
child: FittedBox(
|
||||
fit: videoViewParameters.fit,
|
||||
alignment: videoViewParameters.alignment,
|
||||
child: ValueListenableBuilder<PlatformVideoController?>(
|
||||
valueListenable: widget.controller.notifier,
|
||||
builder: (context, notifier, _) => notifier == null
|
||||
? const SizedBox.shrink()
|
||||
: ValueListenableBuilder<int?>(
|
||||
valueListenable: notifier.id,
|
||||
builder: (context, id, _) {
|
||||
return ValueListenableBuilder<Rect?>(
|
||||
valueListenable: notifier.rect,
|
||||
builder: (context, rect, _) {
|
||||
if (id != null &&
|
||||
rect != null &&
|
||||
_visible) {
|
||||
return SizedBox(
|
||||
// Apply aspect ratio if provided.
|
||||
width:
|
||||
videoViewParameters.aspectRatio ==
|
||||
null
|
||||
? rect.width
|
||||
: rect.height *
|
||||
videoViewParameters
|
||||
.aspectRatio!,
|
||||
height: rect.height,
|
||||
child: Stack(
|
||||
children: [
|
||||
const SizedBox(),
|
||||
Positioned.fill(
|
||||
child: Texture(
|
||||
textureId: id,
|
||||
filterQuality:
|
||||
videoViewParameters
|
||||
.filterQuality,
|
||||
),
|
||||
),
|
||||
// Keep the |Texture| hidden before the first frame renders. In native implementation, if no default frame size is passed (through VideoController), a starting 1 pixel sized texture/surface is created to initialize the render context & check for H/W support.
|
||||
// This is then resized based on the video dimensions & accordingly texture ID, texture, EGLDisplay, EGLSurface etc. (depending upon platform) are also changed. Just don't show that 1 pixel texture to the UI.
|
||||
// NOTE: Unmounting |Texture| causes the |MarkTextureFrameAvailable| to not do anything on GNU/Linux.
|
||||
if (rect.width <= 1.0 &&
|
||||
rect.height <= 1.0)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color:
|
||||
videoViewParameters.fill,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (videoViewParameters.subtitleViewConfiguration.visible &&
|
||||
!(widget.controller.player.platform?.configuration.libass ??
|
||||
false))
|
||||
Positioned.fill(
|
||||
child: SubtitleView(
|
||||
controller: widget.controller,
|
||||
key: _subtitleViewKey,
|
||||
configuration:
|
||||
videoViewParameters.subtitleViewConfiguration,
|
||||
),
|
||||
),
|
||||
if (videoViewParameters.controls != null)
|
||||
Positioned.fill(
|
||||
child: videoViewParameters.controls!.call(this),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
typedef VideoControlsBuilder = Widget Function(VideoState state);
|
||||
|
||||
// --------------------------------------------------
|
||||
|
||||
/// Makes the native window enter fullscreen.
|
||||
Future<void> defaultEnterNativeFullscreen() async {
|
||||
try {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
await Future.wait(
|
||||
[
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.immersiveSticky,
|
||||
overlays: [],
|
||||
),
|
||||
SystemChrome.setPreferredOrientations(
|
||||
[
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.landscapeRight,
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
|
||||
await const MethodChannel('com.alexmercerind/media_kit_video')
|
||||
.invokeMethod(
|
||||
'Utils.EnterNativeFullscreen',
|
||||
);
|
||||
}
|
||||
} catch (exception, stacktrace) {
|
||||
debugPrint(exception.toString());
|
||||
debugPrint(stacktrace.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes the native window exit fullscreen.
|
||||
Future<void> defaultExitNativeFullscreen() async {
|
||||
try {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
await Future.wait(
|
||||
[
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values,
|
||||
),
|
||||
SystemChrome.setPreferredOrientations(
|
||||
[],
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
|
||||
await const MethodChannel('com.alexmercerind/media_kit_video')
|
||||
.invokeMethod(
|
||||
'Utils.ExitNativeFullscreen',
|
||||
);
|
||||
}
|
||||
} catch (exception, stacktrace) {
|
||||
debugPrint(exception.toString());
|
||||
debugPrint(stacktrace.toString());
|
||||
}
|
||||
}
|
||||
// --------------------------------------------------
|
||||
@@ -0,0 +1,483 @@
|
||||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
// ignore_for_file: avoid_web_libraries_in_flutter
|
||||
import 'dart:js_interop';
|
||||
|
||||
import 'package:web/web.dart';
|
||||
import 'dart:async';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
|
||||
import 'package:media_kit_video/media_kit_video_controls/media_kit_video_controls.dart'
|
||||
as media_kit_video_controls;
|
||||
import 'package:media_kit_video/src/utils/dispose_safe_notifer.dart';
|
||||
|
||||
import 'package:media_kit_video/src/utils/wakelock.dart';
|
||||
|
||||
/// {@template video}
|
||||
///
|
||||
/// Video
|
||||
/// -----
|
||||
/// [Video] widget is used to display video output.
|
||||
///
|
||||
/// Use [VideoController] to initialize & handle the video rendering.
|
||||
///
|
||||
/// **Example:**
|
||||
///
|
||||
/// ```dart
|
||||
/// class MyScreen extends StatefulWidget {
|
||||
/// const MyScreen({Key? key}) : super(key: key);
|
||||
/// @override
|
||||
/// State<MyScreen> createState() => MyScreenState();
|
||||
/// }
|
||||
///
|
||||
/// class MyScreenState extends State<MyScreen> {
|
||||
/// late final player = Player();
|
||||
/// late final controller = VideoController(player);
|
||||
///
|
||||
/// @override
|
||||
/// void initState() {
|
||||
/// super.initState();
|
||||
/// player.open(Media('https://user-images.githubusercontent.com/28951144/229373695-22f88f13-d18f-4288-9bf1-c3e078d83722.mp4'));
|
||||
/// }
|
||||
///
|
||||
/// @override
|
||||
/// void dispose() {
|
||||
/// player.dispose();
|
||||
/// super.dispose();
|
||||
/// }
|
||||
///
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return Scaffold(
|
||||
/// body: Video(
|
||||
/// controller: controller,
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// {@endtemplate}
|
||||
class Video extends StatefulWidget {
|
||||
/// The [VideoController] reference to control this [Video] output.
|
||||
final VideoController controller;
|
||||
|
||||
/// Height of this viewport.
|
||||
final double? width;
|
||||
|
||||
/// Width of this viewport.
|
||||
final double? height;
|
||||
|
||||
/// Fit of the viewport.
|
||||
final BoxFit fit;
|
||||
|
||||
/// Background color to fill the video background.
|
||||
final Color fill;
|
||||
|
||||
/// Alignment of the viewport.
|
||||
final Alignment alignment;
|
||||
|
||||
/// Preferred aspect ratio of the viewport.
|
||||
final double? aspectRatio;
|
||||
|
||||
/// Filter quality of the [Texture] widget displaying the video output.
|
||||
final FilterQuality filterQuality;
|
||||
|
||||
/// Video controls builder.
|
||||
final /* VideoControlsBuilder? */ dynamic controls;
|
||||
|
||||
/// Whether to acquire wake lock while playing the video.
|
||||
final bool wakelock;
|
||||
|
||||
/// Whether to pause the video when application enters background mode.
|
||||
final bool pauseUponEnteringBackgroundMode;
|
||||
|
||||
/// Whether to resume the video when application enters foreground mode.
|
||||
///
|
||||
/// This attribute is only applicable if [pauseUponEnteringBackgroundMode] is `true`.
|
||||
///
|
||||
final bool resumeUponEnteringForegroundMode;
|
||||
|
||||
/// The configuration for subtitles e.g. [TextStyle] & padding etc.
|
||||
final SubtitleViewConfiguration subtitleViewConfiguration;
|
||||
|
||||
/// The callback invoked when the [Video] enters fullscreen.
|
||||
final Future<void> Function() onEnterFullscreen;
|
||||
|
||||
/// The callback invoked when the [Video] exits fullscreen.
|
||||
final Future<void> Function() onExitFullscreen;
|
||||
|
||||
/// FocusNode for keyboard input.
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// {@macro video}
|
||||
const Video({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.width,
|
||||
this.height,
|
||||
this.fit = BoxFit.contain,
|
||||
this.fill = const Color(0xFF000000),
|
||||
this.alignment = Alignment.center,
|
||||
this.aspectRatio,
|
||||
this.filterQuality = FilterQuality.low,
|
||||
this.controls = media_kit_video_controls.AdaptiveVideoControls,
|
||||
this.wakelock = true,
|
||||
this.pauseUponEnteringBackgroundMode = true,
|
||||
this.resumeUponEnteringForegroundMode = false,
|
||||
this.subtitleViewConfiguration = const SubtitleViewConfiguration(),
|
||||
this.onEnterFullscreen = defaultEnterNativeFullscreen,
|
||||
this.onExitFullscreen = defaultExitNativeFullscreen,
|
||||
this.focusNode,
|
||||
});
|
||||
|
||||
@override
|
||||
State<Video> createState() => VideoState();
|
||||
}
|
||||
|
||||
class VideoState extends State<Video> with WidgetsBindingObserver {
|
||||
late final _contextNotifier = DisposeSafeNotifier<BuildContext?>(null);
|
||||
late ValueNotifier<VideoViewParameters> videoViewParametersNotifier;
|
||||
late bool _disposeNotifiers;
|
||||
final _subtitleViewKey = GlobalKey<SubtitleViewState>();
|
||||
final _wakelock = Wakelock();
|
||||
final _subscriptions = <StreamSubscription>[];
|
||||
late int? _width = widget.controller.player.state.width;
|
||||
late int? _height = widget.controller.player.state.height;
|
||||
late bool _visible = (_width ?? 0) > 0 && (_height ?? 0) > 0;
|
||||
bool _pauseDueToPauseUponEnteringBackgroundMode = false;
|
||||
|
||||
ValueKey _key = const ValueKey(true);
|
||||
|
||||
// Public API:
|
||||
|
||||
bool isFullscreen() {
|
||||
return media_kit_video_controls.isFullscreen(_contextNotifier.value!);
|
||||
}
|
||||
|
||||
Future<void> enterFullscreen() {
|
||||
return media_kit_video_controls.enterFullscreen(_contextNotifier.value!);
|
||||
}
|
||||
|
||||
Future<void> exitFullscreen() {
|
||||
return media_kit_video_controls.exitFullscreen(_contextNotifier.value!);
|
||||
}
|
||||
|
||||
Future<void> toggleFullscreen() {
|
||||
return media_kit_video_controls.toggleFullscreen(_contextNotifier.value!);
|
||||
}
|
||||
|
||||
void setSubtitleViewPadding(
|
||||
EdgeInsets padding, {
|
||||
Duration duration = const Duration(milliseconds: 100),
|
||||
}) {
|
||||
return _subtitleViewKey.currentState?.setPadding(
|
||||
padding,
|
||||
duration: duration,
|
||||
);
|
||||
}
|
||||
|
||||
void update({
|
||||
double? width,
|
||||
double? height,
|
||||
BoxFit? fit,
|
||||
Color? fill,
|
||||
Alignment? alignment,
|
||||
double? aspectRatio,
|
||||
FilterQuality? filterQuality,
|
||||
/* VideoControlsBuilder? */ dynamic controls,
|
||||
SubtitleViewConfiguration? subtitleViewConfiguration,
|
||||
FocusNode? focusNode,
|
||||
}) {
|
||||
videoViewParametersNotifier.value =
|
||||
videoViewParametersNotifier.value.copyWith(
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
fill: fill,
|
||||
alignment: alignment,
|
||||
aspectRatio: aspectRatio,
|
||||
filterQuality: filterQuality,
|
||||
controls: controls,
|
||||
subtitleViewConfiguration: subtitleViewConfiguration,
|
||||
focusNode: focusNode,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(Video oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
final currentParams = videoViewParametersNotifier.value;
|
||||
|
||||
final newParams = currentParams.copyWith(
|
||||
width:
|
||||
widget.width != oldWidget.width ? widget.width : currentParams.width,
|
||||
height: widget.height != oldWidget.height
|
||||
? widget.height
|
||||
: currentParams.height,
|
||||
fit: widget.fit != oldWidget.fit ? widget.fit : currentParams.fit,
|
||||
fill: widget.fill != oldWidget.fill ? widget.fill : currentParams.fill,
|
||||
alignment: widget.alignment != oldWidget.alignment
|
||||
? widget.alignment
|
||||
: currentParams.alignment,
|
||||
aspectRatio: widget.aspectRatio != oldWidget.aspectRatio
|
||||
? widget.aspectRatio
|
||||
: currentParams.aspectRatio,
|
||||
filterQuality: widget.filterQuality != oldWidget.filterQuality
|
||||
? widget.filterQuality
|
||||
: currentParams.filterQuality,
|
||||
controls: widget.controls != oldWidget.controls
|
||||
? widget.controls
|
||||
: currentParams.controls,
|
||||
subtitleViewConfiguration: widget.subtitleViewConfiguration !=
|
||||
oldWidget.subtitleViewConfiguration
|
||||
? widget.subtitleViewConfiguration
|
||||
: currentParams.subtitleViewConfiguration,
|
||||
focusNode: widget.focusNode != oldWidget.focusNode
|
||||
? widget.focusNode
|
||||
: currentParams.focusNode,
|
||||
);
|
||||
|
||||
if (newParams != currentParams) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
videoViewParametersNotifier.value = newParams;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
videoViewParametersNotifier =
|
||||
media_kit_video_controls.VideoStateInheritedWidget.maybeOf(
|
||||
context,
|
||||
)?.videoViewParametersNotifier ??
|
||||
ValueNotifier<VideoViewParameters>(
|
||||
VideoViewParameters(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
fit: widget.fit,
|
||||
fill: widget.fill,
|
||||
alignment: widget.alignment,
|
||||
aspectRatio: widget.aspectRatio,
|
||||
filterQuality: widget.filterQuality,
|
||||
controls: widget.controls,
|
||||
subtitleViewConfiguration: widget.subtitleViewConfiguration,
|
||||
focusNode: widget.focusNode,
|
||||
),
|
||||
);
|
||||
_disposeNotifiers =
|
||||
media_kit_video_controls.VideoStateInheritedWidget.maybeOf(
|
||||
context,
|
||||
)?.disposeNotifiers ??
|
||||
true;
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (widget.pauseUponEnteringBackgroundMode) {
|
||||
if ([
|
||||
AppLifecycleState.paused,
|
||||
AppLifecycleState.detached,
|
||||
].contains(state)) {
|
||||
if (widget.controller.player.state.playing) {
|
||||
_pauseDueToPauseUponEnteringBackgroundMode = true;
|
||||
widget.controller.player.pause();
|
||||
}
|
||||
} else {
|
||||
if (widget.resumeUponEnteringForegroundMode &&
|
||||
_pauseDueToPauseUponEnteringBackgroundMode) {
|
||||
_pauseDueToPauseUponEnteringBackgroundMode = false;
|
||||
widget.controller.player.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
super.didChangeAppLifecycleState(state);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
// --------------------------------------------------
|
||||
// Do not show the video frame until width & height are available.
|
||||
// Since [ValueNotifier<Rect?>] inside [VideoController] only gets updated by the render loop (i.e. it will not fire when video's width & height are not available etc.), it's important to handle this separately here.
|
||||
_subscriptions.addAll(
|
||||
[
|
||||
widget.controller.player.stream.width.listen(
|
||||
(value) {
|
||||
_width = value;
|
||||
final visible = (_width ?? 0) > 0 && (_height ?? 0) > 0;
|
||||
if (_visible != visible) {
|
||||
setState(() {
|
||||
_visible = visible;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
widget.controller.player.stream.height.listen(
|
||||
(value) {
|
||||
_height = value;
|
||||
final visible = (_width ?? 0) > 0 && (_height ?? 0) > 0;
|
||||
if (_visible != visible) {
|
||||
setState(() {
|
||||
_visible = visible;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
// --------------------------------------------------
|
||||
if (widget.wakelock) {
|
||||
if (widget.controller.player.state.playing) {
|
||||
_wakelock.enable();
|
||||
}
|
||||
_subscriptions.add(
|
||||
widget.controller.player.stream.playing.listen(
|
||||
(value) {
|
||||
if (value) {
|
||||
_wakelock.enable();
|
||||
} else {
|
||||
_wakelock.disable();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_wakelock.disable();
|
||||
for (final subscription in _subscriptions) {
|
||||
subscription.cancel();
|
||||
}
|
||||
if (_disposeNotifiers) {
|
||||
videoViewParametersNotifier.dispose();
|
||||
_contextNotifier.dispose();
|
||||
// ignore: collection_methods_unrelated_type
|
||||
VideoStateInheritedWidgetContextNotifierState.fallback.remove(this);
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void refreshView() {
|
||||
setState(() {
|
||||
_key = ValueKey(!_key.value);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return media_kit_video_controls.VideoStateInheritedWidget(
|
||||
state: this as dynamic,
|
||||
contextNotifier: _contextNotifier,
|
||||
videoViewParametersNotifier: videoViewParametersNotifier,
|
||||
child: ValueListenableBuilder<VideoViewParameters>(
|
||||
valueListenable: videoViewParametersNotifier,
|
||||
builder: (context, videoViewParameters, _) {
|
||||
return Container(
|
||||
clipBehavior: Clip.none,
|
||||
width: videoViewParameters.width,
|
||||
height: videoViewParameters.height,
|
||||
color: videoViewParameters.fill,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
ClipRect(
|
||||
child: FittedBox(
|
||||
fit: videoViewParameters.fit,
|
||||
alignment: videoViewParameters.alignment,
|
||||
child: ValueListenableBuilder<PlatformVideoController?>(
|
||||
valueListenable: widget.controller.notifier,
|
||||
builder: (context, notifier, _) => notifier == null
|
||||
? const SizedBox.shrink()
|
||||
: ValueListenableBuilder<int?>(
|
||||
valueListenable: notifier.id,
|
||||
builder: (context, id, _) {
|
||||
return ValueListenableBuilder<Rect?>(
|
||||
valueListenable: notifier.rect,
|
||||
builder: (context, rect, _) {
|
||||
if (id != null &&
|
||||
rect != null &&
|
||||
_visible) {
|
||||
return SizedBox(
|
||||
// Apply aspect ratio if provided.
|
||||
width:
|
||||
videoViewParameters.aspectRatio ==
|
||||
null
|
||||
? rect.width
|
||||
: rect.height *
|
||||
videoViewParameters
|
||||
.aspectRatio!,
|
||||
height: rect.height,
|
||||
child: HtmlElementView(
|
||||
key: _key,
|
||||
viewType:
|
||||
'com.alexmercerind.media_kit_video.$id',
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (videoViewParameters.subtitleViewConfiguration.visible &&
|
||||
!(widget.controller.player.platform?.configuration.libass ??
|
||||
false))
|
||||
Positioned.fill(
|
||||
child: SubtitleView(
|
||||
controller: widget.controller,
|
||||
key: _subtitleViewKey,
|
||||
configuration:
|
||||
videoViewParameters.subtitleViewConfiguration,
|
||||
),
|
||||
),
|
||||
if (videoViewParameters.controls != null)
|
||||
Positioned.fill(
|
||||
child: videoViewParameters.controls!.call(this as dynamic),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
|
||||
/// Makes the native window enter fullscreen.
|
||||
Future<void> defaultEnterNativeFullscreen() async {
|
||||
try {
|
||||
await document.documentElement?.requestFullscreen().toDart;
|
||||
} catch (exception, stacktrace) {
|
||||
debugPrint(exception.toString());
|
||||
debugPrint(stacktrace.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes the native window exit fullscreen.
|
||||
Future<void> defaultExitNativeFullscreen() async {
|
||||
try {
|
||||
await document.exitFullscreen().toDart;
|
||||
} catch (exception, stacktrace) {
|
||||
debugPrint(exception.toString());
|
||||
debugPrint(stacktrace.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
export 'real.dart' if (dart.library.js_interop) 'stub.dart';
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
||||
import 'package:media_kit_video/src/utils/query_decoders.dart';
|
||||
import 'package:media_kit_video/src/video_controller/platform_video_controller.dart';
|
||||
|
||||
/// {@template android_video_controller}
|
||||
///
|
||||
/// AndroidVideoController
|
||||
/// ----------------------
|
||||
///
|
||||
/// The [PlatformVideoController] implementation based on native JNI & C/C++ used on Android.
|
||||
///
|
||||
/// {@endtemplate}
|
||||
class AndroidVideoController extends PlatformVideoController {
|
||||
/// Whether [AndroidVideoController] is supported on the current platform or not.
|
||||
static bool get supported => Platform.isAndroid;
|
||||
|
||||
/// Pointer address to the global object reference of `android.view.Surface` i.e. `(intptr_t)(*android.view.Surface)`.
|
||||
final ValueNotifier<int?> wid = ValueNotifier<int?>(null);
|
||||
|
||||
/// [Lock] used to synchronize [onLoadHooks], [onUnloadHooks] & [subscription].
|
||||
final lock = Lock();
|
||||
|
||||
NativePlayer get platform => player.platform as NativePlayer;
|
||||
|
||||
Future<void> setProperty(String key, String value) async {
|
||||
await platform.setProperty(key, value, waitForInitialization: false);
|
||||
}
|
||||
|
||||
Future<void> setProperties(Map<String, String> properties) async {
|
||||
for (final entry in properties.entries) {
|
||||
await setProperty(entry.key, entry.value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Listener for updating the --wid property.
|
||||
Future<void> widListener() {
|
||||
return lock.synchronized(() async {
|
||||
final width = rect.value?.width.toInt() ?? 1;
|
||||
final height = rect.value?.height.toInt() ?? 1;
|
||||
final androidSurfaceSizeValue = [width, height].join('x');
|
||||
final widValue = wid.value?.toString() ?? '0';
|
||||
// When --wid is 0, vo=null is required to avoid SIGSEGV.
|
||||
final voValue = widValue == '0' ? 'null' : configuration.vo!;
|
||||
final vidValue = widValue == '0' ? 'no' : 'auto';
|
||||
// It is important to re-initialize --vo after --android-surface-size.
|
||||
await setProperty('vo', 'null');
|
||||
await setProperties(
|
||||
{
|
||||
// ORDER IS IMPORTANT.
|
||||
'android-surface-size': androidSurfaceSizeValue,
|
||||
'wid': widValue,
|
||||
'vo': voValue,
|
||||
// It is important to re-initialize --vid in-case of --vo=mediacodec_embed.
|
||||
// Not doing so causes error "Could not open codec." & video never gets rendered.
|
||||
if (configuration.vo == 'mediacodec_embed') 'vid': vidValue,
|
||||
},
|
||||
);
|
||||
// Instead of seeking to the start (Duration.zero), seek to the current playback position
|
||||
// without jumping the user to the start of the media.
|
||||
final currentPosition = player.state.position;
|
||||
await player.seek(currentPosition);
|
||||
});
|
||||
}
|
||||
|
||||
/// [StreamSubscription] for listening to video [Rect].
|
||||
StreamSubscription<VideoParams>? videoParamsSubscription;
|
||||
|
||||
/// {@macro android_video_controller}
|
||||
AndroidVideoController._(
|
||||
super.player,
|
||||
super.configuration,
|
||||
) {
|
||||
wid.addListener(widListener);
|
||||
videoParamsSubscription = player.stream.videoParams.listen(
|
||||
(event) => lock.synchronized(() async {
|
||||
final int width;
|
||||
final int height;
|
||||
if (event.rotate == 0 || event.rotate == 180) {
|
||||
width = event.dw ?? 0;
|
||||
height = event.dh ?? 0;
|
||||
} else {
|
||||
// width & height are swapped for 90 or 270 degrees rotation.
|
||||
width = event.dh ?? 0;
|
||||
height = event.dw ?? 0;
|
||||
}
|
||||
|
||||
final isZero = width == 0 || height == 0;
|
||||
final isSame = width == rect.value?.width.toInt() &&
|
||||
height == rect.value?.height.toInt();
|
||||
if (isZero || isSame) {
|
||||
return;
|
||||
}
|
||||
|
||||
final handle = await player.handle;
|
||||
|
||||
await _channel.invokeMethod(
|
||||
'VideoOutputManager.SetSurfaceSize',
|
||||
{
|
||||
'handle': handle.toString(),
|
||||
'width': width.toString(),
|
||||
'height': height.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
rect.value = Rect.fromLTWH(
|
||||
0.0,
|
||||
0.0,
|
||||
width.toDouble(),
|
||||
height.toDouble(),
|
||||
);
|
||||
|
||||
if (!waitUntilFirstFrameRenderedCompleter.isCompleted) {
|
||||
waitUntilFirstFrameRenderedCompleter.complete();
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/// {@macro android_video_controller}
|
||||
static Future<PlatformVideoController> create(
|
||||
Player player,
|
||||
VideoControllerConfiguration configuration,
|
||||
) async {
|
||||
Future<String> getDefaultHwdec() async {
|
||||
// Enforce software rendering in emulators.
|
||||
bool hw = configuration.enableHardwareAcceleration;
|
||||
final bool isEmulator = await _channel.invokeMethod('Utils.IsEmulator');
|
||||
if (isEmulator) {
|
||||
hw = false;
|
||||
debugPrint('media_kit: Emulator detected.');
|
||||
debugPrint('media_kit: Enforcing S/W rendering.');
|
||||
}
|
||||
return hw ? 'auto-safe' : 'no';
|
||||
}
|
||||
|
||||
// Update [configuration] to have default values.
|
||||
configuration = configuration.copyWith(
|
||||
vo: configuration.vo ?? 'gpu',
|
||||
hwdec: configuration.hwdec ?? await getDefaultHwdec(),
|
||||
);
|
||||
|
||||
// Retrieve the native handle of the [Player].
|
||||
final handle = await player.handle;
|
||||
// Return the existing [VideoController] if it's already created.
|
||||
if (_controllers.containsKey(handle)) {
|
||||
return _controllers[handle]!;
|
||||
}
|
||||
|
||||
// In case no video-decoders are found, this means media_kit_libs_***_audio is being used.
|
||||
// Thus, --vid=no is required to prevent libmpv from trying to decode video (otherwise bad things may happen).
|
||||
//
|
||||
// Search for common H264 decoder to check if video support is available.
|
||||
final decoders = await queryDecoders(handle);
|
||||
if (!decoders.contains('h264')) {
|
||||
throw UnsupportedError(
|
||||
'[VideoController] is not available.'
|
||||
' '
|
||||
'Please use media_kit_libs_***_video instead of media_kit_libs_***_audio.',
|
||||
);
|
||||
}
|
||||
|
||||
// Creation:
|
||||
final controller = AndroidVideoController._(
|
||||
player,
|
||||
configuration,
|
||||
);
|
||||
|
||||
// Register [_dispose] for execution upon [Player.dispose].
|
||||
player.platform?.release.add(controller._dispose);
|
||||
|
||||
// Store the [VideoController] in the [_controllers].
|
||||
_controllers[handle] = controller;
|
||||
|
||||
await _channel.invokeMethod(
|
||||
'VideoOutputManager.Create',
|
||||
{
|
||||
'handle': handle.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
await controller.setProperties(
|
||||
{
|
||||
// It is necessary to set vo=null here to avoid SIGSEGV, --wid must be assigned before vo=gpu is set.
|
||||
'vo': 'null',
|
||||
'hwdec': configuration.hwdec!,
|
||||
'vid': 'auto',
|
||||
'opengl-es': 'yes',
|
||||
'force-window': 'yes',
|
||||
'gpu-context': 'android',
|
||||
'sub-use-margins': 'no',
|
||||
'sub-font-provider': 'none',
|
||||
'sub-scale-with-window': 'yes',
|
||||
'hwdec-codecs': 'h264,hevc,mpeg4,mpeg2video,vp8,vp9,av1',
|
||||
},
|
||||
);
|
||||
|
||||
// Return the [PlatformVideoController].
|
||||
return controller;
|
||||
}
|
||||
|
||||
/// Sets the required size of the video output.
|
||||
/// This may yield substantial performance improvements if a small [width] & [height] is specified.
|
||||
///
|
||||
/// Remember:
|
||||
/// * “Premature optimization is the root of all evil”
|
||||
/// * “With great power comes great responsibility”
|
||||
@override
|
||||
Future<void> setSize({
|
||||
int? width,
|
||||
int? height,
|
||||
}) {
|
||||
throw UnsupportedError(
|
||||
'[AndroidVideoController.setSize] is not available on Android',
|
||||
);
|
||||
}
|
||||
|
||||
/// Disposes the instance. Releases allocated resources back to the system.
|
||||
Future<void> _dispose() async {
|
||||
super.dispose();
|
||||
wid.dispose();
|
||||
wid.removeListener(widListener);
|
||||
await videoParamsSubscription?.cancel();
|
||||
final handle = await player.handle;
|
||||
_controllers.remove(handle);
|
||||
await _channel.invokeMethod(
|
||||
'VideoOutputManager.Dispose',
|
||||
{
|
||||
'handle': handle.toString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Currently created [AndroidVideoController]s.
|
||||
static final _controllers = HashMap<int, AndroidVideoController>();
|
||||
|
||||
/// [MethodChannel] for invoking platform specific native implementation.
|
||||
static final _channel =
|
||||
const MethodChannel('com.alexmercerind/media_kit_video')
|
||||
..setMethodCallHandler(
|
||||
(MethodCall call) async {
|
||||
try {
|
||||
debugPrint(call.method.toString());
|
||||
debugPrint(call.arguments.toString());
|
||||
switch (call.method) {
|
||||
case 'VideoOutput.Resize':
|
||||
{
|
||||
// Notify about updated texture ID & [Rect].
|
||||
final int handle = call.arguments['handle'];
|
||||
final Rect rect = Rect.fromLTWH(
|
||||
call.arguments['rect']['left'] * 1.0,
|
||||
call.arguments['rect']['top'] * 1.0,
|
||||
call.arguments['rect']['width'] * 1.0,
|
||||
call.arguments['rect']['height'] * 1.0,
|
||||
);
|
||||
final int id = call.arguments['id'];
|
||||
final int wid = call.arguments['wid'];
|
||||
_controllers[handle]?.rect.value = rect;
|
||||
_controllers[handle]?.id.value = id;
|
||||
_controllers[handle]?.wid.value = wid;
|
||||
break;
|
||||
}
|
||||
case 'VideoOutput.WaitUntilFirstFrameRenderedNotify':
|
||||
{
|
||||
// Notify about updated texture ID & [Rect].
|
||||
final int handle = call.arguments['handle'];
|
||||
debugPrint(handle.toString());
|
||||
// Notify about the first frame being rendered.
|
||||
final completer = _controllers[handle]
|
||||
?.waitUntilFirstFrameRenderedCompleter;
|
||||
if (!(completer?.isCompleted ?? true)) {
|
||||
completer?.complete();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (exception, stacktrace) {
|
||||
debugPrint(exception.toString());
|
||||
debugPrint(stacktrace.toString());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
||||
import 'package:media_kit_video/src/video_controller/platform_video_controller.dart';
|
||||
|
||||
// Stub declaration for avoiding compilation errors on Dart JS using conditional imports.
|
||||
|
||||
class AndroidVideoController extends PlatformVideoController {
|
||||
static const bool supported = false;
|
||||
|
||||
AndroidVideoController._(
|
||||
super.player,
|
||||
super.configuration,
|
||||
);
|
||||
|
||||
static Future<PlatformVideoController> create(
|
||||
Player player,
|
||||
VideoControllerConfiguration configuration,
|
||||
) =>
|
||||
throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<void> setSize({int? width, int? height}) => throw UnimplementedError();
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
export 'real.dart' if (dart.library.js_interop) 'stub.dart';
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
||||
import 'package:media_kit_video/src/utils/query_decoders.dart';
|
||||
import 'package:media_kit_video/src/video_controller/platform_video_controller.dart';
|
||||
|
||||
/// {@template native_video_controller}
|
||||
///
|
||||
/// NativeVideoController
|
||||
/// ---------------------
|
||||
///
|
||||
/// The [PlatformVideoController] implementation based on native C/C++ used on:
|
||||
/// * Windows
|
||||
/// * GNU/Linux
|
||||
/// * macOS
|
||||
/// * iOS
|
||||
///
|
||||
/// {@endtemplate}
|
||||
class NativeVideoController extends PlatformVideoController {
|
||||
/// Whether [NativeVideoController] is supported on the current platform or not.
|
||||
static bool get supported =>
|
||||
Platform.isWindows ||
|
||||
Platform.isLinux ||
|
||||
Platform.isMacOS ||
|
||||
Platform.isIOS;
|
||||
|
||||
/// Fixed width of the video output.
|
||||
int? width;
|
||||
|
||||
/// Fixed height of the video output.
|
||||
int? height;
|
||||
|
||||
/// Width of the video (from [VideoParams]).
|
||||
int? videoParamsWidth;
|
||||
|
||||
/// Height of the video (from [VideoParams]).
|
||||
int? videoParamsHeight;
|
||||
|
||||
/// [Lock] used to synchronize [onLoadHooks], [onUnloadHooks] & [subscription].
|
||||
final lock = Lock();
|
||||
|
||||
NativePlayer get platform => player.platform as NativePlayer;
|
||||
|
||||
Future<void> setProperty(String key, String value) async {
|
||||
await platform.setProperty(key, value, waitForInitialization: false);
|
||||
}
|
||||
|
||||
Future<void> setProperties(Map<String, String> properties) async {
|
||||
// ORDER IS IMPORTANT.
|
||||
for (final entry in properties.entries) {
|
||||
await setProperty(entry.key, entry.value);
|
||||
}
|
||||
}
|
||||
|
||||
/// [StreamSubscription] for listening to video [Rect].
|
||||
StreamSubscription<VideoParams>? videoParamsSubscription;
|
||||
|
||||
/// {@macro native_video_controller}
|
||||
NativeVideoController._(
|
||||
super.player,
|
||||
super.configuration,
|
||||
) : width = configuration.width,
|
||||
height = configuration.height {
|
||||
videoParamsSubscription = player.stream.videoParams.listen(
|
||||
(event) => lock.synchronized(() async {
|
||||
if ([0, null].contains(event.dw) || [0, null].contains(event.dh)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int handle = await player.handle;
|
||||
|
||||
final int width;
|
||||
final int height;
|
||||
if (event.rotate == 0 || event.rotate == 180) {
|
||||
width = event.dw ?? 0;
|
||||
height = event.dh ?? 0;
|
||||
} else {
|
||||
// width & height are swapped for 90 or 270 degrees rotation.
|
||||
width = event.dh ?? 0;
|
||||
height = event.dw ?? 0;
|
||||
}
|
||||
|
||||
if (videoParamsWidth == width && videoParamsHeight == height) {
|
||||
return;
|
||||
}
|
||||
|
||||
videoParamsWidth = width;
|
||||
videoParamsHeight = height;
|
||||
|
||||
await _channel.invokeMethod(
|
||||
'VideoOutputManager.SetSize',
|
||||
{
|
||||
'handle': handle.toString(),
|
||||
'width': width.toString(),
|
||||
'height': height.toString(),
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/// {@macro native_video_controller}
|
||||
static Future<PlatformVideoController> create(
|
||||
Player player,
|
||||
VideoControllerConfiguration configuration,
|
||||
) async {
|
||||
// Update [configuration] to have default values.
|
||||
configuration = configuration.copyWith(
|
||||
vo: configuration.vo ?? 'libmpv',
|
||||
hwdec: configuration.hwdec ?? 'auto',
|
||||
);
|
||||
|
||||
// Retrieve the native handle of the [Player].
|
||||
final handle = await player.handle;
|
||||
// Return the existing [VideoController] if it's already created.
|
||||
if (_controllers.containsKey(handle)) {
|
||||
return _controllers[handle]!;
|
||||
}
|
||||
|
||||
// In case no video-decoders are found, this means media_kit_libs_***_audio is being used.
|
||||
// Thus, --vid=no is required to prevent libmpv from trying to decode video (otherwise bad things may happen).
|
||||
//
|
||||
// Search for common H264 decoder to check if video support is available.
|
||||
final decoders = await queryDecoders(handle);
|
||||
if (!decoders.contains('h264')) {
|
||||
throw UnsupportedError(
|
||||
'[VideoController] is not available.'
|
||||
' '
|
||||
'Please use media_kit_libs_***_video instead of media_kit_libs_***_audio.',
|
||||
);
|
||||
}
|
||||
|
||||
// Creation:
|
||||
final controller = NativeVideoController._(
|
||||
player,
|
||||
configuration,
|
||||
);
|
||||
|
||||
// Register [_dispose] for execution upon [Player.dispose].
|
||||
player.platform?.release.add(controller._dispose);
|
||||
|
||||
// Store the [NativeVideoController] in the [_controllers].
|
||||
_controllers[handle] = controller;
|
||||
|
||||
await controller.setProperties(
|
||||
{
|
||||
'vo': configuration.vo!,
|
||||
'hwdec': configuration.hwdec!,
|
||||
'vid': 'auto',
|
||||
},
|
||||
);
|
||||
|
||||
// Wait until first texture ID is received.
|
||||
// We are not waiting on the native-side itself because it will block the UI thread.
|
||||
final completer = Completer<void>();
|
||||
void listener() {
|
||||
final value = controller.id.value;
|
||||
if (value != null) {
|
||||
debugPrint('NativeVideoController: Texture ID: $value');
|
||||
completer.complete();
|
||||
}
|
||||
}
|
||||
|
||||
controller.id.addListener(listener);
|
||||
|
||||
await _channel.invokeMethod(
|
||||
'VideoOutputManager.Create',
|
||||
{
|
||||
'handle': handle.toString(),
|
||||
'configuration': {
|
||||
'width': configuration.width.toString(),
|
||||
'height': configuration.height.toString(),
|
||||
'enableHardwareAcceleration':
|
||||
configuration.enableHardwareAcceleration,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await completer.future;
|
||||
controller.id.removeListener(listener);
|
||||
|
||||
// Return the [VideoController].
|
||||
return controller;
|
||||
}
|
||||
|
||||
/// Sets the required size of the video output.
|
||||
/// This may yield substantial performance improvements if a small [width] & [height] is specified.
|
||||
///
|
||||
/// Remember:
|
||||
/// * “Premature optimization is the root of all evil”
|
||||
/// * “With great power comes great responsibility”
|
||||
@override
|
||||
Future<void> setSize({
|
||||
int? width,
|
||||
int? height,
|
||||
}) async {
|
||||
final handle = await player.handle;
|
||||
if (this.width == width && this.height == height) {
|
||||
// No need to resize if the requested size is same as the current size.
|
||||
return;
|
||||
}
|
||||
if (width != null && height != null) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
await _channel.invokeMethod(
|
||||
'VideoOutputManager.SetSize',
|
||||
{
|
||||
'handle': handle.toString(),
|
||||
'width': width.toString(),
|
||||
'height': height.toString(),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
this.width = null;
|
||||
this.height = null;
|
||||
await _channel.invokeMethod(
|
||||
'VideoOutputManager.SetSize',
|
||||
{
|
||||
'handle': handle.toString(),
|
||||
'width': videoParamsWidth?.toString() ?? 'null',
|
||||
'height': videoParamsHeight?.toString() ?? 'null',
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Disposes the instance. Releases allocated resources back to the system.
|
||||
Future<void> _dispose() async {
|
||||
super.dispose();
|
||||
await videoParamsSubscription?.cancel();
|
||||
final handle = await player.handle;
|
||||
_controllers.remove(handle);
|
||||
await _channel.invokeMethod(
|
||||
'VideoOutputManager.Dispose',
|
||||
{
|
||||
'handle': handle.toString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Currently created [NativeVideoController]s.
|
||||
/// This is used to notify about updated texture IDs & [Rect]s through [_channel].
|
||||
static final _controllers = HashMap<int, NativeVideoController>();
|
||||
|
||||
/// [MethodChannel] for invoking platform specific native implementation.
|
||||
static final _channel =
|
||||
const MethodChannel('com.alexmercerind/media_kit_video')
|
||||
..setMethodCallHandler(
|
||||
(MethodCall call) async {
|
||||
try {
|
||||
debugPrint(call.method.toString());
|
||||
debugPrint(call.arguments.toString());
|
||||
switch (call.method) {
|
||||
case 'VideoOutput.Resize':
|
||||
{
|
||||
// Notify about updated texture ID & [Rect].
|
||||
final int handle = call.arguments['handle'];
|
||||
final Rect rect = Rect.fromLTWH(
|
||||
call.arguments['rect']['left'] * 1.0,
|
||||
call.arguments['rect']['top'] * 1.0,
|
||||
call.arguments['rect']['width'] * 1.0,
|
||||
call.arguments['rect']['height'] * 1.0,
|
||||
);
|
||||
final int id = call.arguments['id'];
|
||||
_controllers[handle]?.rect.value = rect;
|
||||
_controllers[handle]?.id.value = id;
|
||||
// Notify about the first frame being rendered.
|
||||
if (rect.width > 0 && rect.height > 0) {
|
||||
final completer = _controllers[handle]
|
||||
?.waitUntilFirstFrameRenderedCompleter;
|
||||
if (!(completer?.isCompleted ?? true)) {
|
||||
completer?.complete();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (exception, stacktrace) {
|
||||
debugPrint(exception.toString());
|
||||
debugPrint(stacktrace.toString());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
||||
import 'package:media_kit_video/src/video_controller/platform_video_controller.dart';
|
||||
|
||||
// Stub declaration for avoiding compilation errors on Dart JS using conditional imports.
|
||||
|
||||
class NativeVideoController extends PlatformVideoController {
|
||||
static const bool supported = false;
|
||||
|
||||
NativeVideoController._(
|
||||
super.player,
|
||||
super.configuration,
|
||||
);
|
||||
|
||||
static Future<PlatformVideoController> create(
|
||||
Player player,
|
||||
VideoControllerConfiguration configuration,
|
||||
) =>
|
||||
throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<void> setSize({int? width, int? height}) => throw UnimplementedError();
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'dart:async';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
||||
import 'package:media_kit_video/src/video_controller/video_controller.dart';
|
||||
|
||||
/// {@template platform_video_controller}
|
||||
///
|
||||
/// PlatformVideoController
|
||||
/// -----------------------
|
||||
///
|
||||
/// This class provides the interface for platform specific [VideoController] implementations.
|
||||
/// The platform specific implementations are expected to implement the methods accordingly.
|
||||
///
|
||||
/// The subclasses are then used in composition with the [VideoController] class, based on the platform the application is running on.
|
||||
///
|
||||
/// {@endtemplate}
|
||||
abstract class PlatformVideoController {
|
||||
/// The [Player] instance associated with this instance.
|
||||
final Player player;
|
||||
|
||||
/// User defined configuration for [VideoController].
|
||||
final VideoControllerConfiguration configuration;
|
||||
|
||||
/// Texture ID of the video output, registered with Flutter engine by the native implementation.
|
||||
final ValueNotifier<int?> id = ValueNotifier<int?>(null);
|
||||
|
||||
/// [Rect] of the video output, received from the native implementation.
|
||||
final ValueNotifier<Rect?> rect = ValueNotifier<Rect?>(null);
|
||||
|
||||
/// {@macro platform_video_controller}
|
||||
PlatformVideoController(
|
||||
this.player,
|
||||
this.configuration,
|
||||
);
|
||||
|
||||
/// Sets the required size of the video output.
|
||||
/// This may yield substantial performance improvements if a small [width] & [height] is specified.
|
||||
///
|
||||
/// Remember:
|
||||
/// * “Premature optimization is the root of all evil”
|
||||
/// * “With great power comes great responsibility”
|
||||
Future<void> setSize({
|
||||
int? width,
|
||||
int? height,
|
||||
});
|
||||
|
||||
/// A [Future] that completes when the first video frame has been rendered.
|
||||
Future<void> get waitUntilFirstFrameRendered =>
|
||||
waitUntilFirstFrameRenderedCompleter.future;
|
||||
|
||||
/// [Completer] used to signal the decoding & rendering of the first video frame.
|
||||
/// Use [waitUntilFirstFrameRendered] to wait for the first frame to be rendered.
|
||||
@protected
|
||||
final waitUntilFirstFrameRenderedCompleter = Completer<void>();
|
||||
|
||||
void dispose() {
|
||||
id.dispose();
|
||||
rect.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// {@template video_controller_configuration}
|
||||
///
|
||||
/// VideoControllerConfiguration
|
||||
/// ----------------------------
|
||||
/// Configurable options for customizing the [VideoController] behavior.
|
||||
///
|
||||
/// {@endtemplate}
|
||||
class VideoControllerConfiguration {
|
||||
/// Sets the [`--vo`](https://mpv.io/manual/stable/#options-vo) property on native backend.
|
||||
///
|
||||
/// Default: Platform specific.
|
||||
/// * Windows, GNU/Linux, macOS & iOS: `libmpv`
|
||||
/// * Android: `gpu`
|
||||
final String? vo;
|
||||
|
||||
/// Sets the [`--hwdec`](https://mpv.io/manual/stable/#options-hwdec) property on native backend.
|
||||
///
|
||||
/// Default: Platform specific.
|
||||
/// * Windows, GNU/Linux, macOS & iOS : `auto`
|
||||
/// * Android: `auto-safe`
|
||||
final String? hwdec;
|
||||
|
||||
/// The scale for the video output.
|
||||
/// This may be used for performance reasons. Specifying this option will cause [width] & [height] to be ignored.
|
||||
///
|
||||
/// Default: `1.0`
|
||||
final double scale;
|
||||
|
||||
/// The fixed width for the video output.
|
||||
/// This may be used for performance reasons.
|
||||
///
|
||||
/// Default: `null`
|
||||
final int? width;
|
||||
|
||||
/// The fixed height for the video output.
|
||||
/// This may be used for performance reasons.
|
||||
///
|
||||
/// Default: `null`
|
||||
final int? height;
|
||||
|
||||
/// Whether to enable hardware acceleration.
|
||||
///
|
||||
/// DO NOT DISABLE THIS OPTION MEANINGLESSLY.
|
||||
/// THE BATTERY WILL DRAIN, THE DEVICE MAY HEAT UP & CPU USAGE WILL BE HIGH.
|
||||
///
|
||||
/// Default: `true`
|
||||
final bool enableHardwareAcceleration;
|
||||
|
||||
/// Whether to attach `android.view.Surface` after video parameters are known.
|
||||
///
|
||||
/// Default:
|
||||
/// * [vo] == gpu : `true`
|
||||
/// * [vo] != gpu : `false`
|
||||
final bool? androidAttachSurfaceAfterVideoParameters;
|
||||
|
||||
/// {@macro video_controller_configuration}
|
||||
const VideoControllerConfiguration({
|
||||
this.vo,
|
||||
this.hwdec,
|
||||
this.width,
|
||||
this.height,
|
||||
this.scale = 1.0,
|
||||
this.enableHardwareAcceleration = true,
|
||||
this.androidAttachSurfaceAfterVideoParameters,
|
||||
});
|
||||
|
||||
/// Returns a copy of this class with the given fields replaced by the new values.
|
||||
VideoControllerConfiguration copyWith({
|
||||
String? vo,
|
||||
String? hwdec,
|
||||
double? scale,
|
||||
int? width,
|
||||
int? height,
|
||||
bool? enableHardwareAcceleration,
|
||||
bool? androidAttachSurfaceAfterVideoParameters,
|
||||
}) =>
|
||||
VideoControllerConfiguration(
|
||||
vo: vo ?? this.vo,
|
||||
hwdec: hwdec ?? this.hwdec,
|
||||
scale: scale ?? this.scale,
|
||||
width: width ?? this.width,
|
||||
height: height ?? this.height,
|
||||
enableHardwareAcceleration:
|
||||
enableHardwareAcceleration ?? this.enableHardwareAcceleration,
|
||||
androidAttachSurfaceAfterVideoParameters:
|
||||
androidAttachSurfaceAfterVideoParameters ??
|
||||
this.androidAttachSurfaceAfterVideoParameters,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'dart:async';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
||||
import 'package:media_kit_video/src/video_controller/platform_video_controller.dart';
|
||||
|
||||
import 'package:media_kit_video/src/video_controller/native_video_controller/native_video_controller.dart';
|
||||
import 'package:media_kit_video/src/video_controller/android_video_controller/android_video_controller.dart';
|
||||
import 'package:media_kit_video/src/video_controller/web_video_controller/web_video_controller.dart';
|
||||
|
||||
/// {@template video_controller}
|
||||
///
|
||||
/// VideoController
|
||||
/// ---------------
|
||||
///
|
||||
/// [VideoController] is used to initialize & display video output.
|
||||
/// It takes reference to existing [Player] instance from `package:media_kit`.
|
||||
///
|
||||
/// Passing [VideoController] to [Video] widget will cause the video output to be displayed.
|
||||
///
|
||||
/// ```dart
|
||||
/// late final player = Player();
|
||||
/// late final controller = VideoController(player);
|
||||
/// ```
|
||||
///
|
||||
/// **Configurable options:**
|
||||
///
|
||||
/// 1. You can limit size of the video output by specifying [VideoControllerConfiguration.width] & [VideoControllerConfiguration.height].
|
||||
/// * A smaller width & height may yield substantial performance improvements.
|
||||
/// * By default, both [VideoControllerConfiguration.height] & [VideoControllerConfiguration.width] are `null` i.e. output is based on video's resolution.
|
||||
/// 2. You can reduce scale of the video output by specifying [VideoControllerConfiguration.scale].
|
||||
/// * A smaller scale may yield substantial performance improvements. Specifying this value will cause [VideoControllerConfiguration.width] & [VideoControllerConfiguration.height] to be ignored.
|
||||
/// * By default, [VideoControllerConfiguration.scale] is `1.0` i.e. output is based on video's resolution.
|
||||
/// 3. You can switch between GPU & CPU rendering by specifying [VideoControllerConfiguration.enableHardwareAcceleration].
|
||||
/// * Disabling the option may improve stability on certain devices.
|
||||
/// * By default, [VideoControllerConfiguration.enableHardwareAcceleration] is `true` i.e. GPU (Direct3D/OpenGL/METAL) is utilized.
|
||||
///
|
||||
/// **Platform specific limitations & differences:**
|
||||
///
|
||||
/// **Android**
|
||||
/// * [VideoControllerConfiguration.width] & [VideoControllerConfiguration.height] arguments have no effect.
|
||||
///
|
||||
/// **Web**
|
||||
/// * [VideoControllerConfiguration.width] & [VideoControllerConfiguration.height] arguments have no effect.
|
||||
/// * Only single [Video] output can be displayed for a [VideoController].
|
||||
/// Displaying multiple [Video] widgets with same [VideoController] will cause only last mounted [Video] to be displayed.
|
||||
/// * [VideoControllerConfiguration.enableHardwareAcceleration] is ignored i.e. GPU usage is dependent upon the web browser.
|
||||
///
|
||||
/// {@endtemplate}
|
||||
class VideoController {
|
||||
/// The [Player] instance associated with this [VideoController].
|
||||
final Player player;
|
||||
|
||||
/// Platform specific internal implementation initialized depending upon the current platform.
|
||||
final platform = Completer<PlatformVideoController>();
|
||||
|
||||
/// Platform specific internal implementation initialized depending upon the current platform.
|
||||
final notifier = ValueNotifier<PlatformVideoController?>(null);
|
||||
|
||||
/// Texture ID of the video output, registered with Flutter engine by the native implementation.
|
||||
final ValueNotifier<int?> id = ValueNotifier<int?>(null);
|
||||
|
||||
/// [Rect] of the video output, received from the native implementation.
|
||||
final ValueNotifier<Rect?> rect = ValueNotifier<Rect?>(null);
|
||||
|
||||
/// {@macro video_controller}
|
||||
VideoController(
|
||||
this.player, {
|
||||
VideoControllerConfiguration configuration =
|
||||
const VideoControllerConfiguration(),
|
||||
}) {
|
||||
player.platform?.isVideoControllerAttached = true;
|
||||
|
||||
() async {
|
||||
final completer = Completer();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => completer.complete());
|
||||
await completer.future;
|
||||
|
||||
try {
|
||||
if (NativeVideoController.supported) {
|
||||
final result = await NativeVideoController.create(
|
||||
player,
|
||||
configuration,
|
||||
);
|
||||
platform.complete(result);
|
||||
notifier.value = result;
|
||||
} else if (AndroidVideoController.supported) {
|
||||
final result = await AndroidVideoController.create(
|
||||
player,
|
||||
configuration,
|
||||
);
|
||||
platform.complete(result);
|
||||
notifier.value = result;
|
||||
} else if (WebVideoController.supported) {
|
||||
final result = await WebVideoController.create(
|
||||
player,
|
||||
configuration,
|
||||
);
|
||||
platform.complete(result);
|
||||
notifier.value = result;
|
||||
}
|
||||
|
||||
if (platform.isCompleted) {
|
||||
// Populate [id] & [rect] [ValueNotifier]s with the values from [platform] implementation of [PlatformVideoController].
|
||||
final controller = await platform.future;
|
||||
// Add listeners.
|
||||
void fn0() => id.value = controller.id.value;
|
||||
void fn1() => rect.value = controller.rect.value;
|
||||
fn0();
|
||||
fn1();
|
||||
controller.id.addListener(fn0);
|
||||
controller.rect.addListener(fn1);
|
||||
// Remove listeners upon [Player.dispose].
|
||||
player.platform?.release.add(() async {
|
||||
controller.id.removeListener(fn0);
|
||||
controller.rect.removeListener(fn1);
|
||||
});
|
||||
} else {
|
||||
platform.completeError(
|
||||
UnimplementedError(
|
||||
'[VideoController] is unavailable for this platform.',
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (exception, stacktrace) {
|
||||
platform.completeError(exception);
|
||||
debugPrint(exception.toString());
|
||||
debugPrint(stacktrace.toString());
|
||||
}
|
||||
|
||||
if (!(player.platform?.videoControllerCompleter.isCompleted ?? true)) {
|
||||
player.platform?.videoControllerCompleter.complete();
|
||||
}
|
||||
}();
|
||||
}
|
||||
|
||||
/// Sets the required size of the video output.
|
||||
/// This may yield substantial performance improvements if a small [width] & [height] is specified.
|
||||
///
|
||||
/// Remember:
|
||||
/// * “Premature optimization is the root of all evil”
|
||||
/// * “With great power comes great responsibility”
|
||||
Future<void> setSize({
|
||||
int? width,
|
||||
int? height,
|
||||
}) async {
|
||||
final instance = await platform.future;
|
||||
return instance.setSize(
|
||||
width: width,
|
||||
height: height,
|
||||
);
|
||||
}
|
||||
|
||||
/// A [Future] that completes when the first video frame has been rendered.
|
||||
Future<void> get waitUntilFirstFrameRendered async {
|
||||
final instance = await platform.future;
|
||||
return instance.waitUntilFirstFrameRendered;
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
// ignore_for_file: avoid_web_libraries_in_flutter
|
||||
import 'dart:async';
|
||||
import 'dart:js_interop';
|
||||
import 'dart:js_interop_unsafe';
|
||||
import 'package:web/web.dart' as web;
|
||||
|
||||
import 'dart:ui_web';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
||||
import 'package:media_kit_video/src/video_controller/platform_video_controller.dart';
|
||||
|
||||
/// {@template web_video_controller}
|
||||
///
|
||||
/// WebVideoController
|
||||
/// ------------------
|
||||
///
|
||||
/// The [WebVideoController] implementation based on HTML & JavaScript used on web.
|
||||
///
|
||||
/// {@endtemplate}
|
||||
class WebVideoController extends PlatformVideoController {
|
||||
/// Whether [WebVideoController] is supported on the current platform or not.
|
||||
static const bool supported = kIsWeb;
|
||||
|
||||
/// {@macro web_video_controller}
|
||||
WebVideoController._(
|
||||
super.player,
|
||||
super.configuration,
|
||||
);
|
||||
|
||||
/// {@macro web_video_controller}
|
||||
static Future<PlatformVideoController> create(
|
||||
Player player,
|
||||
VideoControllerConfiguration configuration,
|
||||
) async {
|
||||
// Retrieve the native handle of the [Player].
|
||||
final handle = await player.handle;
|
||||
|
||||
final controller = WebVideoController._(
|
||||
player,
|
||||
configuration,
|
||||
);
|
||||
// Register [_dispose] for execution upon [Player.dispose].
|
||||
player.platform?.release.add(controller._dispose);
|
||||
|
||||
// Retrieve the [html.VideoElement] instance from [js.context].
|
||||
final instances = globalContext.getProperty(_kInstances.toJS) as JSObject;
|
||||
final element = instances.getProperty(handle.toJS) as web.HTMLVideoElement;
|
||||
controller._element = element;
|
||||
// Register the [html.VideoElement] as platform view.
|
||||
platformViewRegistry.registerViewFactory(
|
||||
'com.alexmercerind.media_kit_video.$handle',
|
||||
(int _) => controller._element!,
|
||||
);
|
||||
|
||||
// On web implementation, we are having [handle] & [controller.id] same, which in itself is a simple counter based value managed within [Player].
|
||||
// Since there is no texture creation or rendering involved.
|
||||
controller.id.value = handle;
|
||||
|
||||
// Listen to the resize event of the [html.VideoElement].
|
||||
controller._resizeStreamSubscription = controller._element?.onResize.listen(
|
||||
(event) {
|
||||
debugPrint(
|
||||
'media_kit: WebVideoController: ${controller._element?.videoWidth}, ${controller._element?.videoHeight}',
|
||||
);
|
||||
// Update the size of the [PlatformVideoController].
|
||||
controller.rect.value = Rect.fromLTWH(
|
||||
0.0,
|
||||
0.0,
|
||||
controller._element?.videoWidth.toDouble() ?? 0.0,
|
||||
controller._element?.videoHeight.toDouble() ?? 0.0,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// Notify about the first frame being rendered.
|
||||
// Case: If some [Media] is already playing when [VideoController] is created.
|
||||
if (player.state.width != null && player.state.height != null) {
|
||||
if (!controller.waitUntilFirstFrameRenderedCompleter.isCompleted) {
|
||||
controller.waitUntilFirstFrameRenderedCompleter.complete();
|
||||
}
|
||||
}
|
||||
|
||||
controller._element?.onCanPlayThrough.listen(
|
||||
(_) {
|
||||
// Notify about first frame being rendered.
|
||||
if (!controller.waitUntilFirstFrameRenderedCompleter.isCompleted) {
|
||||
controller.waitUntilFirstFrameRenderedCompleter.complete();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Return the [PlatformVideoController].
|
||||
return controller;
|
||||
}
|
||||
|
||||
/// Sets the required size of the video output.
|
||||
/// This may yield substantial performance improvements if a small [width] & [height] is specified.
|
||||
///
|
||||
/// Remember:
|
||||
/// * “Premature optimization is the root of all evil”
|
||||
/// * “With great power comes great responsibility”
|
||||
@override
|
||||
Future<void> setSize({
|
||||
int? width,
|
||||
int? height,
|
||||
}) {
|
||||
throw UnsupportedError(
|
||||
'[AndroidVideoController.setSize] is not available on web',
|
||||
);
|
||||
}
|
||||
|
||||
/// Disposes the instance. Releases allocated resources back to the system.
|
||||
Future<void> _dispose() async {
|
||||
super.dispose();
|
||||
await _resizeStreamSubscription?.cancel();
|
||||
}
|
||||
|
||||
/// HTML [html.HTMLVideoElement] instance reference.
|
||||
web.HTMLVideoElement? _element;
|
||||
|
||||
StreamSubscription<web.Event>? _resizeStreamSubscription;
|
||||
|
||||
/// JavaScript object attribute used to store various [VideoElement] instances in [js.context].
|
||||
static const _kInstances = '\$com.alexmercerind.media_kit.instances';
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
||||
import 'package:media_kit_video/src/video_controller/platform_video_controller.dart';
|
||||
|
||||
// Stub declaration for avoiding compilation errors on Dart Native using conditional imports.
|
||||
|
||||
class WebVideoController extends PlatformVideoController {
|
||||
static const bool supported = false;
|
||||
|
||||
WebVideoController._(
|
||||
super.player,
|
||||
super.configuration,
|
||||
);
|
||||
|
||||
static Future<PlatformVideoController> create(
|
||||
Player player,
|
||||
VideoControllerConfiguration configuration,
|
||||
) =>
|
||||
throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<void> setSize({int? width, int? height}) => throw UnimplementedError();
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
export 'stub.dart' if (dart.library.js_interop) 'real.dart';
|
||||
@@ -0,0 +1,104 @@
|
||||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2023 & onwards, Abdelaziz Mahdy <abdelaziz.h.mahdy@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'dart:async';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:media_kit_video/src/video/video.dart';
|
||||
import 'package:media_kit_video/src/subtitle/subtitle_view.dart';
|
||||
|
||||
/// {@template video_view_parameters}
|
||||
///
|
||||
/// VideoViewParameters
|
||||
/// -------------------
|
||||
///
|
||||
/// The attributes of a [Video] widget composed into a single class.
|
||||
///
|
||||
/// {@endtemplate}
|
||||
class VideoViewParameters {
|
||||
final double? width;
|
||||
final double? height;
|
||||
final BoxFit fit;
|
||||
final Color fill;
|
||||
final Alignment alignment;
|
||||
final double? aspectRatio;
|
||||
final FilterQuality filterQuality;
|
||||
final /* VideoControlsBuilder? */ dynamic controls;
|
||||
final SubtitleViewConfiguration subtitleViewConfiguration;
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// {@macro video_view_parameters}
|
||||
VideoViewParameters({
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.fit,
|
||||
required this.fill,
|
||||
required this.alignment,
|
||||
required this.aspectRatio,
|
||||
required this.filterQuality,
|
||||
required this.controls,
|
||||
required this.subtitleViewConfiguration,
|
||||
this.focusNode,
|
||||
});
|
||||
|
||||
VideoViewParameters copyWith({
|
||||
double? width,
|
||||
double? height,
|
||||
BoxFit? fit,
|
||||
Color? fill,
|
||||
Alignment? alignment,
|
||||
double? aspectRatio,
|
||||
FilterQuality? filterQuality,
|
||||
/* VideoControlsBuilder? */ dynamic controls,
|
||||
bool? pauseUponEnteringBackgroundMode,
|
||||
bool? resumeUponEnteringForegroundMode,
|
||||
SubtitleViewConfiguration? subtitleViewConfiguration,
|
||||
Future<void> Function()? onEnterFullscreen,
|
||||
Future<void> Function()? onExitFullscreen,
|
||||
FocusNode? focusNode,
|
||||
}) {
|
||||
return VideoViewParameters(
|
||||
width: width ?? this.width,
|
||||
height: height ?? this.height,
|
||||
fit: fit ?? this.fit,
|
||||
fill: fill ?? this.fill,
|
||||
alignment: alignment ?? this.alignment,
|
||||
aspectRatio: aspectRatio ?? this.aspectRatio,
|
||||
filterQuality: filterQuality ?? this.filterQuality,
|
||||
controls: controls ?? this.controls,
|
||||
subtitleViewConfiguration:
|
||||
subtitleViewConfiguration ?? this.subtitleViewConfiguration,
|
||||
focusNode: focusNode ?? this.focusNode,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is VideoViewParameters &&
|
||||
other.width == width &&
|
||||
other.height == height &&
|
||||
other.fit == fit &&
|
||||
other.fill == fill &&
|
||||
other.alignment == alignment &&
|
||||
other.aspectRatio == aspectRatio &&
|
||||
other.filterQuality == filterQuality &&
|
||||
other.controls == controls &&
|
||||
other.subtitleViewConfiguration == subtitleViewConfiguration &&
|
||||
other.focusNode == focusNode;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
width.hashCode ^
|
||||
height.hashCode ^
|
||||
fit.hashCode ^
|
||||
fill.hashCode ^
|
||||
alignment.hashCode ^
|
||||
aspectRatio.hashCode ^
|
||||
filterQuality.hashCode ^
|
||||
controls.hashCode ^
|
||||
subtitleViewConfiguration.hashCode ^
|
||||
focusNode.hashCode;
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
+34
@@ -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_
|
||||
+75
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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, ¶ms);
|
||||
|
||||
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(¶ms);
|
||||
}
|
||||
|
||||
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, ¶ms);
|
||||
|
||||
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(¶ms);
|
||||
}
|
||||
|
||||
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, ¶ms)
|
||||
)
|
||||
|
||||
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, ¶ms)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
+55
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+168
@@ -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
Reference in new issue
Block a user