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:
|
media_kit_video:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: media_kit_video
|
path: "../../third_party/media_kit_video"
|
||||||
sha256: afaa509e7b7e0bf247557a3a740cde903a52c34ace9810f94500e127bd7b043d
|
relative: true
|
||||||
url: "https://pub.dev"
|
source: path
|
||||||
source: hosted
|
|
||||||
version: "2.0.1"
|
version: "2.0.1"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
signals: ^7.0.0
|
signals: ^7.0.0
|
||||||
media_kit: ^1.1.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
|
media_kit_libs_video: ^1.0.0
|
||||||
path_provider: ^2.1.0
|
path_provider: ^2.1.0
|
||||||
web_socket_channel: ^3.0.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` |
|
| `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 |
|
| `tool` | harnesses: `range_logger`, `shaper`, `fake_viewer`, `e2e_check` | dev only |
|
||||||
| `deploy` | systemd unit for the dedicated server | |
|
| `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
|
Guard: `packages/core/test/flutter_free_test.dart` fails if anything under
|
||||||
`core/lib` imports Flutter, `dart:ui` or `signals_flutter`; CI runs
|
`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
|
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).
|
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
|
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
|
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
|
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