From 963383b1215ec543742ded3a0f850c15574a757b Mon Sep 17 00:00:00 2001 From: aayush262 <99584765+aayush2622@users.noreply.github.com> Date: Thu, 16 Jul 2026 14:11:59 +0530 Subject: [PATCH 1/2] chore: Bump wakelock --- media_kit_video/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/media_kit_video/pubspec.yaml b/media_kit_video/pubspec.yaml index 75c3b13cc..ce7160a64 100644 --- a/media_kit_video/pubspec.yaml +++ b/media_kit_video/pubspec.yaml @@ -23,7 +23,7 @@ dependencies: path: ../media_kit synchronized: ^3.1.0 - wakelock_plus: 1.3.3 + wakelock_plus: ^1.6.1 wakelock_plus_ohos: ^0.0.3 universal_platform: ^1.0.0+1 plugin_platform_interface: ^2.0.2 From f5709954fd769f77207e4f4c43c63fa68b03ceef Mon Sep 17 00:00:00 2001 From: aayush262 <99584765+aayush2622@users.noreply.github.com> Date: Thu, 13 Aug 2026 00:32:37 +0530 Subject: [PATCH 2/2] Merge pull request #1 from Predidit/main f --- .../android/build.gradle | 8 +- .../ios/media_kit_libs_ios_video/ios/Makefile | 4 +- .../media_kit_libs_ios_video/Package.swift | 38 +- .../media_kit_libs_macos_video/macos/Makefile | 4 +- .../media_kit_libs_macos_video/Package.swift | 38 +- .../ohos/src/main/cpp/CMakeLists.txt | 4 +- .../windows/CMakeLists.txt | 10 +- .../player/native/core/native_library.dart | 3 +- .../lib/src/player/native/player/real.dart | 6 +- media_kit/test/src/player/player_test.dart | 23 ++ .../ohos_video_controller/real.dart | 101 ++--- media_kit_video/linux/CMakeLists.txt | 16 +- media_kit_video/linux/gl_render_thread.cc | 4 - .../media_kit_video/gl_render_thread.h | 5 +- .../include/media_kit_video/texture_gl.h | 23 +- .../include/media_kit_video/video_output.h | 44 +- .../media_kit_video/video_output_manager.h | 38 +- .../linux/media_kit_video_plugin.cc | 7 +- media_kit_video/linux/texture_gl.cc | 389 +++++++++--------- media_kit_video/linux/utils.cc | 15 +- media_kit_video/linux/video_output.cc | 265 +++++------- media_kit_video/linux/video_output_manager.cc | 11 +- .../media_kit_video/MediaKitVideoPlugin.ets | 35 +- .../media_kit_video/TextureUpdateCallback.ets | 11 - .../media_kit_video/VideoOutput.ets | 54 +-- .../media_kit_video/VideoOutputManager.ets | 23 +- 26 files changed, 520 insertions(+), 659 deletions(-) delete mode 100644 media_kit_video/ohos/src/main/ets/com/alexmercerind/media_kit_video/TextureUpdateCallback.ets diff --git a/libs/android/media_kit_libs_android_video/android/build.gradle b/libs/android/media_kit_libs_android_video/android/build.gradle index 3359bddcf..9dacc5bb1 100644 --- a/libs/android/media_kit_libs_android_video/android/build.gradle +++ b/libs/android/media_kit_libs_android_video/android/build.gradle @@ -58,10 +58,10 @@ task downloadDependencies(type: Exec) { // Download all *.jar dependencies & verify their SHA-256 checksums. def filesToDownload = [ - ["url": "https://github.com/Predidit/libmpv-android-video-build/releases/download/v1.2.6/default-arm64-v8a.jar", "sha256": "1f6b8f971be63bb4bd4d5fd64b1eed1be75ada591168ba13b5647fbf845c2414", "destination": file("$buildDir/v1.2.6/default-arm64-v8a.jar")], - ["url": "https://github.com/Predidit/libmpv-android-video-build/releases/download/v1.2.6/default-armeabi-v7a.jar", "sha256": "b539cc8684fb3e4a0be7a6cce75272e2b7c571fc14c9174786bb17fb338cfee2", "destination": file("$buildDir/v1.2.6/default-armeabi-v7a.jar")], - ["url": "https://github.com/Predidit/libmpv-android-video-build/releases/download/v1.2.6/default-x86_64.jar", "sha256": "25a3cc1331326d41efb13a8da959eb14104eabe1b8a318e30c0f703444dcaf7c", "destination": file("$buildDir/v1.2.6/default-x86_64.jar")], - ["url": "https://github.com/Predidit/libmpv-android-video-build/releases/download/v1.2.6/default-x86.jar", "sha256": "847120273bba862749143e58b22f625d229bb73b137ef9b38aa05f097a4509e8", "destination": file("$buildDir/v1.2.6/default-x86.jar")] + ["url": "https://github.com/Predidit/libmpv-android-video-build/releases/download/v1.2.7/default-arm64-v8a.jar", "sha256": "13e882d96b8cd235425172b022e4a94dfcae5f07985dff85c8d648e7369fa2d1", "destination": file("$buildDir/v1.2.7/default-arm64-v8a.jar")], + ["url": "https://github.com/Predidit/libmpv-android-video-build/releases/download/v1.2.7/default-armeabi-v7a.jar", "sha256": "7f522ed762ea6dfeba93a02e3837c5538790030b9965a03ed3a00276adc7b32c", "destination": file("$buildDir/v1.2.7/default-armeabi-v7a.jar")], + ["url": "https://github.com/Predidit/libmpv-android-video-build/releases/download/v1.2.7/default-x86_64.jar", "sha256": "aed0fffc99e5e554d48e1af90bc700133c25fbc02615bf1bf17db9299365c481", "destination": file("$buildDir/v1.2.7/default-x86_64.jar")], + ["url": "https://github.com/Predidit/libmpv-android-video-build/releases/download/v1.2.7/default-x86.jar", "sha256": "9269643264a1c9689116467f313d5e1b23ea56a68d338ab940c5e8fcf07061c6", "destination": file("$buildDir/v1.2.7/default-x86.jar")] ] filesToDownload.each { fileInfo -> diff --git a/libs/ios/media_kit_libs_ios_video/ios/Makefile b/libs/ios/media_kit_libs_ios_video/ios/Makefile index afd8f2922..085ba1ab7 100644 --- a/libs/ios/media_kit_libs_ios_video/ios/Makefile +++ b/libs/ios/media_kit_libs_ios_video/ios/Makefile @@ -1,7 +1,7 @@ all: Frameworks/*.xcframework Frameworks/.symlinks -MPV_XCFRAMEWORKS_VERSION=0.6.6 -MPV_XCFRAMEWORKS_SHA256SUM=8fdd935bbf923bd01633bef19efc6a4ff534871f2f39f0541f78c6e8a5343fc6 +MPV_XCFRAMEWORKS_VERSION=0.6.8 +MPV_XCFRAMEWORKS_SHA256SUM=d428641cc6c100de8234eae04e229966e46949eb73fcabd526a008a02e9bf968 TAR=tar TAR_WILDCARDS_FLAG := $(shell ${TAR} --version 2>&1 | grep -q 'GNU' && echo "--wildcards" || echo "") diff --git a/libs/ios/media_kit_libs_ios_video/ios/media_kit_libs_ios_video/Package.swift b/libs/ios/media_kit_libs_ios_video/ios/media_kit_libs_ios_video/Package.swift index 8cdc92f9e..2b74ad25e 100644 --- a/libs/ios/media_kit_libs_ios_video/ios/media_kit_libs_ios_video/Package.swift +++ b/libs/ios/media_kit_libs_ios_video/ios/media_kit_libs_ios_video/Package.swift @@ -24,26 +24,26 @@ let libmpvTargets = [ "Xml2" ] -let libmpvArtifactBase = "https://github.com/Predidit/libmpv-darwin-build/releases/download/0.6.7/libmpv-xcframeworks_0.6.7_ios-universal-video-default" +let libmpvArtifactBase = "https://github.com/Predidit/libmpv-darwin-build/releases/download/0.6.8/libmpv-xcframeworks_0.6.8_ios-universal-video-default" let libmpvChecksums = [ - "Ass": "8607605bded29730fbd6c20d1b6988444d330ed7e4b1142b10e7cfaa55f772ec", - "Avcodec": "a79367de7ebb92b9cc45ada63edc1a218e0299db967dd9a248d4c5dd19ea6191", - "Avfilter": "2775ca25af51d177ec3a25491f549c50f2b6a18950e85af5cf0520bd18097895", - "Avformat": "3f7f23642737819c0a33b8d9f3e3106ab7c2089bfd6946d8f8ebd8132747a6b1", - "Avutil": "f6f7b7ecf8f31eead7396b098e915f6accf4c1b8eb230c6bbb08e77d6bee9beb", - "Dav1d": "0d4bc8138648d5c47291b5e0ae65110e37e9989d7dd5652e6f5da42b3341ec1c", - "Freetype": "b7ee3376aff8c4a98f2cb5490db06bfdf4541d3984a6ae5ed7be0dcf8c9fda81", - "Fribidi": "6dbb29733d4832b2853acfb61390c7e2756f2597ebe37e0f2cc4f67008cd57a9", - "Harfbuzz": "f6a463b5a33368d4f84da09ba1cf0cf6be8a7120a74746f522a4114ea7187610", - "Mbedcrypto": "d1693d8053b8025b16e6eb246991afd81139bba4187d29aaee9be8dafa4aa2ec", - "Mbedtls": "48739aa2fe45d234b51a0f019ea69623e15b2006463a90c7b023cfb3497082a7", - "Mbedx509": "7c9e490743d56c7b6303e5a069afc3878ed797fab5c73b7055f29dd637b03586", - "Mpv": "11ab22f45a46b4baa69f87c657d574d4f6c4845daf097fa21f3b82b75770c3e5", - "Png16": "1692ae51f6c148d80a1c244e631a6f1495c18e80d397139db16b47e2021b6fc6", - "Swresample": "8bfae5718a74b79dd64956649c92102d19c9ffc2597657607d6138d51f8a23a9", - "Swscale": "5551a0421843efb55da8a520be240cc9f3a6f46c93572de4f0e7345c91c4171f", - "Uchardet": "74dca5c489a12f324a7f6b087090670eef9364fec71886f8551a6fd434f435bb", - "Xml2": "f4cf4ad858d64a291505d7d23d5218586849547541b3694ef979390117f0e1db" + "Ass": "96751f9b3536a2dc031f4abefc3f2e52b1440ade80a969446b024abb1dafe8bf", + "Avcodec": "e8067adfbbf9108c9ca70b3b2ceee496e82e3037181c8b65473cf92ec0a6883a", + "Avfilter": "82ea8041f831214eadb5aac8c40be8829a6af1c534287efdabecfe63f38120ab", + "Avformat": "7140cd2595e46d9e8fe4a4a8f0bc15982ea5b328733f844c66ff3a4f84237b6b", + "Avutil": "ed7abc0497fdb4eedd18b376a7889fd5e6d26588b1b6beda584c5b4be2afbe07", + "Dav1d": "78038b49a493d71a5a0a20ea98c4875340276c88d3295349c92cfc93d8ca8e22", + "Freetype": "aa2efdfd84d6ecf40e2db9730914e03242a48dbda4f38a6a3721dff032a74a13", + "Fribidi": "92d15100178db3258d860390c852b9d910bc8723f09db5d5078050b1a8a5d732", + "Harfbuzz": "b9bd9c8a2d7dec742ba74b639441fc5b6ccc37024907c70b2eccda3f6f8517da", + "Mbedcrypto": "c130c48401361b2ac3d404bc1afe721c4832f16dcbbaa5c65644e294843a9eb1", + "Mbedtls": "c13f8649684ec7d7997c6638acc4d310a754db96845e7c1078e6cdf20b6b72d3", + "Mbedx509": "5fb5214fe3fff44a159c64dd33c0087cac9f4d934c37e136a72f99646b348d2e", + "Mpv": "443ae3d64e13c68cac18b7c611e82959f030d3d622709df4540c0bee9282dc8b", + "Png16": "0106717d37931e1dcc312c98db47a10afb47b4eeb93d14ef6c3e497aa6797a23", + "Swresample": "6c753d91bb4caa717b070d015aba700d86f943b910707b1f9fed50437cfb96aa", + "Swscale": "75ffa8ecd9e75aac37e9181dbeaccd68883996cef869f2b7a2dec3aca47a22fc", + "Uchardet": "b9d133c2c41dc5399cba002d6c3d2b8dbc04b643309f4b291dd0c123d84fd4f0", + "Xml2": "642635c6172c55c622948cbb194ae61381822d5a2526238b21bc8204c04d05db" ] let libmpvProductTargets: [String] = ["media_kit_libs_ios_video"] + libmpvTargets diff --git a/libs/macos/media_kit_libs_macos_video/macos/Makefile b/libs/macos/media_kit_libs_macos_video/macos/Makefile index f8b99cd10..13ac4f4c7 100644 --- a/libs/macos/media_kit_libs_macos_video/macos/Makefile +++ b/libs/macos/media_kit_libs_macos_video/macos/Makefile @@ -1,7 +1,7 @@ all: Frameworks/*.xcframework Frameworks/.symlinks -MPV_XCFRAMEWORKS_VERSION=0.6.6 -MPV_XCFRAMEWORKS_SHA256SUM=b0fbdf27bad29f2aba07c14896e137d46b9529638692c48777be0c110f9617cd +MPV_XCFRAMEWORKS_VERSION=0.6.8 +MPV_XCFRAMEWORKS_SHA256SUM=c396976a267eaaa64bc603f3cc1a2ee02d27c7d5d57935d2458f364afdf0f3cb TAR=tar TAR_WILDCARDS_FLAG := $(shell ${TAR} --version 2>&1 | grep -q 'GNU' && echo "--wildcards" || echo "") diff --git a/libs/macos/media_kit_libs_macos_video/macos/media_kit_libs_macos_video/Package.swift b/libs/macos/media_kit_libs_macos_video/macos/media_kit_libs_macos_video/Package.swift index 104fb6893..d3c241417 100644 --- a/libs/macos/media_kit_libs_macos_video/macos/media_kit_libs_macos_video/Package.swift +++ b/libs/macos/media_kit_libs_macos_video/macos/media_kit_libs_macos_video/Package.swift @@ -24,26 +24,26 @@ let libmpvTargets = [ "Xml2" ] -let libmpvArtifactBase = "https://github.com/Predidit/libmpv-darwin-build/releases/download/0.6.7/libmpv-xcframeworks_0.6.7_macos-universal-video-default" +let libmpvArtifactBase = "https://github.com/Predidit/libmpv-darwin-build/releases/download/0.6.8/libmpv-xcframeworks_0.6.8_macos-universal-video-default" let libmpvChecksums = [ - "Ass": "d513ff96f1d43b56ec9ef1334812d83dee51c3ea81b71aa26de2105d6876556c", - "Avcodec": "f56c4a92108312df069e77bfe2e672842832e3a2e9526a16710519ea8403662f", - "Avfilter": "cb5a590ae4b2be70dcc80209b760290233b603d9cce968fe0628c40d704309c1", - "Avformat": "25ea68a6e9fd5b4619ff00b1d967648ee139f047aa62a88042d320afede2ddbc", - "Avutil": "50e2c8ae79f2baed26951ab1f93386dde7d6f245357d65e0cac8a49200300a5d", - "Dav1d": "e2ca965b149ba85a67502234b40a68a2335a36e76204360be2f40c4d3e29027d", - "Freetype": "b33dda0eb19eb61bf2c1cab6ee8598fc59e4be5472a6d4730cf5b546b50e019b", - "Fribidi": "24b138976d8a315ae1e99fb563454c01727d7d9c2593da6b7ae5975472d4f5ee", - "Harfbuzz": "98301d6f95d3f915e870677f8ccce8514d86a17863837437aefe92f204ea2731", - "Mbedcrypto": "a152c81bbf0bf6caba31309db1c9987ed081224641f98694e8780870a71c78a4", - "Mbedtls": "dac7eb987d97df86fb9400ca5e90e5c17cb86ec63f4d4b77fd552da25a4eb12b", - "Mbedx509": "8b13a78328574c64490d6addd09136492e4fcf284466ab17d496f26b27d773a4", - "Mpv": "5be94d380a12ce56d1016d48b46f4cac663178bd010c42389e6c9ec162ee4278", - "Png16": "7b723cf98825386ec7588c25aacfeb8b25bb7335d1b0f7d2a1caee6950f872c3", - "Swresample": "01f757ac170fdcae5724e607827cd15a18d3205be91890e1d4fd28f17332d174", - "Swscale": "f3af7665670ec86912a82e7234de178bbfeb0a2967da204c09a8c64d85f0edc2", - "Uchardet": "30d1ac3cf0bf74adf99fd891c6b8213ffb8fa697576af743b715aa5193e84527", - "Xml2": "b76b6e63bf253be48c499c1f6c8f1a7b07b7814fe2015a8597367c83833cb161" + "Ass": "349cc791690222de522ccfc54a6cf6cf67221834a33f53e7beef947f6375e106", + "Avcodec": "a15bd6d4d30e63f9c6d194aad4988db1baf5504075c1774c70bd55bcc138eed3", + "Avfilter": "0f86101192d6a79e5f77350287010aec38bd116ff45599acf57f27e24377ba7f", + "Avformat": "c9050b13121ec7f8a7b4edd65e2c9d11563dadfb534581685878f6bbe2ef09c0", + "Avutil": "8d3c7e560c14e013718e614a516333bf1158d8fb0bd6781463bb6d7d556fa0f3", + "Dav1d": "b9336c02f641539fbe8012764ad002d3038ee632fbeacd7b4be23e18b5c282a1", + "Freetype": "ea4204874ad7993c4ff0a04c258768e70c0349bc722f335be5e2187b1750dbc3", + "Fribidi": "a42fcb59cb12a11469a6e201cab3ba60fb7c216455cb34ff30bb18ce5c1a8be9", + "Harfbuzz": "0e50fb9dedd625215725234c637cde497717dae4b7837e3a599ac6997e6a19a4", + "Mbedcrypto": "f0349d5180f1b84c2093e64ffa9fadce1d5076e0129ddbca3a4e71b800849467", + "Mbedtls": "2bceb6e9577be7eff37f2603420836f15590b01759f6b2cd5b0e39473f5d1f91", + "Mbedx509": "8bec77448cf12c5ad9a6faae8e063a1c1b420ec5adb71f29f4a5f8418bd8423b", + "Mpv": "9d3c36df1eede452a3cc8ce9fcdc6989fbb4e87649165f32a505e319c502984a", + "Png16": "db4a5ba9618c4342694431b83fdc7b2fcb705daf2f28377f28567c7d4d6653d9", + "Swresample": "b83a71df4e9e76098448863e01a3f0d77e7fd7fd78ad4d3720fd0c41401d31dc", + "Swscale": "0cd0e34e02048259cd2c4697f3879b01780927fc9ae373224450e5197ceaf85c", + "Uchardet": "cfa014837d33f03a0d96a0f7563e0f269201a7a56cdc415e61e584314388a1ae", + "Xml2": "d2af8a9a85cb061907127d26904b1060e4c22d9c1a91f0eaa8b294fe352c5e4c" ] let libmpvProductTargets: [String] = ["media_kit_libs_macos_video"] + libmpvTargets diff --git a/libs/ohos/media_kit_libs_ohos/ohos/src/main/cpp/CMakeLists.txt b/libs/ohos/media_kit_libs_ohos/ohos/src/main/cpp/CMakeLists.txt index 0a3645f62..894e09117 100644 --- a/libs/ohos/media_kit_libs_ohos/ohos/src/main/cpp/CMakeLists.txt +++ b/libs/ohos/media_kit_libs_ohos/ohos/src/main/cpp/CMakeLists.txt @@ -52,8 +52,8 @@ endfunction() set(LIBMPV "libmpv_aarch64.zip") # Download URL & SHA256 hash of the libmpv archive. -set(LIBMPV_URL "https://github.com/ErBWs/libmpv-ohos-build/releases/download/20260702/${LIBMPV}") -set(LIBMPV_SHA "e528917a9d2ba7c7d47182ddafa15c1dc025387ae0916ed2ad8e0a108017d48d") +set(LIBMPV_URL "https://github.com/ErBWs/libmpv-ohos-build/releases/download/20260811/${LIBMPV}") +set(LIBMPV_SHA "2bfb9844a7552c450694581b315b26cdf6202eaa57724e9f70f3facfad75163b") # Download location of the libmpv archive. set(LIBMPV_ARCHIVE "${CMAKE_CURRENT_SOURCE_DIR}/${LIBMPV}") diff --git a/libs/windows/media_kit_libs_windows_video/windows/CMakeLists.txt b/libs/windows/media_kit_libs_windows_video/windows/CMakeLists.txt index 3686124c9..41010ed42 100644 --- a/libs/windows/media_kit_libs_windows_video/windows/CMakeLists.txt +++ b/libs/windows/media_kit_libs_windows_video/windows/CMakeLists.txt @@ -68,15 +68,15 @@ string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" LIBMPV_TARGET_PROCESSOR) string(TOLOWER "${CMAKE_GENERATOR_PLATFORM}" LIBMPV_GENERATOR_PLATFORM) if(LIBMPV_TARGET_PROCESSOR MATCHES "^(arm64|aarch64)$" OR LIBMPV_GENERATOR_PLATFORM MATCHES "^(arm64|aarch64)$") - set(LIBMPV "mpv-dev-aarch64-20260623-git-ad59ff1.zip") - set(LIBMPV_SHA256 "381711758315b81d17df24fbb9dc4333b33b840c14fd674e264fb3306d21b7b0") + set(LIBMPV "mpv-dev-aarch64-20260811-git-ad59ff1.zip") + set(LIBMPV_SHA256 "1b0d58db5bc1b24437d63120ae6e1b0d887ec504e53523dc4666893d00e7f4e8") else() - set(LIBMPV "mpv-dev-x86_64-20260623-git-ad59ff1.7z") - set(LIBMPV_SHA256 "159d7379898e2e2490ab3e0dca233daa5b7389fcd32d4cb8c420399f44284399") + set(LIBMPV "mpv-dev-x86_64-20260811-git-ad59ff1.7z") + set(LIBMPV_SHA256 "0161ad026f9ebd418a9b660c8e8c569f7959b5bb8e5f4a63e462ffc73327d4d8") endif() # Download URL of the libmpv archive. -set(LIBMPV_URL "https://github.com/Predidit/libmpv-win32-video-cmake/releases/download/20260623/${LIBMPV}") +set(LIBMPV_URL "https://github.com/Predidit/libmpv-win32-video-cmake/releases/download/20260811/${LIBMPV}") # Download location of the libmpv archive. set(LIBMPV_ARCHIVE "${CMAKE_BINARY_DIR}/${LIBMPV}") diff --git a/media_kit/lib/src/player/native/core/native_library.dart b/media_kit/lib/src/player/native/core/native_library.dart index 106c6761e..b01393d6c 100644 --- a/media_kit/lib/src/player/native/core/native_library.dart +++ b/media_kit/lib/src/player/native/core/native_library.dart @@ -65,6 +65,7 @@ abstract class NativeLibrary { 'libmpv.so', ], 'ohos': [ + 'libmpv.so', 'libmpv.so.2', ], }[Platform.operatingSystem]; @@ -92,7 +93,7 @@ abstract class NativeLibrary { 'android': 'Cannot find libmpv.so. Please ensure it\'s presence in the APK.', 'ohos': - 'Cannot find libmpv.so.2. Please ensure it\'s presence in the HAP.', + 'Cannot find libmpv. Please ensure it\'s presence in the HAP.', }[Platform.operatingSystem]!, ); } diff --git a/media_kit/lib/src/player/native/player/real.dart b/media_kit/lib/src/player/native/player/real.dart index 5363cf541..31b96b45f 100644 --- a/media_kit/lib/src/player/native/player/real.dart +++ b/media_kit/lib/src/player/native/player/real.dart @@ -2328,6 +2328,11 @@ class NativePlayer extends PlatformPlayer { // Set --vid=no by default to prevent redundant video decoding. // [VideoController] internally sets --vid=auto upon attachment to enable video rendering & decoding. if (!test) 'vid': 'no', + // The OSC is a built-in Lua script and may be unavailable in libmpv + // builds compiled without Lua. Set it as an initialization option so + // supported builds disable it before scripts are loaded, while builds + // without the option can safely ignore it. + if (!configuration.osc) 'osc': 'no', }; if (Platform.isAndroid && @@ -2423,7 +2428,6 @@ class NativePlayer extends PlatformPlayer { properties.addAll( { if (!configuration.osc) ...{ - 'osc': 'no', 'osd-level': '0', }, 'title': configuration.title, diff --git a/media_kit/test/src/player/player_test.dart b/media_kit/test/src/player/player_test.dart index 30e711179..47a92bdc3 100644 --- a/media_kit/test/src/player/player_test.dart +++ b/media_kit/test/src/player/player_test.dart @@ -101,6 +101,29 @@ void main() { addTearDown(player.dispose); }, ); + test( + 'player-configuration-osc-disabled-does-not-log-property-not-found', + () async { + final player = Player( + configuration: const PlayerConfiguration(osc: false), + ); + var hasOscPropertyError = false; + final subscription = player.stream.log.listen((event) { + if (event.text.contains('_setProperty(osc')) { + hasOscPropertyError = true; + } + }); + + await player.platform?.waitForPlayerInitialization; + await Future.delayed(Duration.zero); + + expect(hasOscPropertyError, isFalse); + + await subscription.cancel(); + await player.dispose(); + }, + skip: UniversalPlatform.isWeb, + ); test( 'player-open-playable-media', () async { diff --git a/media_kit_video/lib/src/video_controller/ohos_video_controller/real.dart b/media_kit_video/lib/src/video_controller/ohos_video_controller/real.dart index 4bc7ceba3..6d44ac81a 100644 --- a/media_kit_video/lib/src/video_controller/ohos_video_controller/real.dart +++ b/media_kit_video/lib/src/video_controller/ohos_video_controller/real.dart @@ -44,18 +44,6 @@ class OhosVideoController extends PlatformVideoController { } } - /// Listener for updating the --wid property. - Future widListener() { - return lock.synchronized(() async { - final widValue = wid.value?.toString() ?? '0'; - await setProperties({'wid': widValue}); - // 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? videoParamsSubscription; @@ -64,7 +52,6 @@ class OhosVideoController extends PlatformVideoController { super.player, super.configuration, ) { - wid.addListener(widListener); videoParamsSubscription = player.stream.videoParams.listen( (event) => lock.synchronized(() async { final int width; @@ -157,26 +144,48 @@ class OhosVideoController extends PlatformVideoController { // Store the [VideoController] in the [_controllers]. _controllers[handle] = controller; - await _channel.invokeMethod( + final Map? data = await _channel.invokeMethod( 'VideoOutputManager.Create', { 'handle': handle.toString(), }, ); - await controller.setProperties( - { - 'vo': configuration.vo!, - 'hwdec': configuration.hwdec!, - 'vid': 'auto', - 'force-window': 'yes', - 'sub-use-margins': 'no', - 'sub-scale-with-window': 'no', - 'osd-font': 'HarmonyOS Sans SC', - }, + if (data == null) { + throw StateError('[OhosVideoController] failed to create video output.'); + } + + final id = (data['id'] as num).toInt(); + final wid = (data['wid'] as num).toInt(); + final rect = Rect.fromLTWH( + (data['rect']['left'] as num).toDouble(), + (data['rect']['top'] as num).toDouble(), + (data['rect']['width'] as num).toDouble(), + (data['rect']['height'] as num).toDouble(), ); - await controller.setProperties({'ohos-surface-size': '1x1'}); + controller.id.value = id; + controller.rect.value = rect; + controller.wid.value = wid; + + await controller.lock.synchronized(() async { + // MPV's HarmonyOS video output requires a valid surface ID before the + // GPU video output is initialized. + await controller.setProperty('vo', 'null'); + await controller.setProperties( + { + 'ohos-surface-size': '${rect.width.toInt()}x${rect.height.toInt()}', + 'wid': wid.toString(), + 'hwdec': configuration.hwdec!, + 'vid': 'auto', + 'force-window': 'yes', + 'sub-use-margins': 'no', + 'sub-scale-with-window': 'no', + 'osd-font': 'HarmonyOS Sans SC', + }, + ); + await controller.setProperty('vo', configuration.vo!); + }); // Return the [PlatformVideoController]. return controller; @@ -200,9 +209,6 @@ class OhosVideoController extends PlatformVideoController { /// Disposes the instance. Releases allocated resources back to the system. Future _dispose() async { - super.dispose(); - wid.dispose(); - wid.removeListener(widListener); await videoParamsSubscription?.cancel(); final handle = await player.handle; _controllers.remove(handle); @@ -212,46 +218,13 @@ class OhosVideoController extends PlatformVideoController { 'handle': handle.toString(), }, ); + wid.dispose(); + super.dispose(); } /// Currently created [OhosVideoController]s. static final _controllers = HashMap(); /// [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; - } - default: - { - break; - } - } - } catch (exception, stacktrace) { - debugPrint(exception.toString()); - debugPrint(stacktrace.toString()); - } - }, - ); + static const _channel = MethodChannel('com.alexmercerind/media_kit_video'); } diff --git a/media_kit_video/linux/CMakeLists.txt b/media_kit_video/linux/CMakeLists.txt index 9e2794331..f7a5be630 100644 --- a/media_kit_video/linux/CMakeLists.txt +++ b/media_kit_video/linux/CMakeLists.txt @@ -31,15 +31,15 @@ endif() message(STATUS "Detected architecture: ${ARCH_NAME}") if(ARCH_NAME STREQUAL "x86_64") - set(LIBMPV_ZIP_URL "https://github.com/Predidit/libmpv-linux-build/releases/download/20260124/libmpv_x86_64.zip") - set(LIBMPV_ZIP_SHA256 "f84dfe1b06cdc720fd5886728b99dc8dd65778d2407897bcd838d59de8c71b22") - set(LIBMPV_HEADER_ZIP_URL "https://github.com/Predidit/libmpv-linux-build/releases/download/20260124/libmpv_x86_64_header.zip") - set(LIBMPV_HEADER_ZIP_SHA256 "a721705009b8f058594b7d3ff6e9a9d876c1a67486bc231e1be215863940e3e4") + set(LIBMPV_ZIP_URL "https://github.com/Predidit/libmpv-linux-build/releases/download/20260810/libmpv_x86_64.zip") + set(LIBMPV_ZIP_SHA256 "45922100e5240bf69a72fa2ae140d5b54e1ed03993a8a3746dc798e3ae8ad6e4") + set(LIBMPV_HEADER_ZIP_URL "https://github.com/Predidit/libmpv-linux-build/releases/download/20260810/libmpv_x86_64_header.zip") + set(LIBMPV_HEADER_ZIP_SHA256 "c654ee0145167694c7ab55c02810d59b5e73e09c5404b32c0b037b53e42c164d") elseif(ARCH_NAME STREQUAL "aarch64") - set(LIBMPV_ZIP_URL "https://github.com/Predidit/libmpv-linux-build/releases/download/20260124/libmpv_aarch64.zip") - set(LIBMPV_ZIP_SHA256 "ca6429cbc5228d40c6a48c09c5f1e708cc3b5bf1d5058551d1d86e4bcb0aac5a") - set(LIBMPV_HEADER_ZIP_URL "https://github.com/Predidit/libmpv-linux-build/releases/download/20260124/libmpv_aarch64_header.zip") - set(LIBMPV_HEADER_ZIP_SHA256 "497628e4bb647096807ec6be11e5fe26e44eaa48908f6fc1e7d6f1fe728182be") + set(LIBMPV_ZIP_URL "https://github.com/Predidit/libmpv-linux-build/releases/download/20260810/libmpv_aarch64.zip") + set(LIBMPV_ZIP_SHA256 "da0609556e2864dbe828102972edea96173a4be1ad1427a7493942dccb039b9f") + set(LIBMPV_HEADER_ZIP_URL "https://github.com/Predidit/libmpv-linux-build/releases/download/20260810/libmpv_aarch64_header.zip") + set(LIBMPV_HEADER_ZIP_SHA256 "1333f1717bd2449bd99ffdf3fd4653bdf919c4113d0b49f54f8dbd94a8dc678c") endif() set(LIBMPV_ZIP_PATH "${CMAKE_BINARY_DIR}/libmpv_${ARCH_NAME}.zip") diff --git a/media_kit_video/linux/gl_render_thread.cc b/media_kit_video/linux/gl_render_thread.cc index 9d5aef029..6c9ba8c1e 100644 --- a/media_kit_video/linux/gl_render_thread.cc +++ b/media_kit_video/linux/gl_render_thread.cc @@ -66,10 +66,6 @@ void GLRenderThread::PostAndWait(std::function task) { wait_cv.wait(lock, [&]() { return done; }); } -bool GLRenderThread::IsCurrentThread() const { - return std::this_thread::get_id() == thread_id_; -} - void GLRenderThread::Run() { // Store thread ID { diff --git a/media_kit_video/linux/include/media_kit_video/gl_render_thread.h b/media_kit_video/linux/include/media_kit_video/gl_render_thread.h index a0be9d115..a0e3c624f 100644 --- a/media_kit_video/linux/include/media_kit_video/gl_render_thread.h +++ b/media_kit_video/linux/include/media_kit_video/gl_render_thread.h @@ -24,12 +24,9 @@ class GLRenderThread { // Post a task to the GL render thread void Post(std::function task); - + // Post a task and wait for completion (synchronous) void PostAndWait(std::function task); - - // Check if we're on the GL render thread - bool IsCurrentThread() const; private: void Run(); diff --git a/media_kit_video/linux/include/media_kit_video/texture_gl.h b/media_kit_video/linux/include/media_kit_video/texture_gl.h index 81a2ffb01..26fdc6143 100644 --- a/media_kit_video/linux/include/media_kit_video/texture_gl.h +++ b/media_kit_video/linux/include/media_kit_video/texture_gl.h @@ -22,30 +22,17 @@ G_DECLARE_FINAL_TYPE(TextureGL, texture_gl, TEXTURE_GL, TEXTURE_GL, FlTextureGL) TextureGL* texture_gl_new(VideoOutput* video_output); -/** - * @brief Checks if texture needs resize and performs it if necessary. - * This manages the mailbox triple buffering - creates/resizes all three buffers. - */ +// Creates/resizes all three mailbox buffers if needed. GL thread only. void texture_gl_check_and_resize(TextureGL* self, gint64 required_width, gint64 required_height); -/** - * @brief Renders mpv frame to the back buffer (called from dedicated GL thread). - * Uses mailbox model: renders to back buffer, then swaps with mailbox atomically. - * @return TRUE if rendering was performed, FALSE if skipped. - */ +// Renders an mpv frame to the back buffer. GL thread only. +// Returns TRUE if a frame was rendered. gboolean texture_gl_render(TextureGL* self); -/** - * @brief Publishes the rendered frame using mailbox swap. - * Atomically swaps back buffer with mailbox, old mailbox content becomes new back buffer. - * Called from dedicated GL thread after render finishes. - */ +// Publishes the back buffer into the mailbox (dirty=1). GL thread only. void texture_gl_swap_buffers(TextureGL* self); -/** - * @brief Populates texture with video frame using mailbox model. - * Atomically swaps front buffer with mailbox to get the latest frame. - */ +// Consumer side of the mailbox; called from Flutter's raster thread. gboolean texture_gl_populate_texture(FlTextureGL* texture, guint32* target, guint32* name, diff --git a/media_kit_video/linux/include/media_kit_video/video_output.h b/media_kit_video/linux/include/media_kit_video/video_output.h index 5eb7ba084..eda931afe 100644 --- a/media_kit_video/linux/include/media_kit_video/video_output.h +++ b/media_kit_video/linux/include/media_kit_video/video_output.h @@ -47,57 +47,30 @@ G_DECLARE_FINAL_TYPE(VideoOutput, #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 view |FlView| reference. - * @param handle |mpv_handle| reference casted to gint64. - * @param configuration Video output configuration. - * @param gl_render_thread |GLRenderThread| reference for dedicated GL rendering. - * @return VideoOutput* - */ +// Creates a new |VideoOutput| for given |handle| (|mpv_handle| casted to +// gint64). Falls back to S/W rendering if no usable EGL display is found. VideoOutput* video_output_new(FlTextureRegistrar* texture_registrar, - FlView* view, gint64 handle, VideoOutputConfiguration configuration, GLRenderThread* gl_render_thread); -/** - * @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. - */ +// Sets the callback invoked when the texture ID updates i.e. video +// dimensions change. 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. - */ +// Sets the required video output size. Pass 0 to size the texture based on +// the video's own 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); EGLContext video_output_get_egl_context(VideoOutput* self); -EGLSurface video_output_get_egl_surface(VideoOutput* self); - GLRenderThread* video_output_get_gl_render_thread(VideoOutput* self); guint8* video_output_get_pixel_buffer(VideoOutput* self); @@ -110,10 +83,7 @@ gint64 video_output_get_texture_id(VideoOutput* self); void video_output_notify_texture_update(VideoOutput* self); +// Schedules resize-check + render on the dedicated GL thread. void video_output_notify_render(VideoOutput* self); -void video_output_check_and_resize(VideoOutput* self); - -void video_output_render(VideoOutput* self); - #endif // VIDEO_OUTPUT_H_ diff --git a/media_kit_video/linux/include/media_kit_video/video_output_manager.h b/media_kit_video/linux/include/media_kit_video/video_output_manager.h index ff6c03ab2..3a651ca5a 100644 --- a/media_kit_video/linux/include/media_kit_video/video_output_manager.h +++ b/media_kit_video/linux/include/media_kit_video/video_output_manager.h @@ -26,51 +26,23 @@ G_DECLARE_FINAL_TYPE(VideoOutputManager, VideoOutputManager)) VideoOutputManager* video_output_manager_new( - FlTextureRegistrar* texture_registrar, - FlView* view); + FlTextureRegistrar* texture_registrar); -/** - * @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|. - */ +// Creates a new |VideoOutput| instance for given |handle|. 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. - */ +// Sets the required video output size. Pass 0 to size the texture based on +// the video's own 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. - */ +// Disposes |VideoOutput| instance for given |handle|. void video_output_manager_dispose(VideoOutputManager* self, gint64 handle); #endif diff --git a/media_kit_video/linux/media_kit_video_plugin.cc b/media_kit_video/linux/media_kit_video_plugin.cc index da6efa33f..58537d9d8 100644 --- a/media_kit_video/linux/media_kit_video_plugin.cc +++ b/media_kit_video/linux/media_kit_video_plugin.cc @@ -183,10 +183,9 @@ static MediaKitVideoPlugin* media_kit_video_plugin_new( 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); + // |view| is only needed for native fullscreen handling. + self->view = fl_plugin_registrar_get_view(registrar); + self->video_output_manager = video_output_manager_new(texture_registrar); return self; } diff --git a/media_kit_video/linux/texture_gl.cc b/media_kit_video/linux/texture_gl.cc index d46a13d02..82be578f4 100644 --- a/media_kit_video/linux/texture_gl.cc +++ b/media_kit_video/linux/texture_gl.cc @@ -12,93 +12,120 @@ #include #include #include +#include +#include -// Number of buffers for mailbox triple buffering #define NUM_BUFFERS 3 -// Buffer structure for mailbox triple buffering -// Each buffer has its own GPU resources +// One buffer of the mailbox model; each owns its full set of GPU resources. typedef struct { - guint32 fbo; // FBO for mpv rendering - guint32 texture; // Texture attached to FBO (mpv side) - EGLImageKHR egl_image; // EGLImage for sharing between contexts - guint32 flutter_texture; // Flutter's texture bound to EGLImage - gboolean flutter_texture_valid; // Whether Flutter texture is valid - std::atomic render_sync; // Sync created after mpv render (atomic for cross-thread access) + guint32 fbo; // FBO mpv renders into (mpv context) + guint32 texture; // Texture backing the FBO (mpv context) + EGLImageKHR egl_image; // Shares |texture| across contexts + guint32 flutter_texture; // Flutter-side texture bound to |egl_image| + EGLContext flutter_context; // Raster context |flutter_texture| was created in + gboolean flutter_texture_valid; + std::atomic render_sync; // Fence from producer, consumed cross-thread } RenderBuffer; /** - * Mailbox Triple Buffering Model with Drain-Only Consumer: - * - * Three buffers with fixed roles that rotate via atomic pointer swaps: - * - back: Producer (GL thread) renders to this buffer - * - mailbox: Holds the latest complete frame with dirty flag - * - front: Consumer (Flutter main thread) reads from this buffer - * - * Key design: mailbox_state combines index and dirty flag in ONE atomic: - * mailbox_state = (dirty << 8) | index - * This eliminates race conditions between checking dirty and swapping. - * - * Producer workflow (GL thread): - * 1. Render frame to back buffer - * 2. Atomic exchange: put (dirty=1, back_index) into mailbox, get old index - * 3. Old mailbox index becomes new back buffer - * - * Consumer workflow (Flutter main thread) - DRAIN-ONLY: - * 1. Atomic CAS: if dirty=1, swap (dirty=0, front_index) with mailbox - * 2. If CAS succeeds: got new frame, update front_index - * 3. If dirty=0: no new frame, keep current front buffer - * 4. Display front buffer - * - * Drain-only semantics ensures: - * - Consumer only swaps when mailbox has NEW content (dirty=1) - * - Consumer never puts its displayed frame back unless getting new one - * - Producer can always safely overwrite mailbox content - * - Single atomic operation prevents dirty/index race condition + * Mailbox triple buffering with a drain-only consumer (intentional design: + * the producer may overwrite the mailbox frame; dropped frames are expected). + * + * Roles rotate via atomic index swaps: + * - back: producer (GL thread) renders here, owns back_index exclusively + * - mailbox: latest complete frame + dirty flag + * - front: consumer (Flutter raster thread) displays, owns front_index + * + * mailbox_state packs both fields into ONE atomic — (dirty << 8) | index — + * so checking dirty and swapping the index cannot race. + * + * Producer: render to back → exchange (dirty=1, back_index) into mailbox, + * old mailbox index becomes the new back buffer. + * Consumer (drain-only): CAS-swap only when dirty=1; otherwise keep + * displaying the current front buffer. */ struct _TextureGL { FlTextureGL parent_instance; - - // The three buffers for mailbox model + RenderBuffer buffers[NUM_BUFFERS]; - - // Mailbox model: atomic indices for lock-free buffer swapping - // Producer (GL thread) owns back_index exclusively - // Consumer (main thread) owns front_index exclusively - // mailbox uses a combined atomic to avoid race between index and dirty flag - int back_index; // Producer's current buffer (GL thread only) - int front_index; // Consumer's current buffer (main thread only) - // Combined mailbox state: index in lower bits, dirty flag in upper bit - // This ensures atomic swap of both index and dirty flag together - // Encoding: (dirty << 8) | index, where dirty is 0 or 1, index is 0-2 - std::atomic mailbox_state; // Combined: dirty flag (bit 8) + buffer index (bits 0-7) - + + int back_index; // GL thread only + int front_index; // Raster thread only + std::atomic mailbox_state; // (dirty << 8) | index + guint32 current_width; guint32 current_height; gboolean buffers_initialized; gboolean initialization_posted; - std::atomic resizing; // Flag to indicate resize in progress - + std::atomic resizing; + VideoOutput* video_output; }; G_DEFINE_TYPE(TextureGL, texture_gl, fl_texture_gl_get_type()) +// GL texture names are only meaningful inside the share group that created +// them. |flutter_texture|s are created in populate (Flutter's raster context), +// but dispose runs on the platform thread where that context is never +// current — deleting there would leak the real texture, or delete an +// unrelated same-named object in GDK's own context. So dispose parks the +// names here, tagged with their creating context, and the next populate of +// any |TextureGL| reclaims exactly those entries whose context is current. +// The tag is essential with multiple Flutter engines in one process: a bare +// name must never be deleted in another engine's context. +typedef struct { + EGLContext context; // Raster context the name was created in. + guint32 texture; +} RetiredTexture; + +static std::mutex retired_textures_mutex; +static std::vector retired_textures; + +static void retire_flutter_texture(EGLContext context, guint32 texture) { + std::lock_guard lock(retired_textures_mutex); + retired_textures.push_back({context, texture}); +} + +// Must be called with a Flutter raster context current. Entries belonging to +// other contexts (other engines) are left for their own populate. +static void drain_retired_textures() { + EGLContext current_context = eglGetCurrentContext(); + if (current_context == EGL_NO_CONTEXT) { + return; + } + std::vector reclaimable; + { + std::lock_guard lock(retired_textures_mutex); + auto it = retired_textures.begin(); + while (it != retired_textures.end()) { + if (it->context == current_context) { + reclaimable.push_back(it->texture); + it = retired_textures.erase(it); + } else { + ++it; + } + } + } + for (guint32 texture : reclaimable) { + glDeleteTextures(1, &texture); + } +} + static void texture_gl_init(TextureGL* self) { for (int i = 0; i < NUM_BUFFERS; i++) { self->buffers[i].fbo = 0; self->buffers[i].texture = 0; self->buffers[i].egl_image = EGL_NO_IMAGE_KHR; self->buffers[i].flutter_texture = 0; + self->buffers[i].flutter_context = EGL_NO_CONTEXT; self->buffers[i].flutter_texture_valid = FALSE; self->buffers[i].render_sync.store(EGL_NO_SYNC_KHR, std::memory_order_relaxed); } - // Initialize mailbox model indices - // back=0 for producer, front=1 for consumer, mailbox=2 initially (not dirty) + // back=0, front=1, mailbox=2 (not dirty). self->back_index = 0; self->front_index = 1; - // mailbox_state = (dirty << 8) | index = (0 << 8) | 2 = 2 self->mailbox_state.store(2, std::memory_order_relaxed); self->current_width = 1; @@ -114,45 +141,45 @@ static void texture_gl_dispose(GObject* object) { VideoOutput* video_output = self->video_output; GLRenderThread* gl_thread = video_output_get_gl_render_thread(video_output); - // Clean up Flutter's textures (main thread) + // Flutter-side textures belong to the raster context; park them for the + // next populate instead of deleting in whatever context is current here. for (int i = 0; i < NUM_BUFFERS; i++) { if (self->buffers[i].flutter_texture != 0) { - glDeleteTextures(1, &self->buffers[i].flutter_texture); + retire_flutter_texture(self->buffers[i].flutter_context, + self->buffers[i].flutter_texture); self->buffers[i].flutter_texture = 0; + self->buffers[i].flutter_context = EGL_NO_CONTEXT; } } - - // Clean up GPU resources in dedicated GL thread + + // Everything else must be released in the dedicated GL thread. if (video_output != NULL && gl_thread != NULL) { gl_thread->PostAndWait([self, video_output]() { EGLDisplay egl_display = video_output_get_egl_display(video_output); EGLContext egl_context = video_output_get_egl_context(video_output); - - // Clean up all buffers + for (int i = 0; i < NUM_BUFFERS; i++) { RenderBuffer* buf = &self->buffers[i]; - - // Clean up EGLSyncKHR + EGLSyncKHR sync = buf->render_sync.load(std::memory_order_acquire); if (sync != EGL_NO_SYNC_KHR) { eglDestroySyncKHR(egl_display, sync); buf->render_sync.store(EGL_NO_SYNC_KHR, std::memory_order_release); } - - // Clean up EGLImage + if (buf->egl_image != EGL_NO_IMAGE_KHR) { eglDestroyImageKHR(egl_display, buf->egl_image); buf->egl_image = EGL_NO_IMAGE_KHR; } } - - // Clean up mpv's OpenGL resources (in mpv's isolated context) + + // mpv-side GL objects live in the isolated context. if (egl_context != EGL_NO_CONTEXT) { eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, egl_context); - + for (int i = 0; i < NUM_BUFFERS; i++) { RenderBuffer* buf = &self->buffers[i]; - + if (buf->texture != 0) { glDeleteTextures(1, &buf->texture); buf->texture = 0; @@ -165,7 +192,7 @@ static void texture_gl_dispose(GObject* object) { } }); } - + self->current_width = 1; self->current_height = 1; self->video_output = NULL; @@ -184,60 +211,56 @@ TextureGL* texture_gl_new(VideoOutput* video_output) { } /** - * Called from the dedicated GL rendering thread. - * Creates or resizes all three buffers for the mailbox model. + * Creates or resizes all three buffers. Runs in the dedicated GL thread. */ void texture_gl_check_and_resize(TextureGL* self, gint64 required_width, gint64 required_height) { VideoOutput* video_output = self->video_output; - + if (required_width < 1 || required_height < 1) { return; } - + gboolean first_frame = !self->buffers_initialized; gboolean resize = self->current_width != (guint32)required_width || self->current_height != (guint32)required_height; - + if (!first_frame && !resize) { return; } - + 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 + eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, egl_context); - - // Mark as resizing to prevent consumer from accessing buffers + + // Keep the consumer off the buffers while they are being recreated. self->resizing.store(TRUE, std::memory_order_release); - - // Free previous resources for all buffers + for (int i = 0; i < NUM_BUFFERS; i++) { RenderBuffer* buf = &self->buffers[i]; - + if (!first_frame) { - // Wait for any pending GPU work before destroying resources + // Wait for pending GPU work before destroying resources. EGLSyncKHR sync = buf->render_sync.load(std::memory_order_acquire); if (sync != EGL_NO_SYNC_KHR) { - eglClientWaitSyncKHR(egl_display, sync, + eglClientWaitSyncKHR(egl_display, sync, EGL_SYNC_FLUSH_COMMANDS_BIT_KHR, EGL_FOREVER_KHR); eglDestroySyncKHR(egl_display, sync); buf->render_sync.store(EGL_NO_SYNC_KHR, std::memory_order_release); } - + if (buf->egl_image != EGL_NO_IMAGE_KHR) { eglDestroyImageKHR(egl_display, buf->egl_image); buf->egl_image = EGL_NO_IMAGE_KHR; } - + glDeleteTextures(1, &buf->texture); glDeleteFramebuffers(1, &buf->fbo); } - - // Create FBO and texture for this buffer + glGenFramebuffers(1, &buf->fbo); glBindFramebuffer(GL_FRAMEBUFFER, buf->fbo); - + glGenTextures(1, &buf->texture); glBindTexture(GL_TEXTURE_2D, buf->texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); @@ -246,12 +269,10 @@ void texture_gl_check_and_resize(TextureGL* self, gint64 required_width, gint64 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 texture to FBO + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, buf->texture, 0); - - // Create EGLImage from texture for sharing between contexts + EGLint egl_image_attribs[] = { EGL_NONE }; buf->egl_image = eglCreateImageKHR( egl_display, @@ -259,74 +280,59 @@ void texture_gl_check_and_resize(TextureGL* self, gint64 required_width, gint64 EGL_GL_TEXTURE_2D_KHR, (EGLClientBuffer)(guintptr)buf->texture, egl_image_attribs); - + glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindTexture(GL_TEXTURE_2D, 0); - - // Mark Flutter texture as invalid (needs recreation) + + // Flutter-side texture must be recreated against the new EGLImage. buf->flutter_texture_valid = FALSE; buf->render_sync.store(EGL_NO_SYNC_KHR, std::memory_order_release); } - - // Flush to ensure textures are ready + glFlush(); - - // Reset mailbox model indices + + // Reset mailbox: back=0, front=1, mailbox=2 (not dirty). self->back_index = 0; self->front_index = 1; - // mailbox_state = (dirty << 8) | index = (0 << 8) | 2 = 2 self->mailbox_state.store(2, std::memory_order_release); - - // Mark buffers as initialized and update dimensions + self->buffers_initialized = TRUE; self->current_width = required_width; self->current_height = required_height; - + self->resizing.store(FALSE, std::memory_order_release); } /** - * Renders mpv frame to the back buffer. - * Called from the dedicated GL rendering thread. + * Renders an mpv frame to the back buffer. Runs in the dedicated GL thread. */ gboolean texture_gl_render(TextureGL* self) { VideoOutput* video_output = self->video_output; 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); - + if (!render_context || !self->buffers_initialized) { return FALSE; } - - // Get the back buffer (producer's exclusive buffer) - int back_idx = self->back_index; - RenderBuffer* back_buf = &self->buffers[back_idx]; - + + RenderBuffer* back_buf = &self->buffers[self->back_index]; if (back_buf->fbo == 0) { return FALSE; } - - // Before reusing this buffer, wait for any previous render to complete - // This ensures GPU has finished with this buffer before we overwrite it + + // GPU must be done with this buffer before it is overwritten. EGLSyncKHR old_sync = back_buf->render_sync.exchange(EGL_NO_SYNC_KHR, std::memory_order_acq_rel); if (old_sync != EGL_NO_SYNC_KHR) { - // Wait for previous GPU work to complete, then destroy the sync eglClientWaitSyncKHR(egl_display, old_sync, EGL_SYNC_FLUSH_COMMANDS_BIT_KHR, EGL_FOREVER_KHR); eglDestroySyncKHR(egl_display, old_sync); } - - gint32 required_width = self->current_width; - gint32 required_height = self->current_height; - - // Switch to mpv's isolated context for rendering + eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, egl_context); - - // Bind back buffer's FBO + glBindFramebuffer(GL_FRAMEBUFFER, back_buf->fbo); - - // Render mpv frame to back buffer's texture - mpv_opengl_fbo fbo{(gint32)back_buf->fbo, required_width, required_height, 0}; + mpv_opengl_fbo fbo{(gint32)back_buf->fbo, (gint32)self->current_width, + (gint32)self->current_height, 0}; int flip_y = 0; mpv_render_param params[] = { {MPV_RENDER_PARAM_OPENGL_FBO, &fbo}, @@ -334,39 +340,42 @@ gboolean texture_gl_render(TextureGL* self) { {MPV_RENDER_PARAM_INVALID, NULL}, }; mpv_render_context_render(render_context, params); - - // Unbind FBO glBindFramebuffer(GL_FRAMEBUFFER, 0); - - // Flush to ensure rendering commands are submitted to GPU + + // Submit commands, then publish a fence the consumer synchronizes against. glFlush(); - - // Create sync fence to mark render completion - // Consumer will use this for GPU-side synchronization EGLSyncKHR new_sync = eglCreateSyncKHR(egl_display, EGL_SYNC_FENCE_KHR, NULL); back_buf->render_sync.store(new_sync, std::memory_order_release); - + return TRUE; } /** - * Publishes the rendered frame using mailbox swap. - * Atomically swaps back buffer with mailbox and sets dirty flag in ONE operation. - * Called from dedicated GL thread after render finishes. + * Publishes the rendered frame: atomically moves back_index into the mailbox + * with dirty=1; the old mailbox buffer becomes the new back buffer. */ void texture_gl_swap_buffers(TextureGL* self) { - // Atomic swap with dirty flag set: - // We put our back_index into mailbox with dirty=1 - // We get back the old mailbox index (ignore its dirty flag) - int new_state = (1 << 8) | self->back_index; // dirty=1, index=back_index + int new_state = (1 << 8) | self->back_index; int old_state = self->mailbox_state.exchange(new_state, std::memory_order_acq_rel); - // Extract just the index from old state (ignore dirty flag) self->back_index = old_state & 0xFF; } +// 1x1 placeholder returned while buffers are unavailable. thread_local: each +// engine populates from its own raster thread, so the name never leaks into +// another engine's context. +static guint32 get_dummy_texture() { + static thread_local guint32 dummy_texture = 0; + if (dummy_texture == 0) { + glGenTextures(1, &dummy_texture); + glBindTexture(GL_TEXTURE_2D, dummy_texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + glBindTexture(GL_TEXTURE_2D, 0); + } + return dummy_texture; +} + /** - * Populates texture with video frame using mailbox model. - * Called from Flutter's main thread. + * Consumer side of the mailbox. Called from Flutter's raster thread. */ gboolean texture_gl_populate_texture(FlTextureGL* texture, guint32* target, @@ -378,81 +387,70 @@ gboolean texture_gl_populate_texture(FlTextureGL* texture, VideoOutput* video_output = self->video_output; GLRenderThread* gl_thread = video_output_get_gl_render_thread(video_output); EGLDisplay egl_display = video_output_get_egl_display(video_output); - - // Trigger initialization on first call + + // populate is the only place the FlTextureGL contract guarantees Flutter's + // raster context is current — reclaim parked texture names here. + drain_retired_textures(); + + // Kick off buffer initialization on first call. if (!self->initialization_posted && !self->buffers_initialized) { gint64 required_width = video_output_get_width(video_output); gint64 required_height = video_output_get_height(video_output); - + if (required_width > 0 && required_height > 0 && gl_thread) { self->initialization_posted = TRUE; video_output_notify_render(video_output); } } - - // If resize is in progress, return dummy texture + if (self->resizing.load(std::memory_order_acquire)) { *target = GL_TEXTURE_2D; - static guint32 dummy_texture = 0; - if (dummy_texture == 0) { - glGenTextures(1, &dummy_texture); - glBindTexture(GL_TEXTURE_2D, dummy_texture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); - glBindTexture(GL_TEXTURE_2D, 0); - } - *name = dummy_texture; + *name = get_dummy_texture(); *width = 1; *height = 1; return TRUE; } - - // Drain-only consumer: only swap if mailbox has new content (dirty flag set) - // Use CAS loop to atomically check dirty and swap in one operation + + // Drain-only: CAS-swap our front buffer into the mailbox only when it holds + // a new frame (dirty bit 8 set); otherwise keep the current front buffer. int current_state = self->mailbox_state.load(std::memory_order_acquire); - while (current_state & 0x100) { // Check dirty flag (bit 8) - // Mailbox has new frame - try to swap - // New state: dirty=0, index=our front_index + while (current_state & 0x100) { int new_state = self->front_index; // dirty=0, index=front_index if (self->mailbox_state.compare_exchange_weak(current_state, new_state, std::memory_order_acq_rel, std::memory_order_acquire)) { - // Swap succeeded - extract the index we got self->front_index = current_state & 0xFF; break; } - // CAS failed, current_state has been updated, retry } - // If dirty was not set, we keep using current front buffer - - // front_index points to the frame we should display - int front_idx = self->front_index; - RenderBuffer* front_buf = &self->buffers[front_idx]; - - // GPU synchronization: ensure producer's rendering is complete before we use the texture - // Take ownership of the sync object atomically + + RenderBuffer* front_buf = &self->buffers[self->front_index]; + + // Take ownership of the producer's fence and wait on it — GPU-side when + // possible (queues the wait in Flutter's command stream without blocking). EGLSyncKHR sync = front_buf->render_sync.exchange(EGL_NO_SYNC_KHR, std::memory_order_acq_rel); if (sync != EGL_NO_SYNC_KHR) { - // Use GPU-side wait for better performance (doesn't block CPU) - // This inserts a wait into Flutter's GL command stream if (epoxy_has_egl_extension(egl_display, "EGL_KHR_wait_sync")) { eglWaitSyncKHR(egl_display, sync, 0); } else { - // Fallback to CPU wait if eglWaitSyncKHR not available eglClientWaitSyncKHR(egl_display, sync, EGL_SYNC_FLUSH_COMMANDS_BIT_KHR, EGL_FOREVER_KHR); } - // Destroy the sync after use (we own it now) eglDestroySyncKHR(egl_display, sync); } - - // Check if we need to create/recreate Flutter texture for this buffer + + // (Re)bind Flutter's texture to this buffer's EGLImage if invalidated. if (!front_buf->flutter_texture_valid && front_buf->egl_image != EGL_NO_IMAGE_KHR) { - // Delete old texture if exists if (front_buf->flutter_texture != 0) { - glDeleteTextures(1, &front_buf->flutter_texture); + if (front_buf->flutter_context == eglGetCurrentContext()) { + glDeleteTextures(1, &front_buf->flutter_texture); + } else { + retire_flutter_texture(front_buf->flutter_context, + front_buf->flutter_texture); + } } - - // Create Flutter's texture from this buffer's EGLImage + glGenTextures(1, &front_buf->flutter_texture); + front_buf->flutter_context = eglGetCurrentContext(); glBindTexture(GL_TEXTURE_2D, front_buf->flutter_texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); @@ -460,31 +458,22 @@ gboolean texture_gl_populate_texture(FlTextureGL* texture, glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, front_buf->egl_image); glBindTexture(GL_TEXTURE_2D, 0); - + front_buf->flutter_texture_valid = TRUE; - - // Notify Flutter about texture availability + video_output_notify_texture_update(video_output); } - + *target = GL_TEXTURE_2D; *name = front_buf->flutter_texture; *width = self->current_width; *height = self->current_height; - - // If texture is not valid yet, return dummy texture - if (!front_buf->flutter_texture_valid || front_buf->flutter_texture == 0) { - static guint32 dummy_texture = 0; - if (dummy_texture == 0) { - glGenTextures(1, &dummy_texture); - glBindTexture(GL_TEXTURE_2D, dummy_texture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); - glBindTexture(GL_TEXTURE_2D, 0); - } - *name = dummy_texture; + + if (!front_buf->flutter_texture_valid) { + *name = get_dummy_texture(); *width = 1; *height = 1; } - + return TRUE; } diff --git a/media_kit_video/linux/utils.cc b/media_kit_video/linux/utils.cc index 386e81be7..1c3dcb57a 100644 --- a/media_kit_video/linux/utils.cc +++ b/media_kit_video/linux/utils.cc @@ -1,11 +1,10 @@ -// // This file is a part of media_kit -// // (https://github.com/media-kit/media-kit). -// // -// // Copyright © 2021 & onwards, Hitesh Kumar Saini . -// // All rights reserved. -// // Use of this source code is governed by MIT license that can be found in -// the -// // LICENSE file. +// This file is a part of media_kit +// (https://github.com/media-kit/media-kit). +// +// Copyright © 2021 & onwards, Hitesh Kumar Saini . +// 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" diff --git a/media_kit_video/linux/video_output.cc b/media_kit_video/linux/video_output.cc index 2a2b488e7..59e82961c 100644 --- a/media_kit_video/linux/video_output.cc +++ b/media_kit_video/linux/video_output.cc @@ -12,17 +12,15 @@ #include "include/media_kit_video/gl_render_thread.h" #include -#include #include #include struct _VideoOutput { GObject parent_instance; TextureGL* texture_gl; - EGLDisplay egl_display; /* EGL display for mpv rendering (shared with flutter). */ - EGLConfig egl_config; /* EGL config from Flutter (for compatibility). */ - EGLContext egl_context; /* Isolated EGL context (non-shared). */ - EGLSurface egl_surface; /* Place holder surface for activating egl context */ + EGLDisplay egl_display; /* Same EGLDisplay the Flutter engine renders on. */ + EGLConfig egl_config; + EGLContext egl_context; /* Isolated (non-shared) context for mpv. */ guint8* pixel_buffer; TextureSW* texture_sw; GMutex mutex; /* Only used in S/W rendering. */ @@ -43,7 +41,7 @@ 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); @@ -53,11 +51,10 @@ static void video_output_dispose(GObject* object) { if (self->texture_gl) { fl_texture_registrar_unregister_texture(self->texture_registrar, FL_TEXTURE(self->texture_gl)); - - // Clean up EGL resources in dedicated GL thread + + // EGL resources must be released in the dedicated GL thread. if (self->render_context != NULL || self->egl_context != EGL_NO_CONTEXT) { self->gl_render_thread->PostAndWait([self]() { - // Free mpv_render_context with our isolated EGL context if (self->render_context != NULL) { if (self->egl_context != EGL_NO_CONTEXT) { eglMakeCurrent(self->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, self->egl_context); @@ -65,15 +62,13 @@ static void video_output_dispose(GObject* object) { mpv_render_context_free(self->render_context); self->render_context = NULL; } - - // Clean up EGL context 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 @@ -101,7 +96,6 @@ static void video_output_init(VideoOutput* self) { self->egl_display = EGL_NO_DISPLAY; self->egl_config = NULL; self->egl_context = EGL_NO_CONTEXT; - self->egl_surface = EGL_NO_SURFACE; self->texture_sw = NULL; self->pixel_buffer = NULL; self->handle = NULL; @@ -118,7 +112,6 @@ static void video_output_init(VideoOutput* self) { } VideoOutput* video_output_new(FlTextureRegistrar* texture_registrar, - FlView* view, gint64 handle, VideoOutputConfiguration configuration, GLRenderThread* gl_render_thread) { @@ -138,46 +131,65 @@ VideoOutput* video_output_new(FlTextureRegistrar* texture_registrar, #endif gboolean hardware_acceleration_supported = FALSE; - - // Get EGL display and config in main thread (where Flutter context is available) - // Only attempt if hardware acceleration is enabled + if (self->configuration.enable_hardware_acceleration) { - EGLDisplay flutter_display = eglGetCurrentDisplay(); - EGLContext flutter_context = eglGetCurrentContext(); - - if (flutter_display != EGL_NO_DISPLAY && flutter_context != EGL_NO_CONTEXT) { - self->egl_display = flutter_display; - - // Get Flutter's EGL config by querying its context - EGLint config_id = 0; - if (eglQueryContext(flutter_display, flutter_context, EGL_CONFIG_ID, &config_id)) { - // Retrieve the actual EGLConfig from the config ID - EGLint num_configs = 0; - EGLint config_attribs[] = { EGL_CONFIG_ID, config_id, EGL_NONE }; - - if (eglChooseConfig(flutter_display, config_attribs, &self->egl_config, 1, &num_configs) && num_configs > 0) { - g_print("media_kit: VideoOutput: Got Flutter's EGL display (%p) and config (ID: %d)\n", - flutter_display, config_id); - - // Create texture_gl in main thread (needed by mpv callback) - self->texture_gl = texture_gl_new(self); - if (!fl_texture_registrar_register_texture( - texture_registrar, FL_TEXTURE(self->texture_gl))) { - g_printerr("media_kit: VideoOutput: Failed to register texture.\n"); - g_object_unref(self->texture_gl); - self->texture_gl = NULL; - self->egl_config = NULL; - } - } else { - g_printerr("media_kit: VideoOutput: Failed to get Flutter's EGL config by ID.\n"); - self->egl_config = NULL; - } + // The GTK embedder (FlOpenGLManager) renders with EGL + GLES2 on both + // X11 and Wayland, but its contexts are never current on the platform + // thread. EGL returns the same EGLDisplay for the same native display, + // so derive the engine's display from the GDK display — same-display is + // all EGLImage/EGLSync sharing requires (mpv's context is non-shared). + GdkDisplay* display = gdk_display_get_default(); + EGLDisplay egl_display = EGL_NO_DISPLAY; + if (epoxy_has_egl_extension(EGL_NO_DISPLAY, "EGL_EXT_platform_base")) { + if (GDK_IS_WAYLAND_DISPLAY(display)) { + egl_display = eglGetPlatformDisplayEXT( + EGL_PLATFORM_WAYLAND_EXT, gdk_wayland_display_get_wl_display(display), NULL); + } else if (GDK_IS_X11_DISPLAY(display)) { + egl_display = eglGetPlatformDisplayEXT( + EGL_PLATFORM_X11_EXT, gdk_x11_display_get_xdisplay(display), NULL); + } + } + + // eglQueryString(EGL_VERSION) != NULL means the engine already + // initialized this display. Otherwise this is not the EGL-based embedder + // (legacy GLX embedders are unsupported); fall through to S/W rendering. + if (egl_display != EGL_NO_DISPLAY && + eglQueryString(egl_display, EGL_VERSION) != NULL) { + // mpv renders into an FBO in a surfaceless context; any GLES2 config + // works since it never backs an actual surface. + const EGLint config_attribs[] = { + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_NONE, + }; + EGLint num_configs = 0; + if (eglChooseConfig(egl_display, config_attribs, &self->egl_config, 1, &num_configs) && + num_configs > 0) { + self->egl_display = egl_display; + g_print("media_kit: VideoOutput: Got engine EGL display (%p) with GLES2 config.\n", + egl_display); } else { - g_printerr("media_kit: VideoOutput: Failed to query Flutter's EGL config ID.\n"); + g_printerr("media_kit: VideoOutput: Failed to choose EGL config.\n"); self->egl_config = NULL; } } else { - g_printerr("media_kit: VideoOutput: Failed to get Flutter's EGL display or context.\n"); + g_printerr( + "media_kit: VideoOutput: H/W rendering requires the EGL-based " + "Flutter embedder.\n"); + } + + if (self->egl_display != EGL_NO_DISPLAY && self->egl_config != NULL) { + self->texture_gl = texture_gl_new(self); + if (!fl_texture_registrar_register_texture( + texture_registrar, FL_TEXTURE(self->texture_gl))) { + g_printerr("media_kit: VideoOutput: Failed to register texture.\n"); + g_object_unref(self->texture_gl); + self->texture_gl = NULL; + self->egl_config = NULL; + } } } @@ -187,45 +199,38 @@ VideoOutput* video_output_new(FlTextureRegistrar* texture_registrar, // Causes frame drops with `pulse` audio output. (SlotSun/dart_simple_live#42) // mpv_set_option_string(self->handle, "video-timing-offset", "0"); - if (self->texture_gl != NULL && - self->egl_display != EGL_NO_DISPLAY && + if (self->texture_gl != NULL && + self->egl_display != EGL_NO_DISPLAY && self->egl_config != NULL) { - - // Bind OpenGL ES API (Flutter uses OpenGL ES on Linux) eglBindAPI(EGL_OPENGL_ES_API); - - // Create an isolated EGL context using Flutter's config - // Using the SAME egl_display and egl_config as Flutter for maximum compatibility + + // Isolated (non-shared) GLES2 context; frames are shared with Flutter + // via EGLImage on the same display, not via context share lists. const EGLint context_attribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE }; - - self->egl_context = eglCreateContext(self->egl_display, self->egl_config, + self->egl_context = eglCreateContext(self->egl_display, self->egl_config, EGL_NO_CONTEXT, context_attribs); - + if (self->egl_context != EGL_NO_CONTEXT) { - g_print("media_kit: VideoOutput: Created isolated EGL context: %p (display: %p, using Flutter's config)\n", - self->egl_context, self->egl_display); - - // Make our isolated context current for initialization (surfaceless) + // Surfaceless: mpv only ever renders into FBOs. if (eglMakeCurrent(self->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, self->egl_context)) { - // 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 + + // 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; @@ -234,7 +239,7 @@ VideoOutput* video_output_new(FlTextureRegistrar* texture_registrar, 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, @@ -243,7 +248,7 @@ VideoOutput* video_output_new(FlTextureRegistrar* texture_registrar, if (self->destroyed) { return; } - // Asynchronously notify render (don't block mpv thread) + // Asynchronous: must not block mpv's thread. video_output_notify_render(self); }, self); @@ -263,11 +268,8 @@ VideoOutput* video_output_new(FlTextureRegistrar* texture_registrar, g_printerr("media_kit: VideoOutput: Failed to create isolated EGL context. Error: 0x%x\n", eglGetError()); } } - // If hardware acceleration is not supported or disabled, fall back to software rendering }); - // hardware_acceleration_supported is already set by the lambda - - // If hardware acceleration failed and texture was created, clean it up + if (!hardware_acceleration_supported && self->texture_gl != NULL) { fl_texture_registrar_unregister_texture(texture_registrar, FL_TEXTURE(self->texture_gl)); @@ -275,11 +277,10 @@ VideoOutput* video_output_new(FlTextureRegistrar* texture_registrar, self->texture_gl = NULL; } #ifdef MPV_RENDER_API_TYPE_SW + // H/W rendering unavailable; fall back to S/W rendering. 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))) { @@ -381,10 +382,6 @@ EGLContext video_output_get_egl_context(VideoOutput* self) { return self->egl_context; } -EGLSurface video_output_get_egl_surface(VideoOutput* self) { - return self->egl_surface; -} - GLRenderThread* video_output_get_gl_render_thread(VideoOutput* self) { return self->gl_render_thread; } @@ -393,16 +390,10 @@ 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; - +// Reads rotation-corrected display dimensions from mpv's video-out-params. +static void video_output_get_video_dimensions(VideoOutput* self, + gint64* out_width, + gint64* out_height) { mpv_node params; mpv_get_property(self->handle, "video-out-params", MPV_FORMAT_NODE, ¶ms); @@ -426,12 +417,22 @@ gint64 video_output_get_width(VideoOutput* self) { mpv_free_node_contents(¶ms); } - width = rotate == 0 || rotate == 180 ? dw : dh; - height = rotate == 0 || rotate == 180 ? dh : dw; + *out_width = rotate == 0 || rotate == 180 ? dw : dh; + *out_height = rotate == 0 || rotate == 180 ? dh : dw; +} + +gint64 video_output_get_width(VideoOutput* self) { + // Fixed width. + if (self->width) { + return self->width; + } + + gint64 width = 0; + gint64 height = 0; + video_output_get_video_dimensions(self, &width, &height); if (self->texture_sw != NULL) { - // Make sure |width| & |height| fit between |SW_RENDERING_MAX_WIDTH| & - // |SW_RENDERING_MAX_HEIGHT| while maintaining aspect ratio. + // Clamp to S/W rendering limits while maintaining aspect ratio. if (width >= SW_RENDERING_MAX_WIDTH) { return SW_RENDERING_MAX_WIDTH; } @@ -449,39 +450,12 @@ gint64 video_output_get_height(VideoOutput* self) { 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; + video_output_get_video_dimensions(self, &width, &height); if (self->texture_sw != NULL) { - // Make sure |width| & |height| fit between |SW_RENDERING_MAX_WIDTH| & - // |SW_RENDERING_MAX_HEIGHT| while maintaining aspect ratio. + // Clamp to S/W rendering limits while maintaining aspect ratio. if (height >= SW_RENDERING_MAX_HEIGHT) { return SW_RENDERING_MAX_HEIGHT; } @@ -516,52 +490,41 @@ void video_output_notify_texture_update(VideoOutput* self) { } } -void video_output_notify_render(VideoOutput* self) { - if (self->destroyed || !self->gl_render_thread) { - return; - } - // Post combined check_and_resize + render task to GL thread (asynchronously) - self->gl_render_thread->Post([self]() { - video_output_check_and_resize(self); - video_output_render(self); - }); -} - -void video_output_check_and_resize(VideoOutput* self) { +// Both run in the dedicated GL thread. +static void video_output_check_and_resize(VideoOutput* self) { if (self->destroyed || !self->texture_gl) { return; } - - TextureGL* texture = self->texture_gl; + gint64 required_width = video_output_get_width(self); gint64 required_height = video_output_get_height(self); - if (required_width < 1 || required_height < 1) { return; } - - // Check if resize is needed through texture_gl - texture_gl_check_and_resize(texture, required_width, required_height); + + texture_gl_check_and_resize(self->texture_gl, required_width, required_height); } -void video_output_render(VideoOutput* self) { +static void video_output_render(VideoOutput* self) { if (self->destroyed) { return; } - - // H/W rendering with triple buffering + if (self->texture_gl && self->render_context) { - // Render to write buffer - gboolean rendered = texture_gl_render(self->texture_gl); - - // Only swap and notify if rendering was actually performed - if (rendered) { - // Publish the rendered frame (update buffer indices) + if (texture_gl_render(self->texture_gl)) { texture_gl_swap_buffers(self->texture_gl); - - // Notify Flutter that a new frame is available fl_texture_registrar_mark_texture_frame_available( self->texture_registrar, FL_TEXTURE(self->texture_gl)); } } } + +void video_output_notify_render(VideoOutput* self) { + if (self->destroyed || !self->gl_render_thread) { + return; + } + self->gl_render_thread->Post([self]() { + video_output_check_and_resize(self); + video_output_render(self); + }); +} diff --git a/media_kit_video/linux/video_output_manager.cc b/media_kit_video/linux/video_output_manager.cc index 454d1f58f..abc4ea9f9 100644 --- a/media_kit_video/linux/video_output_manager.cc +++ b/media_kit_video/linux/video_output_manager.cc @@ -12,8 +12,7 @@ struct _VideoOutputManager { GObject parent_instance; GHashTable* video_outputs; FlTextureRegistrar* texture_registrar; - FlView* view; - GLRenderThread* gl_render_thread; + GLRenderThread* gl_render_thread; /* Shared by all |VideoOutput|s. */ }; G_DEFINE_TYPE(VideoOutputManager, video_output_manager, G_TYPE_OBJECT) @@ -21,7 +20,7 @@ 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); - self->gl_render_thread = new GLRenderThread(); // Dedicated GL render thread + self->gl_render_thread = new GLRenderThread(); } static void video_output_manager_dispose(GObject* object) { @@ -36,12 +35,10 @@ static void video_output_manager_class_init(VideoOutputManagerClass* klass) { } VideoOutputManager* video_output_manager_new( - FlTextureRegistrar* texture_registrar, - FlView* view) { + FlTextureRegistrar* texture_registrar) { 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; } @@ -52,7 +49,7 @@ void video_output_manager_create(VideoOutputManager* self, 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, self->gl_render_thread); + self->texture_registrar, handle, configuration, self->gl_render_thread); 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), diff --git a/media_kit_video/ohos/src/main/ets/com/alexmercerind/media_kit_video/MediaKitVideoPlugin.ets b/media_kit_video/ohos/src/main/ets/com/alexmercerind/media_kit_video/MediaKitVideoPlugin.ets index 96e1bdea9..da9fe6fc9 100644 --- a/media_kit_video/ohos/src/main/ets/com/alexmercerind/media_kit_video/MediaKitVideoPlugin.ets +++ b/media_kit_video/ohos/src/main/ets/com/alexmercerind/media_kit_video/MediaKitVideoPlugin.ets @@ -16,8 +16,7 @@ import { AbilityAware, AbilityPluginBinding } from '@ohos/flutter_ohos'; -import { TextureUpdateCallback } from './TextureUpdateCallback'; -import { Rect, SizeData, Utils } from './Utils'; +import { Utils } from './Utils'; import { VideoOutputManager } from './VideoOutputManager'; import { common } from '@kit.AbilityKit'; @@ -33,7 +32,9 @@ export default class MediaKitVideoPlugin implements FlutterPlugin, MethodCallHan } onDetachedFromAbility(): void { - return; + this.videoOutputManager?.disposeAll(); + this.videoOutputManager = null; + this.context = null; } getUniqueClassName(): string { @@ -50,31 +51,21 @@ export default class MediaKitVideoPlugin implements FlutterPlugin, MethodCallHan if (this.channel != null) { this.channel.setMethodCallHandler(null); } + this.videoOutputManager?.disposeAll(); + this.videoOutputManager = null; + this.channel = null; + this.binding = null; } onMethodCall(call: MethodCall, result: MethodResult): void { switch (call.method) { case "VideoOutputManager.Create": { const handle = parseInt(call.argument("handle")); - const textureUpdateCallback: TextureUpdateCallback = { - onTextureUpdate: (id: number, wid: number, width: number, height: number): void => { - const rect: Rect = { - left: 0, - top: 0, - width: width, - height: height, - } - const args: SizeData = { - handle: handle, - id: id, - wid: wid, - rect: rect, - } - this.channel?.invokeMethod("VideoOutput.Resize", args); - } + if (this.videoOutputManager == null) { + result.error("unavailable", "VideoOutputManager is not attached to an ability.", null); + break; } - this.videoOutputManager?.create(handle, textureUpdateCallback); - result.success(null); + result.success(this.videoOutputManager.create(handle)); break; } case "VideoOutputManager.SetSurfaceSize": { @@ -111,4 +102,4 @@ export default class MediaKitVideoPlugin implements FlutterPlugin, MethodCallHan } } } -} \ No newline at end of file +} diff --git a/media_kit_video/ohos/src/main/ets/com/alexmercerind/media_kit_video/TextureUpdateCallback.ets b/media_kit_video/ohos/src/main/ets/com/alexmercerind/media_kit_video/TextureUpdateCallback.ets deleted file mode 100644 index ccfbaec9a..000000000 --- a/media_kit_video/ohos/src/main/ets/com/alexmercerind/media_kit_video/TextureUpdateCallback.ets +++ /dev/null @@ -1,11 +0,0 @@ -/** - * This file is a part of media_kit (https://github.com/media-kit/media-kit). - * - * Copyright © 2025 & onwards, Bao Han . - * All rights reserved. - * Use of this source code is governed by MIT license that can be found in the LICENSE file. - */ - -export interface TextureUpdateCallback { - onTextureUpdate: (id: number, wid: number, width: number, height: number) => void; -} \ No newline at end of file diff --git a/media_kit_video/ohos/src/main/ets/com/alexmercerind/media_kit_video/VideoOutput.ets b/media_kit_video/ohos/src/main/ets/com/alexmercerind/media_kit_video/VideoOutput.ets index 44e719786..22c1a75de 100644 --- a/media_kit_video/ohos/src/main/ets/com/alexmercerind/media_kit_video/VideoOutput.ets +++ b/media_kit_video/ohos/src/main/ets/com/alexmercerind/media_kit_video/VideoOutput.ets @@ -8,8 +8,8 @@ import Log from '@ohos/flutter_ohos/src/main/ets/util/Log'; import { SurfaceTextureEntry } from '@ohos/flutter_ohos'; -import { TextureUpdateCallback } from './TextureUpdateCallback'; import { TextureRegistry } from '@ohos/flutter_ohos'; +import { SizeData } from './Utils'; const TAG = "VideoOutput"; @@ -18,28 +18,36 @@ export class VideoOutput { private wid: number = 0; private w: number = 0; private h: number = 0; - private textureUpdateCallback: TextureUpdateCallback; private textureRegistry: TextureRegistry; private surfaceProducer: SurfaceTextureEntry; + private disposed: boolean = false; - constructor(textureRegistry: TextureRegistry, surfaceProducer: SurfaceTextureEntry, - textureUpdateCallback: TextureUpdateCallback) { + constructor(textureRegistry: TextureRegistry, surfaceProducer: SurfaceTextureEntry) { this.surfaceProducer = surfaceProducer; - this.textureUpdateCallback = textureUpdateCallback; this.textureRegistry = textureRegistry; - this.setSurfaceSize(0, 0, true); + this.id = this.surfaceProducer.getTextureId(); + this.wid = this.surfaceProducer.getSurfaceId(); + this.setSurfaceSize(1, 1, true); } dispose(): void { + if (this.disposed) { + return; + } + try { - this.surfaceProducer.release(); - this.onSurfaceDestroyed(); + this.textureRegistry.unregisterTexture(this.id); + this.disposed = true; + this.id = 0; + this.wid = 0; + this.w = 0; + this.h = 0; } catch (e) { Log.e(TAG, `dispose: ${e}`); } } - async setSurfaceSize(width: number, height: number, force: boolean = false): Promise { + setSurfaceSize(width: number, height: number, force: boolean = false): void { try { if (!force && this.w == width && this.h == height) { return; @@ -48,26 +56,22 @@ export class VideoOutput { this.w = width; this.h = height; this.textureRegistry.setTextureBufferSize(this.id, width, height); - this.onSurfaceCreated(); } catch (e) { Log.e(TAG, `setSurfaceSize error: ${e}`); } } - private async onSurfaceCreated(): Promise { - Log.i(TAG, "onSurfaceCreated"); - try { - this.id = this.surfaceProducer.getTextureId(); - this.wid = this.surfaceProducer.getSurfaceId(); - this.textureUpdateCallback.onTextureUpdate(this.id, this.wid, this.w, this.h); - } catch (e) { - Log.e(TAG, `onSurfaceCreated error: ${e}`); + getSizeData(handle: number): SizeData { + return { + handle: handle, + id: this.id, + wid: this.wid, + rect: { + left: 0, + top: 0, + width: this.w, + height: this.h, + }, } } - - private async onSurfaceDestroyed(): Promise { - Log.i(TAG, "onSurfaceDestroyed"); - this.textureUpdateCallback.onTextureUpdate(this.id, 0, 0, 0); - this.surfaceProducer.release(); - } -} \ No newline at end of file +} diff --git a/media_kit_video/ohos/src/main/ets/com/alexmercerind/media_kit_video/VideoOutputManager.ets b/media_kit_video/ohos/src/main/ets/com/alexmercerind/media_kit_video/VideoOutputManager.ets index 023e50a8f..3a02fcde5 100644 --- a/media_kit_video/ohos/src/main/ets/com/alexmercerind/media_kit_video/VideoOutputManager.ets +++ b/media_kit_video/ohos/src/main/ets/com/alexmercerind/media_kit_video/VideoOutputManager.ets @@ -9,8 +9,7 @@ import Log from '@ohos/flutter_ohos/src/main/ets/util/Log'; import { TextureRegistry } from '@ohos/flutter_ohos'; import { VideoOutput } from './VideoOutput'; -import { TextureUpdateCallback } from './TextureUpdateCallback'; -import { common } from '@kit.AbilityKit'; +import { SizeData } from './Utils'; const TAG = "VideoOutputManager"; @@ -22,19 +21,20 @@ export class VideoOutputManager { this.textureRegistryReference = textureRegistryReference; } - async create(handle: number, textureUpdateCallback: TextureUpdateCallback): Promise { + create(handle: number): SizeData { Log.i(TAG, `com.alexmercerind.media_kit_video.VideoOutputManager.create: ${handle}`); if (!this.videoOutputs.has(handle)) { const textureId = this.textureRegistryReference.getTextureId(); const videoOutput = - new VideoOutput(this.textureRegistryReference, this.textureRegistryReference.registerTexture(textureId), - textureUpdateCallback); + new VideoOutput(this.textureRegistryReference, this.textureRegistryReference.registerTexture(textureId)); this.videoOutputs.set(handle, videoOutput); } + + return this.videoOutputs.get(handle)!.getSizeData(handle); } - async dispose(handle: number): Promise { + dispose(handle: number): void { Log.i(TAG, `com.alexmercerind.media_kit_video.VideoOutputManager.dispose: ${handle}`); if (this.videoOutputs.has(handle)) { @@ -43,11 +43,18 @@ export class VideoOutputManager { } } - async setSurfaceSize(handle: number, width: number, height: number): Promise { + disposeAll(): void { + this.videoOutputs.forEach((videoOutput: VideoOutput) => { + videoOutput.dispose(); + }); + this.videoOutputs.clear(); + } + + setSurfaceSize(handle: number, width: number, height: number): void { Log.i(TAG, `com.alexmercerind.media_kit_video.VideoOutputManager.setSurfaceSize: ${handle} ${width} ${height}`); if (this.videoOutputs.has(handle)) { this.videoOutputs.get(handle)?.setSurfaceSize(width, height); } } -} \ No newline at end of file +}