diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d155488..623c00a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -8,27 +8,37 @@ assignees: '' --- **Describe the bug** + A clear and concise description of what the bug is. **Expected behavior** + A clear and concise description of what you expected to happen. **Log** + Add -``` +```dart import 'package:logging/logging.dart'; import 'package:intl/intl.dart'; ``` add the following lines before `registerWith()` -``` +```dart Logger.root.level = Level.ALL; final df = DateFormat("HH:mm:ss.SSS"); Logger.root.onRecord.listen((record) { - print('${record.loggerName}.${record.level.name}: ${df.format(record.time)}: ${record.message}'); + debugPrint( + '${record.loggerName}.${record.level.name}: ${df.format(record.time)}: ${record.message}', + wrapWidth: 0x7FFFFFFFFFFFFFFF); }); ``` and + +
+log + ``` -Past log here +PASTE LOG HERE ``` +
diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..197a34a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,72 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "github-actions" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-patch"] + + # Android example apps. + - package-ecosystem: "gradle" + directories: + - /example/android/app + commit-message: + prefix: "[dependabot]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + labels: + - "autosubmit" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + # Android packages (excluding example apps). + - package-ecosystem: "gradle" + directories: + - /android + commit-message: + prefix: "[dependabot]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + labels: + - "autosubmit" + groups: + test-dependencies: + patterns: + - "androidx.test:*" + - "io.mockk:*" + - "junit:junit" + - "org.mockito:*" + - "org.robolectric:*" + kotlin-gradle-plugin: + patterns: + - "org.jetbrains.kotlin:kotlin-gradle-plugin" + gradle-plugin: + patterns: + - "com.android.tools.build:gradle" + androidx: + patterns: + - "androidx.annotation:annotation" + ignore: + - dependency-name: "com.android.tools.build:gradle" + update-types: ["version-update:semver-patch"] + - dependency-name: "junit:junit" + update-types: ["version-update:semver-patch"] + - dependency-name: "org.mockito:*" + update-types: ["version-update:semver-patch"] + - dependency-name: "androidx.test:*" + update-types: ["version-update:semver-patch"] + - dependency-name: "org.robolectric:*" + update-types: ["version-update:semver-patch"] + - dependency-name: "io.mockk:mockk:*" + update-types: ["version-update:semver-patch"] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d701202..a4add88 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,15 +18,26 @@ on: - cron: '0 6 * * 0' jobs: + CMake-tests: + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v6 + - run: cmake -S cmake/tests -B build/cmake-tests + - run: ctest -C Release --output-on-failure + working-directory: build/cmake-tests + macOS: - runs-on: macos-14 + runs-on: macos-15 defaults: run: working-directory: example strategy: fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: 'recursive' - uses: subosito/flutter-action@v2 @@ -41,20 +52,20 @@ jobs: mv build/macos/Build/Products/Release/fvp_example.app . cmake -E tar cvf fvp_example_macos.7z --format=7zip fvp_example.app - name: Upload - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: fvp-example-macOS path: example/fvp_example_macos.7z iOS: - runs-on: macos-14 + runs-on: macos-15 defaults: run: working-directory: example strategy: fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: 'recursive' - uses: subosito/flutter-action@v2 @@ -68,7 +79,7 @@ jobs: mv build/ios/iphoneos/Runner.app . cmake -E tar cvf fvp_example_ios.7z --format=7zip Runner.app - name: Upload - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: fvp-example-iOS path: example/fvp_example_ios.7z @@ -82,13 +93,10 @@ jobs: strategy: fail-fast: false matrix: - version: ['3.19.x', 'any'] # 3.19 for win7 - channel: ['stable', 'master'] - exclude: - - version: '3.19.x' - channel: 'master' + version: ['any'] # 3.19 for win7, but requires vs2019 on gha + channel: ['stable', 'beta'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: 'recursive' - uses: subosito/flutter-action@v2 @@ -102,7 +110,7 @@ jobs: - run: mv build/windows/x64/runner/Release . - run: cmake -E tar cvf fvp_example_windows-flutter-${{ matrix.version }}-${{ matrix.channel }}.7z --format=7zip Release - name: Upload - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: fvp-example-windows-flutter-${{ matrix.version }}-${{ matrix.channel }} path: | @@ -118,12 +126,12 @@ jobs: fail-fast: false matrix: version: ['3.10.x', 'any'] - channel: ['stable', 'master'] + channel: ['stable', 'beta'] exclude: - version: '3.10.x' - channel: 'master' + channel: 'beta' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: 'recursive' - uses: subosito/flutter-action@v2 @@ -141,11 +149,74 @@ jobs: - run: mv build/linux/x64/release/bundle . - run: cmake -E tar Jcvf fvp_example_linux-${{ matrix.channel }}-${{ matrix.version }}.tar.xz bundle - name: Upload - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: fvp-example-linux-${{ matrix.channel }}-${{ matrix.version }} path: example/fvp_example_linux*.tar.xz + + Linux-arm64: + if: true + runs-on: ubuntu-24.04-arm + defaults: + run: + working-directory: example + strategy: + fail-fast: false + matrix: + version: ['any'] + channel: ['master'] + steps: + - uses: actions/checkout@v6 + with: + submodules: 'recursive' + - run: | + sudo apt-get update -y + sudo apt-get install -y cmake clang ninja-build libgtk-3-dev libpulse-dev + - uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ matrix.version }} + channel: ${{ matrix.channel }} + cache: true +# - run: flutter config --enable-linux-desktop + - run: flutter doctor --verbose + - run: flutter pub get + - run: flutter build linux --verbose + - run: mv build/linux/arm64/release/bundle . + - run: cmake -E tar Jcvf fvp_example_linux-arm64-${{ matrix.channel }}-${{ matrix.version }}.tar.xz bundle + - name: Upload + uses: actions/upload-artifact@v6 + with: + name: fvp-example-linux-arm64-${{ matrix.channel }}-${{ matrix.version }} + path: example/fvp_example_linux*.tar.xz + + + Snap-arm64: + if: false + runs-on: ubuntu-24.04-arm + defaults: + run: + working-directory: example + steps: + - uses: actions/checkout@v6 + with: + submodules: 'recursive' + - name: Install dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y cmake clang ninja-build libgtk-3-dev libpulse-dev + sudo snap install flutter --classic + - run: flutter doctor --verbose + - run: flutter pub get + - run: flutter build linux --verbose + - run: mv build/linux/arm64/release/bundle . + - run: cmake -E tar Jcvf fvp_example_linux-arm64-snap.tar.xz bundle + - name: Upload + uses: actions/upload-artifact@v6 + with: + name: fvp-example-linux-arm64-snap + path: example/fvp_example_linux*.tar.xz + Snap: runs-on: ubuntu-latest defaults: @@ -156,10 +227,10 @@ jobs: matrix: target: [linux] #, apk] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: 'recursive' - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 if: ${{ matrix.target == 'apk' }} with: distribution: 'zulu' @@ -184,7 +255,7 @@ jobs: host: [ubuntu] version: ['3.22.x', 'any'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: 'recursive' - uses: subosito/flutter-action@v2 @@ -192,7 +263,7 @@ jobs: flutter-version: ${{ matrix.version }} channel: 'stable' cache: true - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: '17' @@ -201,7 +272,7 @@ jobs: - run: flutter build apk --verbose - run: mv ./build/app/outputs/apk/release/app-release.apk fvp_example_android.apk - name: Upload - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: fvp-example-android-${{ matrix.host }}-${{ matrix.version }} path: example/fvp_example_android.apk @@ -217,7 +288,7 @@ jobs: matrix: wasm: ['--wasm', ''] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: 'recursive' - uses: subosito/flutter-action@v2 @@ -226,4 +297,4 @@ jobs: cache: true - run: flutter doctor --verbose - run: flutter pub get - - run: flutter build web ${{ matrix.wasm }} --verbose \ No newline at end of file + - run: flutter build web ${{ matrix.wasm }} --verbose diff --git a/.github/workflows/pub.yml b/.github/workflows/pub.yml index a23275a..d732cf1 100644 --- a/.github/workflows/pub.yml +++ b/.github/workflows/pub.yml @@ -14,7 +14,7 @@ jobs: id-token: write # Required for authentication using OIDC runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: 'recursive' - uses: subosito/flutter-action@v2 @@ -24,5 +24,6 @@ jobs: - uses: dart-lang/setup-dart@v1 # Here you can insert custom steps you need # - run: dart tool/generate-code.dart + - run: dart format lib - name: Publish run: dart pub publish --force \ No newline at end of file diff --git a/.gitignore b/.gitignore index d12f841..3c8e8ff 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ mdk-sdk/ *.log *.pyc *.swp +*.part +.fvp-mdk-*/ .DS_Store .atom/ .buildlog/ @@ -30,6 +32,9 @@ migrate_working_dir/ # is commented out by default. #.vscode/ +# OHOS local configuration (machine-specific) +local.properties + # Flutter/Dart/Pub related # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. /pubspec.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 135632b..5f75bb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,109 @@ +# 0.37.3 + +* fix PrivacyInfo.xcprivacy for SPM +* upgrade libmdk for SPM +* fix new cmake build on windows + +# 0.37.2 + +* produce dynamic framework with SPM + +# 0.37.1 + +* support swift package manager + +# 0.37.0 + +* add Player.setPointMap() to support ROI rendering +* add Player.pointMap() to map between widget viewport coordinates and frame coordinates +* support asset for OHOS + +# 0.36.2 + +* fix MDK_KEY in release build + +## 0.36.1 + +* fix ohos plugin native lib name + +## 0.36.0 + +* new ohos platform +* rename native shared lib + +## 0.35.2 + +* log plugin version + +## 0.35.1 + +* fix linux arch for cmake 4.0+ +* fix flutter 3.38 android build + +## 0.35.0 + +* add Player.onSubtitleText +* allow hls & segments w/o or w/ any extension + +## 0.34.0 + +* add rockchip decoder +* deploy libmdk plugins + +## 0.33.1 + +* fix protocol whitelist +* enable dav1d, hap + +## 0.33.0 + +* improve test textureId/playerId only once +* add `VideoPlayerController.setProgram()` +* use highest resolution for rendering if stream have multiple programs/tracks +* enable wayland display +* linux/elinux: improve gl resource cleanup + +## 0.32.1 + +* compatible with video_player 2.10.0+ + +## 0.32.0 + +* support [embedded linux](https://github.com/sony/flutter-elinux), tested on x86 and arm, with x11, wayland and gbm. +* fix an fbo error on linux +* enable rkmpp decoder for rockchip + +## 0.31.2 + +* fix rpath on linux +* cmake: add env var FVP_DEPS_URL to download dependencies from github + +## 0.31.1 + +* fix link error if use cmake 4.0 on linux + +## 0.31.0 + +* fix crash when killing app on iOS +* fix crash when exiting app on linux + + +## 0.30.0 + +* `subtitleFontFile` option can be an http url +* ios: fix no sound in silent mode +* android: fix snapshot result is empty +* android: use `onSurfaceCleanup` for flutter >= 3.28 + +## 0.29.0 + +* not declared as video_player implementation in pubdec.yaml, fix conflicting with other implementations +* fix app does not launch when debugging +* android: use new onSurfaceAvailable() for flutter 3.27+ +* android: support add-to-app mode +* more VideoPlayerController extensions +* ios: cleanup when detaching from engine + ## 0.28.0 * mixWithOthers for iOS diff --git a/README.md b/README.md index 1a155d2..411959e 100644 --- a/README.md +++ b/README.md @@ -9,21 +9,22 @@ Prebuilt example can be download from artifacts of [github actions](https://gith project is create with `flutter create -t plugin --platforms=linux,macos,windows,android,ios -i objc -a java fvp` ## Features -- All platforms: Windows x64(including win7) and arm64, Linux x64 and arm64, macOS, iOS, Android(requires flutter > 3.19 because of minSdk 21). +- All platforms: Windows x64(including win7) and arm64, Linux x64 and arm64, [embedded linux](https://github.com/sony/flutter-elinux), macOS, iOS, Android(requires flutter > 3.19 because of minSdk 21) and HarmonyOS 5.0+. - You can choose official implementation or this plugin's -- Optimal render api: d3d11 for windows, metal for macOS/iOS, OpenGL for Linux and Android(Impeller support) +- Optimal render api: d3d11 for windows, metal for macOS/iOS, OpenGL for Linux, Android(Impeller support) and HarmonyOS. - Hardware decoders are enabled by default - Dolby Vision support on all platforms - Minimal code change for existing [Video Player](https://pub.dev/packages/video_player) apps -- Support most formats via FFmpeg demuxer and software decoders if not supported by gpu. You can use your own ffmpeg 4.0~7.0(or master branch) by removing bundled ffmpeg dynamic library. +- Support most formats via FFmpeg demuxer and software decoders if not supported by gpu. You can use your own ffmpeg 4.0~7.1(or master branch) by removing bundled ffmpeg dynamic library. - High performance. Lower cpu, gpu and memory load than libmpv based players. - Support audio without video +- HEVC, VP8 and VP9 transparent video - Small footprint. Only about 10MB size increase per cpu architecture(platform dependent). ## How to Use -- Add [fvp](https://pub.dev/packages/fvp) in your pubspec.yaml dependencies: `flutter pub add fvp` +- Add [fvp](https://pub.dev/packages/fvp) in your pubspec.yaml dependencies: `flutter pub add fvp`. Since flutter 3.27, fvp must be a direct dependency in your app's pubspec.yaml. - **(Optional)** Add 2 lines in your video_player examples. Without this step, this plugin will be used for video_player unsupported platforms(windows, linux), official implementation will be used otherwise. ```dart @@ -91,6 +92,16 @@ rm -rf {mac,i}os/Pods For other platforms, set environment var `FVP_DEPS_LATEST=1` and rebuilt, will upgrade to the latest sdk. If fvp is installed from pub.dev, run `flutter pub cache clean` is another option. +To use an immutable SDK release or mirror, set its base URL and expected SHA-256. The platform archive name, such as `mdk-sdk-android.7z`, is appended to the URL. + +```bash +FVP_DEPS_URL=https://example.com/mdk-sdk/releases/vX.Y.Z \ +FVP_DEPS_SHA256="replace-with-64-character-sha256" \ +flutter build apk +``` + +Replace the SHA-256 placeholder with the archive's 64-character digest. Both options can also be set as CMake cache variables. A non-empty CMake value takes precedence over the corresponding environment variable. When `FVP_DEPS_SHA256` is set, the downloaded archive is verified and the extracted SDK cache is reused only if its recorded checksum matches. `FVP_DEPS_LATEST` is ignored in this mode. + # Design - Playback control api in dart via ffi @@ -98,12 +109,21 @@ For other platforms, set environment var `FVP_DEPS_LATEST=1` and rebuilt, will u - Callbacks and events in C++ are notified by ReceivePort - Function with a one time callback is async and returns a future +# Enable Hardware Decoders for Embedded Linux +delete libffmpeg.so.* in your app bundle, which is copied from libmdk sdk. +- Raspberry Pi: install ffmpeg and system ffmpeg libraries with v4l2 acceleration will be used. +- Rockchip: build and install https://github.com/nyanmisaka/ffmpeg-rockchip . You may also need environment var `export GL_UBO=1` for arm mali driver on linux 6.x kernel to avoid gl driver bug. # Enable Subtitles -libass is required, and it's added to your app automatically for windows, macOS and android(remove ass.dll, libass.dylib and libass.so from mdk-sdk if you don't need it). For iOS, [download](https://sourceforge.net/projects/mdk-sdk/files/deps/dep.7z/download) and add `ass.framework` to your xcode project. For linux, system libass can be used, you may have to install manually via system package manager. +libass is required, and it's added to your app automatically for windows, macOS, ohos and android(remove ass.dll, libass.dylib and libass.so from mdk-sdk if you don't need it). For iOS, [download](https://sourceforge.net/projects/mdk-sdk/files/deps/dep.7z/download) and add `ass.framework` to your xcode project. For linux, system libass can be used, you may have to install manually via system package manager. -If required subtitle font is not found in the system(e.g. android), you can add [assets/subfont.ttf](https://github.com/mpv-android/mpv-android/raw/master/app/src/main/assets/subfont.ttf) in pubspec.yaml as the fallback. +If required subtitle font is not found in the system(e.g. android), you can add [assets/subfont.ttf](https://github.com/mpv-android/mpv-android/raw/master/app/src/main/assets/subfont.ttf) in pubspec.yaml assets as the fallback. Optionally you can also download the font file by fvp like this +```dart + fvp.registerWith(options: { + 'subtitleFontFile': 'https://github.com/mpv-android/mpv-android/raw/master/app/src/main/assets/subfont.ttf' + }); +``` # DO NOT use flutter-snap Flutter can be installed by snap, but it will add some [enviroment vars(`CPLUS_INCLUDE_PATH` and `LIBRARY_PATH`) which may break C++ compiler](https://github.com/canonical/flutter-snap/blob/main/env.sh#L15-L18). It's not recommended to use snap, althrough building for linux is [fixed](https://github.com/wang-bin/fvp/commit/567c68270ba16b95b1198ae58850707ae4ad7b22), but it's not possible for android. @@ -114,4 +134,4 @@ Flutter can be installed by snap, but it will add some [enviroment vars(`CPLUS_I ![fvp_win](https://user-images.githubusercontent.com/785206/248859525-920bdd51-6947-4a00-87b4-9c1a21a68d51.jpeg) ![fvp_win7](https://user-images.githubusercontent.com/785206/266754957-883d05c9-a057-4c1c-b824-0dc385a13f78.jpg) ![fvp_linux](https://user-images.githubusercontent.com/785206/248859533-ce2ad50b-2ead-43bb-bf25-6e2575c5ebe1.jpeg) -![fvp_macos](https://user-images.githubusercontent.com/785206/248859538-71de39a4-c5f0-4c8f-9920-d7dfc6cd0d9a.jpg) \ No newline at end of file +![fvp_macos](https://user-images.githubusercontent.com/785206/248859538-71de39a4-c5f0-4c8f-9920-d7dfc6cd0d9a.jpg) diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index dbbf890..b638b8e 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -1,7 +1,7 @@ # The Flutter tooling requires that developers have CMake 3.10 or later # installed. You should not increase this version, as doing so will cause # the plugin to fail to compile for some customers of the plugin. -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.17) # Project-level configuration. set(PROJECT_NAME "fvp") @@ -31,7 +31,8 @@ add_library(${PLUGIN_NAME} SHARED # between plugins. This should not be removed; any symbols that should be # exported should be explicitly exported with the FLUTTER_PLUGIN_EXPORT macro. set_target_properties(${PLUGIN_NAME} PROPERTIES - CXX_VISIBILITY_PRESET hidden) + CXX_VISIBILITY_PRESET hidden + OUTPUT_NAME "fvp") target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) # Source include directories and library dependencies. Add any plugin-specific @@ -39,7 +40,7 @@ target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) target_include_directories(${PLUGIN_NAME} INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") -#set_target_properties(fvp_plugin PROPERTIES +#set_target_properties(${PLUGIN_NAME} PROPERTIES # PUBLIC_HEADER fvp_plugin.h # OUTPUT_NAME "fvp_plugin" #) diff --git a/android/build.gradle b/android/build.gradle index 8f0b13a..b63aae5 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.5.0' // for compileSdk 34. default ndk 26.1.10909125 + classpath("com.android.tools.build:gradle:8.13.2") } } @@ -27,7 +27,7 @@ android { } // Bumping the plugin compileSdkVersion requires all clients of this plugin // to bump the version in their app. - compileSdk 34 + compileSdk = 36 // Use the NDK version // declared in /android/app/build.gradle file of the Flutter project. @@ -50,12 +50,12 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } defaultConfig { - minSdkVersion 21 // lower version error in ndk 26. requires flutter > 3.19 + minSdk = 21 // lower version error in ndk 26. requires flutter > 3.19 externalNativeBuild { cmake { @@ -93,11 +93,22 @@ def flutterSdkVersion = { def flutterSdkPath = properties.getProperty("flutter.sdk") if (flutterSdkPath == null) { flutterSdkPath = System.env.FLUTTER_ROOT // from flutter.groovy + + if (flutterSdkPath == null) { + // add-to-app local.properties file located at /.android/local.properties + file(project(":flutter").getProjectDir().getParent() + "/local.properties").withInputStream { properties.load(it) } + flutterSdkPath = properties.getProperty("flutter.sdk") + } } assert flutterSdkPath != null, "flutter.sdk not set in local.properties" - def version = file(flutterSdkPath + "/version") - assert version.exists(), "flutter version file not found" - return version.text.trim() + def versionFile = file(flutterSdkPath + '/bin/cache/flutter.version.json') + if (!versionFile.exists()) { + def version = file(flutterSdkPath + '/version') + assert version.exists(), 'flutter version file not found' + return version.text.trim() + } + def versionJson = new groovy.json.JsonSlurper().parseText(versionFile.text) + return versionJson.flutterVersion }() def preprocessJava(Map textMap) { @@ -122,4 +133,13 @@ if (flutterSdkVersionInt < 32400) { preprocessJava(['//// FLUTTER_3.24_BEGIN': '/\\*// FLUTTER_3.24_BEGIN-', '//// FLUTTER_3.24_END': '\\*/// FLUTTER_3.24_END-']) } else { preprocessJava(['/\\*// FLUTTER_3.24_BEGIN-': '//// FLUTTER_3.24_BEGIN', '\\*/// FLUTTER_3.24_END-': '//// FLUTTER_3.24_END']) + /// rename onSurfaceAvailable to onSurfaceCreated if flutterSdkVersionInt < 32700 + if (flutterSdkVersionInt < 32700) { + println 'rename onSurfaceAvailable to onSurfaceCreated' + preprocessJava(['onSurfaceAvailable': 'onSurfaceCreated']) + } + if (flutterSdkVersionInt < 32800) { + println 'rename onSurfaceCleanup to onSurfaceDestroyed' + preprocessJava(['onSurfaceCleanup': 'onSurfaceDestroyed']) + } } diff --git a/android/fvp_plugin.cpp b/android/fvp_plugin.cpp index e7cdcbb..d573a62 100644 --- a/android/fvp_plugin.cpp +++ b/android/fvp_plugin.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023-2024 WangBin + * Copyright (c) 2023-2026 WangBin */ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -27,6 +27,7 @@ class TexturePlayer final : public mdk::Player int width = 0; int height = 0; jobject surface = nullptr; + void* vo_opaque = nullptr; // can change by TextureRegistry.SurfaceProducer.Callback private: }; @@ -36,18 +37,16 @@ static unordered_map> players; extern "C" { JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) { - + clog << "JNI_OnLoad" << endl; + mdk::javaVM(vm); mdk::SetGlobalOption("profiler.gpu", 1); - clog << "JNI_OnLoad" << endl; JNIEnv *env = nullptr; if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK || !env) { clog << "GetEnv for JNI_VERSION_1_4 failed" << endl; return -1; } - mdk::SetGlobalOption("JavaVM", vm); - mdk::SetGlobalOption("MDK_KEY", "980B9623276F746C5FBB5EC5120D4A99A0B58B635592EAEE41F6817FDF3B28B96AC4A49866257726C19B246863B5ADAF5D17464E86D72A90634E8AE8418F810967F469DCD8908B93A044A13AEDF2B566E0B5810523E2B59E2D83E616B1B807B66253E1607A79BC86AEDE1AEF46F79AA60F36BE44DDEE47B84E165AF2788F8109"); return JNI_VERSION_1_4; } @@ -82,6 +81,7 @@ Java_com_mediadevkit_fvp_FvpPlugin_nativeSetSurface(JNIEnv *env, jobject thiz, j player->setProperty("video.decoder", "surface=" + std::to_string((intptr_t)player->surface)); } else { player->updateNativeSurface(surface, w, h); + player->vo_opaque = surface; } players[tex_id] = player; } @@ -111,4 +111,17 @@ MdkIsEmulator() if (strstr(v, "emulator")) return true; return false; +} + +extern "C" +JNIEXPORT void* JNICALL +MdkGetPlayerVid(int64_t tex_id) +{ + if (tex_id < 0) + return nullptr; + if (const auto it = players.find(tex_id); it != players.end()) { + const auto& player = it->second; + return player->vo_opaque; + } + return nullptr; } \ No newline at end of file diff --git a/android/src/main/java/com/mediadevkit/fvp/FvpPlugin.java b/android/src/main/java/com/mediadevkit/fvp/FvpPlugin.java index a32892f..f599988 100644 --- a/android/src/main/java/com/mediadevkit/fvp/FvpPlugin.java +++ b/android/src/main/java/com/mediadevkit/fvp/FvpPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023-2024 WangBin + * Copyright (c) 2023-2026 WangBin */ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -93,8 +93,8 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { sp.setCallback( new TextureRegistry.SurfaceProducer.Callback() { @Override - public void onSurfaceCreated() { - Log.d("FvpPlugin", "SurfaceProducer.onSurfaceCreated for textureId " + texId); + public void onSurfaceAvailable() { + Log.d("FvpPlugin", "SurfaceProducer.onSurfaceAvailable for textureId " + texId); final Surface newSurface = sp.getSurface(); surfaces.put(texId, newSurface); // will do nothing if same surface @@ -102,8 +102,8 @@ public void onSurfaceCreated() { } @Override - public void onSurfaceDestroyed() { - Log.d("FvpPlugin", "SurfaceProducer.onSurfaceDestroyed for textureId " + texId); + public void onSurfaceCleanup() { + Log.d("FvpPlugin", "SurfaceProducer.onSurfaceCleanup for textureId " + texId); textures.remove(texId); nativeSetSurface(handle, texId, null, 0, 0, tunnel); } @@ -154,9 +154,10 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { static { try { - System.loadLibrary("fvp_plugin"); + System.loadLibrary("mdk"); + System.loadLibrary("fvp"); } catch (UnsatisfiedLinkError e) { - Log.w("FvpPlugin", "static initializer: loadLibrary fvp_plugin error: " + e); + Log.w("FvpPlugin", "static initializer: loadLibrary fvp error: " + e); } } } diff --git a/cmake/deps.cmake b/cmake/deps.cmake index 7fb50a6..be0e6d8 100644 --- a/cmake/deps.cmake +++ b/cmake/deps.cmake @@ -1,58 +1,318 @@ +if(NOT DEFINED FVP_DEPS_URL) + set(FVP_DEPS_URL "" CACHE STRING "Base URL of MDK SDK archives") +endif() +if(NOT DEFINED FVP_DEPS_SHA256) + set(FVP_DEPS_SHA256 "" CACHE STRING "Expected SHA-256 of the MDK SDK archive") +endif() + + +function(fvp_version) + if(NOT DEFINED CMAKE_CURRENT_FUNCTION_LIST_DIR) + message(WARNING "CMAKE_CURRENT_FUNCTION_LIST_DIR not defined") + set(CMAKE_CURRENT_FUNCTION_LIST_DIR ${CMAKE_CURRENT_LIST_DIR}) + endif() + set(PUBSPEC_FILE "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../pubspec.yaml") + if(NOT EXISTS ${PUBSPEC_FILE}) + message(FATAL_ERROR "pubspec.yaml not found: ${PUBSPEC_FILE}") + endif() + file(READ ${PUBSPEC_FILE} PUBSPEC_CONTENTS) + string(REGEX MATCH "version:[ \t]*([0-9]+\\.[0-9]+\\.[0-9]+|[0-9]+\\.[0-9]+|[^ \t\n\r]+)" MATCHED_LINE "${PUBSPEC_CONTENTS}") + + if(MATCHED_LINE) + string(REGEX REPLACE "version:[ \t]*" "" FVP_VERSION "${MATCHED_LINE}") + message(STATUS "Found fvp version: ${FVP_VERSION}") + set(VERSION_HEADER_FILE "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../lib/src/version.h") + file(WRITE ${VERSION_HEADER_FILE} "#pragma once\n#define FVP_VERSION \"${FVP_VERSION}\"\n") + else() + message(WARNING "No version line found in file") + endif() +endfunction(fvp_version) + + +function(_fvp_resolve_deps_options DEFAULT_URL OUT_URL OUT_SHA256) + set(DEPS_URL "${FVP_DEPS_URL}") + string(STRIP "${DEPS_URL}" DEPS_URL) + if("${DEPS_URL}" STREQUAL "" AND "$ENV{FVP_DEPS_URL}" MATCHES "^http") + set(DEPS_URL "$ENV{FVP_DEPS_URL}") + string(STRIP "${DEPS_URL}" DEPS_URL) + endif() + if("${DEPS_URL}" STREQUAL "") + set(DEPS_URL "${DEFAULT_URL}") + string(STRIP "${DEPS_URL}" DEPS_URL) + endif() + string(REGEX REPLACE "/+$" "" DEPS_URL "${DEPS_URL}") + + set(DEPS_SHA256 "${FVP_DEPS_SHA256}") + string(STRIP "${DEPS_SHA256}" DEPS_SHA256) + if("${DEPS_SHA256}" STREQUAL "") + set(DEPS_SHA256 "$ENV{FVP_DEPS_SHA256}") + string(STRIP "${DEPS_SHA256}" DEPS_SHA256) + endif() + if(NOT "${DEPS_SHA256}" STREQUAL "") + string(LENGTH "${DEPS_SHA256}" DEPS_SHA256_LENGTH) + if(NOT DEPS_SHA256_LENGTH EQUAL 64 OR NOT "${DEPS_SHA256}" MATCHES "^[0-9A-Fa-f]+$") + message(FATAL_ERROR "FVP_DEPS_SHA256 must be a 64-character hexadecimal SHA-256 value") + endif() + string(TOLOWER "${DEPS_SHA256}" DEPS_SHA256) + endif() + + set(${OUT_URL} "${DEPS_URL}" PARENT_SCOPE) + set(${OUT_SHA256} "${DEPS_SHA256}" PARENT_SCOPE) +endfunction() + + +function(_fvp_download_file URL OUTPUT EXPECTED_SHA256) + get_filename_component(OUTPUT_DIR "${OUTPUT}" DIRECTORY) + file(MAKE_DIRECTORY "${OUTPUT_DIR}") + set(PARTIAL_OUTPUT "${OUTPUT}.part") + file(REMOVE "${PARTIAL_OUTPUT}") + + message(STATUS "Downloading ${URL}") + file(DOWNLOAD "${URL}" "${PARTIAL_OUTPUT}" + SHOW_PROGRESS + STATUS DOWNLOAD_STATUS + ) + list(GET DOWNLOAD_STATUS 0 DOWNLOAD_STATUS_CODE) + list(GET DOWNLOAD_STATUS 1 DOWNLOAD_STATUS_MESSAGE) + if(NOT DOWNLOAD_STATUS_CODE EQUAL 0) + file(REMOVE "${PARTIAL_OUTPUT}") + message(FATAL_ERROR "Failed to download ${URL}: ${DOWNLOAD_STATUS_MESSAGE}") + endif() + + if(NOT "${EXPECTED_SHA256}" STREQUAL "") + file(SHA256 "${PARTIAL_OUTPUT}" DOWNLOADED_SHA256) + string(TOLOWER "${DOWNLOADED_SHA256}" DOWNLOADED_SHA256) + if(NOT "${DOWNLOADED_SHA256}" STREQUAL "${EXPECTED_SHA256}") + file(REMOVE "${PARTIAL_OUTPUT}") + message(FATAL_ERROR + "SHA-256 mismatch for ${URL}\n" + " expected: ${EXPECTED_SHA256}\n" + " actual: ${DOWNLOADED_SHA256}" + ) + endif() + endif() + + file(RENAME "${PARTIAL_OUTPUT}" "${OUTPUT}") +endfunction() + + +function(_fvp_prepare_mdk_sdk SDK_URL SDK_ARCHIVE EXTRACT_ROOT EXPECTED_SHA256 UPDATE_LATEST) + get_filename_component(REAL_EXTRACT_ROOT "${EXTRACT_ROOT}" REALPATH) + file(MAKE_DIRECTORY "${REAL_EXTRACT_ROOT}") + file(LOCK "${REAL_EXTRACT_ROOT}/fvp-deps.lock" + GUARD FUNCTION + TIMEOUT 300 + RESULT_VARIABLE LOCK_RESULT + ) + if(NOT "${LOCK_RESULT}" STREQUAL "0") + message(FATAL_ERROR "Failed to lock the MDK SDK cache: ${LOCK_RESULT}") + endif() + + set(SDK_DIR "${REAL_EXTRACT_ROOT}/mdk-sdk") + set(SDK_MARKER "${SDK_DIR}/lib/cmake/FindMDK.cmake") + set(SDK_STAMP "${SDK_DIR}/.fvp-deps.sha256") + set(SDK_BACKUP_DIR "${REAL_EXTRACT_ROOT}/.fvp-mdk-backup") + + if(EXISTS "${SDK_BACKUP_DIR}") + if(EXISTS "${SDK_DIR}") + file(REMOVE_RECURSE "${SDK_BACKUP_DIR}") + else() + execute_process( + COMMAND "${CMAKE_COMMAND}" -E rename "${SDK_BACKUP_DIR}" "${SDK_DIR}" + ERROR_VARIABLE RECOVERY_ERROR + RESULT_VARIABLE RECOVERY_RESULT + ) + if(NOT RECOVERY_RESULT EQUAL 0) + message(FATAL_ERROR "Failed to recover the cached mdk-sdk: ${RECOVERY_ERROR}") + endif() + endif() + endif() + + if(NOT "${EXPECTED_SHA256}" STREQUAL "" AND EXISTS "${SDK_MARKER}" AND EXISTS "${SDK_STAMP}") + file(READ "${SDK_STAMP}" CACHED_SHA256) + string(STRIP "${CACHED_SHA256}" CACHED_SHA256) + string(TOLOWER "${CACHED_SHA256}" CACHED_SHA256) + if("${CACHED_SHA256}" STREQUAL "${EXPECTED_SHA256}") + message(STATUS "Using cached mdk-sdk (SHA256: ${CACHED_SHA256})") + return() + endif() + endif() + + set(FORCE_DOWNLOAD OFF) + if("${EXPECTED_SHA256}" STREQUAL "" AND UPDATE_LATEST) + if(EXISTS "${SDK_ARCHIVE}") + message(STATUS "Downloading latest MD5") + _fvp_download_file("${SDK_URL}.md5" "${SDK_ARCHIVE}.md5" "") + file(READ "${SDK_ARCHIVE}.md5" LATEST_MD5) + string(STRIP "${LATEST_MD5}" LATEST_MD5) + file(MD5 "${SDK_ARCHIVE}" ARCHIVE_MD5) + message(STATUS "MD5 [${ARCHIVE_MD5}] => [${LATEST_MD5}]") + if(NOT "${LATEST_MD5}" STREQUAL "${ARCHIVE_MD5}") + set(FORCE_DOWNLOAD ON) + endif() + else() + set(FORCE_DOWNLOAD ON) + endif() + endif() + + if("${EXPECTED_SHA256}" STREQUAL "" AND NOT FORCE_DOWNLOAD AND EXISTS "${SDK_MARKER}") + set(CAN_REUSE_SDK ON) + if(UPDATE_LATEST) + if(EXISTS "${SDK_ARCHIVE}" AND EXISTS "${SDK_STAMP}") + file(SHA256 "${SDK_ARCHIVE}" ARCHIVE_SHA256) + file(READ "${SDK_STAMP}" CACHED_SHA256) + string(STRIP "${CACHED_SHA256}" CACHED_SHA256) + string(TOLOWER "${CACHED_SHA256}" CACHED_SHA256) + if(NOT "${ARCHIVE_SHA256}" STREQUAL "${CACHED_SHA256}") + message(STATUS "Cached mdk-sdk does not match the downloaded archive; re-extracting") + set(CAN_REUSE_SDK OFF) + endif() + else() + message(STATUS "Cached mdk-sdk checksum is unavailable; re-extracting") + set(CAN_REUSE_SDK OFF) + endif() + endif() + if(CAN_REUSE_SDK AND EXISTS "${SDK_STAMP}") + file(READ "${SDK_STAMP}" CACHED_SHA256) + string(STRIP "${CACHED_SHA256}" CACHED_SHA256) + message(STATUS "Using cached mdk-sdk (SHA256: ${CACHED_SHA256})") + elseif(CAN_REUSE_SDK) + message(STATUS "Using cached mdk-sdk (checksum unavailable)") + endif() + if(CAN_REUSE_SDK) + return() + endif() + endif() + + set(NEED_DOWNLOAD "${FORCE_DOWNLOAD}") + if(EXISTS "${SDK_ARCHIVE}" AND NOT NEED_DOWNLOAD) + file(SHA256 "${SDK_ARCHIVE}" ARCHIVE_SHA256) + string(TOLOWER "${ARCHIVE_SHA256}" ARCHIVE_SHA256) + if(NOT "${EXPECTED_SHA256}" STREQUAL "" AND NOT "${ARCHIVE_SHA256}" STREQUAL "${EXPECTED_SHA256}") + message(STATUS "Cached MDK SDK archive does not match FVP_DEPS_SHA256") + set(NEED_DOWNLOAD ON) + endif() + else() + set(NEED_DOWNLOAD ON) + endif() + + if(NEED_DOWNLOAD) + _fvp_download_file("${SDK_URL}" "${SDK_ARCHIVE}" "${EXPECTED_SHA256}") + file(SHA256 "${SDK_ARCHIVE}" ARCHIVE_SHA256) + string(TOLOWER "${ARCHIVE_SHA256}" ARCHIVE_SHA256) + endif() + message(STATUS "MDK SDK archive SHA256: ${ARCHIVE_SHA256}") + + set(TEMP_EXTRACT_DIR "${REAL_EXTRACT_ROOT}/.fvp-mdk-extract") + file(REMOVE_RECURSE "${TEMP_EXTRACT_DIR}") + file(MAKE_DIRECTORY "${TEMP_EXTRACT_DIR}") + execute_process( + COMMAND "${CMAKE_COMMAND}" -E tar "xvf" "${SDK_ARCHIVE}" + WORKING_DIRECTORY "${TEMP_EXTRACT_DIR}" + OUTPUT_QUIET + ERROR_VARIABLE EXTRACT_ERROR + RESULT_VARIABLE EXTRACT_RESULT + ) + if(NOT EXTRACT_RESULT EQUAL 0 OR NOT EXISTS "${TEMP_EXTRACT_DIR}/mdk-sdk/lib/cmake/FindMDK.cmake") + file(REMOVE_RECURSE "${TEMP_EXTRACT_DIR}") + if("${EXPECTED_SHA256}" STREQUAL "") + file(REMOVE "${SDK_ARCHIVE}") + endif() + message(FATAL_ERROR + "Failed to extract mdk-sdk from ${SDK_ARCHIVE}: ${EXTRACT_ERROR}" + ) + endif() + file(WRITE "${TEMP_EXTRACT_DIR}/mdk-sdk/.fvp-deps.sha256" "${ARCHIVE_SHA256}\n") + + if(EXISTS "${SDK_DIR}") + execute_process( + COMMAND "${CMAKE_COMMAND}" -E rename "${SDK_DIR}" "${SDK_BACKUP_DIR}" + ERROR_VARIABLE BACKUP_ERROR + RESULT_VARIABLE BACKUP_RESULT + ) + if(NOT BACKUP_RESULT EQUAL 0) + file(REMOVE_RECURSE "${TEMP_EXTRACT_DIR}") + message(FATAL_ERROR "Failed to back up the cached mdk-sdk: ${BACKUP_ERROR}") + endif() + endif() + execute_process( + COMMAND "${CMAKE_COMMAND}" -E rename "${TEMP_EXTRACT_DIR}/mdk-sdk" "${SDK_DIR}" + ERROR_VARIABLE INSTALL_ERROR + RESULT_VARIABLE INSTALL_RESULT + ) + if(NOT INSTALL_RESULT EQUAL 0) + if(EXISTS "${SDK_BACKUP_DIR}") + execute_process( + COMMAND "${CMAKE_COMMAND}" -E rename "${SDK_BACKUP_DIR}" "${SDK_DIR}" + ERROR_VARIABLE RESTORE_ERROR + RESULT_VARIABLE RESTORE_RESULT + ) + if(NOT RESTORE_RESULT EQUAL 0) + file(REMOVE_RECURSE "${TEMP_EXTRACT_DIR}") + message(FATAL_ERROR + "Failed to install the extracted mdk-sdk: ${INSTALL_ERROR}\n" + "The previous SDK is preserved at ${SDK_BACKUP_DIR}, but could not be restored: ${RESTORE_ERROR}" + ) + endif() + endif() + file(REMOVE_RECURSE "${TEMP_EXTRACT_DIR}") + message(FATAL_ERROR "Failed to install the extracted mdk-sdk: ${INSTALL_ERROR}") + endif() + file(REMOVE_RECURSE "${SDK_BACKUP_DIR}") + file(REMOVE_RECURSE "${TEMP_EXTRACT_DIR}") + message(STATUS "Extracted mdk-sdk (SHA256: ${ARCHIVE_SHA256})") +endfunction() + + macro(fvp_setup_deps) + fvp_version() if(WIN32) - set(MDK_SDK_PKG mdk-sdk-windows-desktop-vs2022.7z) + set(MDK_SDK_PKG mdk-sdk-windows.7z) if(CMAKE_CXX_COMPILER_ARCHITECTURE_ID MATCHES "[xX]64") # msvc - set(MDK_SDK_PKG mdk-sdk-windows-desktop-vs2022-x64.7z) + set(MDK_SDK_PKG mdk-sdk-windows-x64.7z) endif() elseif(ANDROID) set(MDK_SDK_PKG mdk-sdk-android.7z) + elseif(CMAKE_SYSTEM_NAME MATCHES "OHOS") + set(MDK_SDK_PKG mdk-sdk-ohos.7z) elseif(LINUX OR CMAKE_SYSTEM_NAME MATCHES "Linux") set(MDK_SDK_PKG mdk-sdk-linux.tar.xz) - if(CMAKE_SYSTEM_PROCESSOR MATCHES "[xX].*64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "[aA][mM][dD]64") + if(CMAKE_C_COMPILER_ARCHITECTURE_ID MATCHES "[xX].*64") + set(MDK_SDK_PKG mdk-sdk-linux-x64.tar.xz) + elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "[xX].*64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "[aA][mM][dD]64") set(MDK_SDK_PKG mdk-sdk-linux-x64.tar.xz) endif() else() endif() - set(MDK_SDK_URL https://sourceforge.net/projects/mdk-sdk/files/nightly/${MDK_SDK_PKG}) - set(MDK_SDK_SAVE "${CMAKE_CURRENT_SOURCE_DIR}/${MDK_SDK_PKG}") + _fvp_resolve_deps_options( + "https://sourceforge.net/projects/mdk-sdk/files/nightly" + FVP_DEPS_URL_EFFECTIVE + FVP_DEPS_SHA256_EFFECTIVE + ) + set(MDK_SDK_URL "${FVP_DEPS_URL_EFFECTIVE}/${MDK_SDK_PKG}") + get_filename_component(MDK_SDK_EXTRACT_DIR "${CMAKE_CURRENT_SOURCE_DIR}" REALPATH) + set(MDK_SDK_SAVE "${MDK_SDK_EXTRACT_DIR}/${MDK_SDK_PKG}") + message(STATUS "MDK SDK URL: ${MDK_SDK_URL}") + if(NOT "${FVP_DEPS_SHA256_EFFECTIVE}" STREQUAL "") + message(STATUS "MDK SDK expected SHA256: ${FVP_DEPS_SHA256_EFFECTIVE}") + endif() - set(DOWNLOAD_MDK_SDK OFF) - message("FVP_DEPS_LATEST=$ENV{FVP_DEPS_LATEST}") + set(UPDATE_MDK_SDK_LATEST OFF) + set(FVP_DEPS_LATEST_EFFECTIVE "$ENV{FVP_DEPS_LATEST}") + message(STATUS "FVP_DEPS_LATEST=${FVP_DEPS_LATEST_EFFECTIVE}") # TODO: download from github option FVP_DEPS_LATEST_RELEASE=1 - if($ENV{FVP_DEPS_LATEST}) - if(EXISTS ${MDK_SDK_SAVE}) - message("Downloading latest md5") - file(DOWNLOAD ${MDK_SDK_URL}.md5 ${MDK_SDK_SAVE}.md5 SHOW_PROGRESS) - file(READ ${MDK_SDK_SAVE}.md5 MDK_SDK_MD5_LATEST) - string(STRIP "${MDK_SDK_MD5_LATEST}" MDK_SDK_MD5_LATEST) - file(MD5 ${MDK_SDK_SAVE} MDK_SDK_MD5_SAVE) - message("md5 [${MDK_SDK_MD5_SAVE}] => [${MDK_SDK_MD5_LATEST}]") - if(NOT MDK_SDK_MD5_LATEST STREQUAL MDK_SDK_MD5_SAVE) - set(DOWNLOAD_MDK_SDK ON) - endif() - else() - set(DOWNLOAD_MDK_SDK ON) - endif() + if(NOT "${FVP_DEPS_SHA256_EFFECTIVE}" STREQUAL "" AND FVP_DEPS_LATEST_EFFECTIVE) + message(STATUS "Ignoring FVP_DEPS_LATEST because FVP_DEPS_SHA256 is set") + elseif(FVP_DEPS_LATEST_EFFECTIVE) + set(UPDATE_MDK_SDK_LATEST ON) endif() - if(DOWNLOAD_MDK_SDK OR NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/mdk-sdk/lib/cmake/FindMDK.cmake) - if(DOWNLOAD_MDK_SDK OR NOT EXISTS ${MDK_SDK_SAVE}) - message("Downloading mdk-sdk from ${MDK_SDK_URL}") - file(DOWNLOAD ${MDK_SDK_URL} ${MDK_SDK_SAVE} SHOW_PROGRESS) - file(MD5 ${MDK_SDK_SAVE} MDK_SDK_MD5_SAVE) - message("MDK_SDK_MD5_SAVE: ${MDK_SDK_MD5_SAVE}") - endif() - execute_process( - COMMAND ${CMAKE_COMMAND} -E tar "xvf" ${MDK_SDK_SAVE} # "--format=7zip" - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - OUTPUT_STRIP_TRAILING_WHITESPACE - RESULT_VARIABLE EXTRACT_RET - ) - # EXTRACT_RET is 0 even for empty files - if(NOT EXTRACT_RET EQUAL 0 OR NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/mdk-sdk/lib/cmake/FindMDK.cmake) - file(REMOVE ${MDK_SDK_SAVE}) - message(FATAL_ERROR "Failed to extract mdk-sdk. You can download manually from ${MDK_SDK_URL} and extract to ${CMAKE_CURRENT_SOURCE_DIR}") - endif() - endif() - include(${CMAKE_CURRENT_SOURCE_DIR}/mdk-sdk/lib/cmake/FindMDK.cmake) + _fvp_prepare_mdk_sdk( + "${MDK_SDK_URL}" + "${MDK_SDK_SAVE}" + "${MDK_SDK_EXTRACT_DIR}" + "${FVP_DEPS_SHA256_EFFECTIVE}" + "${UPDATE_MDK_SDK_LATEST}" + ) + include("${CMAKE_CURRENT_SOURCE_DIR}/mdk-sdk/lib/cmake/FindMDK.cmake") endmacro() diff --git a/cmake/tests/CMakeLists.txt b/cmake/tests/CMakeLists.txt new file mode 100644 index 0000000..a8fd84f --- /dev/null +++ b/cmake/tests/CMakeLists.txt @@ -0,0 +1,92 @@ +cmake_minimum_required(VERSION 3.15) + +project(fvp_deps_tests NONE) + +enable_testing() + +function(create_mdk_fixture NAME PAYLOAD OUT_ARCHIVE OUT_SHA256) + set(FIXTURE_ROOT "${CMAKE_CURRENT_BINARY_DIR}/fixtures/${NAME}") + set(FIXTURE_ARCHIVE "${CMAKE_CURRENT_BINARY_DIR}/fixtures/${NAME}.tar") + file(REMOVE_RECURSE "${FIXTURE_ROOT}") + file(MAKE_DIRECTORY "${FIXTURE_ROOT}/mdk-sdk/lib/cmake") + file(WRITE "${FIXTURE_ROOT}/mdk-sdk/lib/cmake/FindMDK.cmake" + "set(FVP_TEST_FIND_MDK TRUE)\n" + ) + file(WRITE "${FIXTURE_ROOT}/mdk-sdk/payload.txt" "${PAYLOAD}\n") + execute_process( + COMMAND "${CMAKE_COMMAND}" -E tar "cf" "${FIXTURE_ARCHIVE}" "--format=gnutar" "mdk-sdk" + WORKING_DIRECTORY "${FIXTURE_ROOT}" + RESULT_VARIABLE ARCHIVE_RESULT + ERROR_VARIABLE ARCHIVE_ERROR + ) + if(NOT ARCHIVE_RESULT EQUAL 0) + message(FATAL_ERROR "Failed to create ${NAME} fixture: ${ARCHIVE_ERROR}") + endif() + file(SHA256 "${FIXTURE_ARCHIVE}" FIXTURE_SHA256) + file(MD5 "${FIXTURE_ARCHIVE}" FIXTURE_MD5) + file(WRITE "${FIXTURE_ARCHIVE}.md5" "${FIXTURE_MD5}\n") + set(${OUT_ARCHIVE} "${FIXTURE_ARCHIVE}" PARENT_SCOPE) + set(${OUT_SHA256} "${FIXTURE_SHA256}" PARENT_SCOPE) +endfunction() + +create_mdk_fixture("v1" "fixture-v1" FIXTURE_V1_ARCHIVE FIXTURE_V1_SHA256) +create_mdk_fixture("v2" "fixture-v2" FIXTURE_V2_ARCHIVE FIXTURE_V2_SHA256) + +if(WIN32) + set(FILE_URL_PREFIX "file:///") +else() + set(FILE_URL_PREFIX "file://") +endif() +set(FIXTURE_V1_URL "${FILE_URL_PREFIX}${FIXTURE_V1_ARCHIVE}") +set(FIXTURE_V2_URL "${FILE_URL_PREFIX}${FIXTURE_V2_ARCHIVE}") + +set(INTEGRATION_RELEASE_DIR "${CMAKE_CURRENT_BINARY_DIR}/integration-release") +set(INTEGRATION_ROOT "${CMAKE_CURRENT_BINARY_DIR}/integration root") +set(INTEGRATION_BAD_ROOT "${CMAKE_CURRENT_BINARY_DIR}/integration bad root") +file(MAKE_DIRECTORY "${INTEGRATION_RELEASE_DIR}" "${INTEGRATION_ROOT}" "${INTEGRATION_BAD_ROOT}") +configure_file( + "${FIXTURE_V1_ARCHIVE}" + "${INTEGRATION_RELEASE_DIR}/mdk-sdk-linux-x64.tar.xz" + COPYONLY +) +configure_file( + "${FIXTURE_V1_ARCHIVE}" + "${INTEGRATION_RELEASE_DIR}/mdk-sdk-windows.7z" + COPYONLY +) + +add_test( + NAME fvp_deps + COMMAND "${CMAKE_COMMAND}" + "-DDEPS_FILE=${CMAKE_CURRENT_LIST_DIR}/../deps.cmake" + "-DINVOKE_FILE=${CMAKE_CURRENT_LIST_DIR}/invoke_fetch.cmake" + "-DTEST_ROOT=${CMAKE_CURRENT_BINARY_DIR}/test root" + "-DFIXTURE_V1_URL=${FIXTURE_V1_URL}" + "-DFIXTURE_V1_SHA256=${FIXTURE_V1_SHA256}" + "-DFIXTURE_V2_URL=${FIXTURE_V2_URL}" + "-DFIXTURE_V2_SHA256=${FIXTURE_V2_SHA256}" + -P "${CMAKE_CURRENT_LIST_DIR}/deps_test.cmake" +) + +add_test( + NAME fvp_deps_integration + COMMAND "${CMAKE_COMMAND}" + "-DDEPS_FILE=${CMAKE_CURRENT_LIST_DIR}/../deps.cmake" + "-DFVP_DEPS_URL=${FILE_URL_PREFIX}${INTEGRATION_RELEASE_DIR}" + "-DFVP_DEPS_SHA256=${FIXTURE_V1_SHA256}" + -P "${CMAKE_CURRENT_LIST_DIR}/macro_test.cmake" +) +set_tests_properties(fvp_deps_integration PROPERTIES + ENVIRONMENT "FVP_DEPS_LATEST=1" + WORKING_DIRECTORY "${INTEGRATION_ROOT}" +) + +add_test( + NAME fvp_deps_integration_bad_sha + COMMAND "${CMAKE_COMMAND}" + "-DDEPS_FILE=${CMAKE_CURRENT_LIST_DIR}/../deps.cmake" + "-DMACRO_TEST_FILE=${CMAKE_CURRENT_LIST_DIR}/macro_test.cmake" + "-DSDK_BASE_URL=${FILE_URL_PREFIX}${INTEGRATION_RELEASE_DIR}" + "-DTEST_ROOT=${INTEGRATION_BAD_ROOT}" + -P "${CMAKE_CURRENT_LIST_DIR}/macro_bad_sha_test.cmake" +) diff --git a/cmake/tests/deps_test.cmake b/cmake/tests/deps_test.cmake new file mode 100644 index 0000000..df422ca --- /dev/null +++ b/cmake/tests/deps_test.cmake @@ -0,0 +1,238 @@ +cmake_minimum_required(VERSION 3.15) + +foreach(REQUIRED_VARIABLE + DEPS_FILE + INVOKE_FILE + TEST_ROOT + FIXTURE_V1_URL + FIXTURE_V1_SHA256 + FIXTURE_V2_URL + FIXTURE_V2_SHA256) + if(NOT DEFINED ${REQUIRED_VARIABLE}) + message(FATAL_ERROR "${REQUIRED_VARIABLE} is required") + endif() +endforeach() + +include("${DEPS_FILE}") + +function(assert_equal ACTUAL EXPECTED DESCRIPTION) + if(NOT "${ACTUAL}" STREQUAL "${EXPECTED}") + message(FATAL_ERROR + "${DESCRIPTION}\n" + " expected: ${EXPECTED}\n" + " actual: ${ACTUAL}" + ) + endif() +endfunction() + +function(assert_exists PATH DESCRIPTION) + if(NOT EXISTS "${PATH}") + message(FATAL_ERROR "${DESCRIPTION}: ${PATH}") + endif() +endfunction() + +function(assert_not_exists PATH DESCRIPTION) + if(EXISTS "${PATH}") + message(FATAL_ERROR "${DESCRIPTION}: ${PATH}") + endif() +endfunction() + +function(assert_payload ROOT EXPECTED) + set(PAYLOAD_FILE "${ROOT}/mdk-sdk/payload.txt") + assert_exists("${PAYLOAD_FILE}" "MDK SDK payload is missing") + file(READ "${PAYLOAD_FILE}" ACTUAL_PAYLOAD) + string(STRIP "${ACTUAL_PAYLOAD}" ACTUAL_PAYLOAD) + assert_equal("${ACTUAL_PAYLOAD}" "${EXPECTED}" "Unexpected MDK SDK payload") +endfunction() + +file(REMOVE_RECURSE "${TEST_ROOT}") +file(MAKE_DIRECTORY "${TEST_ROOT}") + +# CMake variables take precedence over environment variables. Empty values +# fall back to the environment and then to the default URL. +set(ENV{FVP_DEPS_URL} "https://env.example.invalid/releases") +string(TOUPPER "${FIXTURE_V2_SHA256}" FIXTURE_V2_SHA256_UPPER) +set(ENV{FVP_DEPS_SHA256} "${FIXTURE_V2_SHA256_UPPER}") +set(FVP_DEPS_URL " ") +set(FVP_DEPS_SHA256 " ") +_fvp_resolve_deps_options("https://default.example.invalid/" RESOLVED_URL RESOLVED_SHA256) +assert_equal("${RESOLVED_URL}" "https://env.example.invalid/releases" "Environment URL was not used") +assert_equal("${RESOLVED_SHA256}" "${FIXTURE_V2_SHA256}" "Environment SHA-256 was not used") + +set(FVP_DEPS_URL "https://cache.example.invalid/releases/") +set(FVP_DEPS_SHA256 "${FIXTURE_V1_SHA256}") +_fvp_resolve_deps_options("https://default.example.invalid/" RESOLVED_URL RESOLVED_SHA256) +assert_equal("${RESOLVED_URL}" "https://cache.example.invalid/releases" "CMake URL did not take precedence") +assert_equal("${RESOLVED_SHA256}" "${FIXTURE_V1_SHA256}" "CMake SHA-256 did not take precedence") + +set(ENV{FVP_DEPS_URL} "") +set(ENV{FVP_DEPS_SHA256} "") +set(FVP_DEPS_URL "") +set(FVP_DEPS_SHA256 "") +_fvp_resolve_deps_options("https://default.example.invalid/" RESOLVED_URL RESOLVED_SHA256) +assert_equal("${RESOLVED_URL}" "https://default.example.invalid" "Default URL was not used") +assert_equal("${RESOLVED_SHA256}" "" "Default SHA-256 must be empty") + +set(ENV{FVP_DEPS_URL} "file:///legacy-environment-value-is-ignored") +_fvp_resolve_deps_options("https://default.example.invalid/" RESOLVED_URL RESOLVED_SHA256) +assert_equal("${RESOLVED_URL}" "https://default.example.invalid" "Non-HTTP environment URL behavior changed") +set(ENV{FVP_DEPS_URL} "") + +# A valid pin downloads, verifies, and stamps the extracted SDK. +set(PINNED_ROOT "${TEST_ROOT}/pinned cache") +set(PINNED_ARCHIVE "${PINNED_ROOT}/mdk-sdk.tar") +_fvp_prepare_mdk_sdk( + "${FIXTURE_V1_URL}" + "${PINNED_ARCHIVE}" + "${PINNED_ROOT}" + "${FIXTURE_V1_SHA256}" + OFF +) +assert_payload("${PINNED_ROOT}" "fixture-v1") +file(READ "${PINNED_ROOT}/mdk-sdk/.fvp-deps.sha256" PINNED_STAMP) +string(STRIP "${PINNED_STAMP}" PINNED_STAMP) +assert_equal("${PINNED_STAMP}" "${FIXTURE_V1_SHA256}" "Incorrect SHA-256 stamp") + +# A matching stamp can be reused offline without the downloaded archive. +file(WRITE "${PINNED_ROOT}/mdk-sdk/local-marker.txt" "keep\n") +file(REMOVE "${PINNED_ARCHIVE}") +_fvp_prepare_mdk_sdk( + "file:///does/not/exist/mdk-sdk.tar" + "${PINNED_ARCHIVE}" + "${PINNED_ROOT}" + "${FIXTURE_V1_SHA256}" + OFF +) +assert_exists("${PINNED_ROOT}/mdk-sdk/local-marker.txt" "Pinned cache was unexpectedly re-extracted") + +# Changing the pin downloads and replaces the extracted SDK without stale files. +_fvp_prepare_mdk_sdk( + "${FIXTURE_V2_URL}" + "${PINNED_ARCHIVE}" + "${PINNED_ROOT}" + "${FIXTURE_V2_SHA256}" + OFF +) +assert_payload("${PINNED_ROOT}" "fixture-v2") +assert_not_exists("${PINNED_ROOT}/mdk-sdk/local-marker.txt" "Stale extracted files were retained") + +# Recover a last-known-good SDK if a process stopped between backup and swap. +file(RENAME "${PINNED_ROOT}/mdk-sdk" "${PINNED_ROOT}/.fvp-mdk-backup") +_fvp_prepare_mdk_sdk( + "file:///does/not/exist/mdk-sdk.tar" + "${PINNED_ARCHIVE}" + "${PINNED_ROOT}" + "${FIXTURE_V2_SHA256}" + OFF +) +assert_payload("${PINNED_ROOT}" "fixture-v2") +assert_not_exists("${PINNED_ROOT}/.fvp-mdk-backup" "Recovered SDK backup was not cleaned up") + +# A bad pin fails without replacing the last valid archive or extracted SDK. +set(BAD_SHA256 "0000000000000000000000000000000000000000000000000000000000000000") +execute_process( + COMMAND "${CMAKE_COMMAND}" + "-DDEPS_FILE=${DEPS_FILE}" + "-DSDK_URL=${FIXTURE_V1_URL}" + "-DSDK_ARCHIVE=${PINNED_ARCHIVE}" + "-DEXTRACT_ROOT=${PINNED_ROOT}" + "-DEXPECTED_SHA256=${BAD_SHA256}" + -P "${INVOKE_FILE}" + RESULT_VARIABLE BAD_HASH_RESULT + OUTPUT_VARIABLE BAD_HASH_OUTPUT + ERROR_VARIABLE BAD_HASH_ERROR +) +if(BAD_HASH_RESULT EQUAL 0) + message(FATAL_ERROR "An incorrect SHA-256 unexpectedly succeeded") +endif() +set(BAD_HASH_LOG "${BAD_HASH_OUTPUT}\n${BAD_HASH_ERROR}") +if(NOT "${BAD_HASH_LOG}" MATCHES "SHA-256 mismatch") + message(FATAL_ERROR "Incorrect SHA-256 failed for an unexpected reason:\n${BAD_HASH_LOG}") +endif() +assert_not_exists("${PINNED_ARCHIVE}.part" "Partial download was not removed") +file(SHA256 "${PINNED_ARCHIVE}" ARCHIVE_AFTER_FAILURE) +assert_equal("${ARCHIVE_AFTER_FAILURE}" "${FIXTURE_V2_SHA256}" "Valid archive was replaced after hash failure") +assert_payload("${PINNED_ROOT}" "fixture-v2") + +# Without a pin, the existing download and cache behavior remains available. +set(LEGACY_ROOT "${TEST_ROOT}/legacy cache") +set(LEGACY_ARCHIVE "${LEGACY_ROOT}/mdk-sdk.tar") +_fvp_prepare_mdk_sdk( + "${FIXTURE_V1_URL}" + "${LEGACY_ARCHIVE}" + "${LEGACY_ROOT}" + "" + OFF +) +assert_payload("${LEGACY_ROOT}" "fixture-v1") +_fvp_prepare_mdk_sdk( + "${FIXTURE_V2_URL}" + "${LEGACY_ARCHIVE}" + "${LEGACY_ROOT}" + "" + ON +) +assert_payload("${LEGACY_ROOT}" "fixture-v2") + +# Existing caches created by older fvp versions have no SHA-256 stamp. A +# requested latest check re-extracts the matching archive to establish one. +file(REMOVE "${LEGACY_ROOT}/mdk-sdk/.fvp-deps.sha256") +file(WRITE "${LEGACY_ROOT}/mdk-sdk/local-marker.txt" "remove\n") +_fvp_prepare_mdk_sdk( + "${FIXTURE_V2_URL}" + "${LEGACY_ARCHIVE}" + "${LEGACY_ROOT}" + "" + ON +) +assert_not_exists("${LEGACY_ROOT}/mdk-sdk/local-marker.txt" "Unstamped latest SDK was not re-extracted") +assert_exists("${LEGACY_ROOT}/mdk-sdk/.fvp-deps.sha256" "Latest SDK stamp was not created") + +file(WRITE "${LEGACY_ROOT}/mdk-sdk/local-marker.txt" "keep\n") +_fvp_prepare_mdk_sdk( + "${FIXTURE_V2_URL}" + "${LEGACY_ARCHIVE}" + "${LEGACY_ROOT}" + "" + ON +) +assert_exists("${LEGACY_ROOT}/mdk-sdk/local-marker.txt" "Unchanged latest SDK was unexpectedly re-extracted") + +# If a previous update replaced only the archive, the stamp forces extraction +# even when the remote MD5 matches that archive. +file(WRITE "${LEGACY_ROOT}/mdk-sdk/.fvp-deps.sha256" "${FIXTURE_V1_SHA256}\n") +_fvp_prepare_mdk_sdk( + "${FIXTURE_V2_URL}" + "${LEGACY_ARCHIVE}" + "${LEGACY_ROOT}" + "" + ON +) +assert_not_exists("${LEGACY_ROOT}/mdk-sdk/local-marker.txt" "Stale SDK survived an archive/stamp mismatch") +file(READ "${LEGACY_ROOT}/mdk-sdk/.fvp-deps.sha256" LATEST_STAMP) +string(STRIP "${LATEST_STAMP}" LATEST_STAMP) +assert_equal("${LATEST_STAMP}" "${FIXTURE_V2_SHA256}" "Latest SDK stamp was not repaired") + +# Ordinary unpinned builds preserve the legacy cache behavior even if a +# leftover archive no longer matches the extracted SDK. +_fvp_download_file("${FIXTURE_V1_URL}" "${LEGACY_ARCHIVE}" "") +file(WRITE "${LEGACY_ROOT}/mdk-sdk/local-marker.txt" "keep\n") +_fvp_prepare_mdk_sdk( + "file:///does/not/exist/mdk-sdk.tar" + "${LEGACY_ARCHIVE}" + "${LEGACY_ROOT}" + "" + OFF +) +assert_exists("${LEGACY_ROOT}/mdk-sdk/local-marker.txt" "Ordinary unpinned cache behavior changed") +assert_payload("${LEGACY_ROOT}" "fixture-v2") + +file(REMOVE "${LEGACY_ARCHIVE}") +_fvp_prepare_mdk_sdk( + "file:///does/not/exist/mdk-sdk.tar" + "${LEGACY_ARCHIVE}" + "${LEGACY_ROOT}" + "" + OFF +) +assert_exists("${LEGACY_ROOT}/mdk-sdk/local-marker.txt" "Unpinned cache behavior changed") diff --git a/cmake/tests/invoke_fetch.cmake b/cmake/tests/invoke_fetch.cmake new file mode 100644 index 0000000..2666ec9 --- /dev/null +++ b/cmake/tests/invoke_fetch.cmake @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.15) + +foreach(REQUIRED_VARIABLE DEPS_FILE SDK_URL SDK_ARCHIVE EXTRACT_ROOT EXPECTED_SHA256) + if(NOT DEFINED ${REQUIRED_VARIABLE}) + message(FATAL_ERROR "${REQUIRED_VARIABLE} is required") + endif() +endforeach() + +include("${DEPS_FILE}") +_fvp_prepare_mdk_sdk( + "${SDK_URL}" + "${SDK_ARCHIVE}" + "${EXTRACT_ROOT}" + "${EXPECTED_SHA256}" + OFF +) diff --git a/cmake/tests/macro_bad_sha_test.cmake b/cmake/tests/macro_bad_sha_test.cmake new file mode 100644 index 0000000..dc3b173 --- /dev/null +++ b/cmake/tests/macro_bad_sha_test.cmake @@ -0,0 +1,33 @@ +cmake_minimum_required(VERSION 3.15) + +foreach(REQUIRED_VARIABLE DEPS_FILE MACRO_TEST_FILE SDK_BASE_URL TEST_ROOT) + if(NOT DEFINED ${REQUIRED_VARIABLE}) + message(FATAL_ERROR "${REQUIRED_VARIABLE} is required") + endif() +endforeach() + +set(BAD_SHA256 "0000000000000000000000000000000000000000000000000000000000000000") +file(REMOVE_RECURSE "${TEST_ROOT}") +file(MAKE_DIRECTORY "${TEST_ROOT}") +execute_process( + COMMAND "${CMAKE_COMMAND}" + "-DDEPS_FILE=${DEPS_FILE}" + "-DFVP_DEPS_URL=${SDK_BASE_URL}" + "-DFVP_DEPS_SHA256=${BAD_SHA256}" + -P "${MACRO_TEST_FILE}" + WORKING_DIRECTORY "${TEST_ROOT}" + RESULT_VARIABLE BAD_HASH_RESULT + OUTPUT_VARIABLE BAD_HASH_OUTPUT + ERROR_VARIABLE BAD_HASH_ERROR +) +if(BAD_HASH_RESULT EQUAL 0) + message(FATAL_ERROR "fvp_setup_deps() accepted an incorrect SHA-256") +endif() +set(BAD_HASH_LOG "${BAD_HASH_OUTPUT}\n${BAD_HASH_ERROR}") +if(NOT "${BAD_HASH_LOG}" MATCHES "SHA-256 mismatch") + message(FATAL_ERROR "Incorrect SHA-256 failed for an unexpected reason:\n${BAD_HASH_LOG}") +endif() +file(GLOB PART_FILES "${TEST_ROOT}/*.part") +if(PART_FILES) + message(FATAL_ERROR "Partial macro integration download was not removed: ${PART_FILES}") +endif() diff --git a/cmake/tests/macro_test.cmake b/cmake/tests/macro_test.cmake new file mode 100644 index 0000000..9f6f7a9 --- /dev/null +++ b/cmake/tests/macro_test.cmake @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.15) + +if(NOT DEFINED DEPS_FILE) + message(FATAL_ERROR "DEPS_FILE is required") +endif() + +set(CMAKE_SYSTEM_NAME "Linux") +set(CMAKE_SYSTEM_PROCESSOR "x86_64") +include("${DEPS_FILE}") + +# The dependency integration test runs outside the plugin source tree. Avoid +# generating version.h; fvp_version() is independent from dependency setup. +function(fvp_version) +endfunction() + +fvp_setup_deps() + +if(NOT FVP_TEST_FIND_MDK) + message(FATAL_ERROR "fvp_setup_deps() did not include FindMDK.cmake") +endif() diff --git a/config.yaml b/config.yaml index 16bb9c1..b5234da 100644 --- a/config.yaml +++ b/config.yaml @@ -34,5 +34,5 @@ globals: - mdk.* comments: true preamble: | - // Copyright (c) 2019-2024 WangBin + // Copyright (c) 2019-2025 WangBin // https://github.com/wang-bin/mdk-sdk \ No newline at end of file diff --git a/darwin/Classes/FvpPlugin.mm b/darwin/Classes/FvpPlugin.mm index 21c9395..d530d1c 100644 --- a/darwin/Classes/FvpPlugin.mm +++ b/darwin/Classes/FvpPlugin.mm @@ -1,4 +1,4 @@ -// Copyright 2023-2024 Wang Bin. All rights reserved. +// Copyright 2023-2025 Wang Bin. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -130,12 +130,17 @@ + (void)registerWithRegistrar:(NSObject*)registrar { auto messenger = registrar.messenger; #else auto messenger = [registrar messenger]; + // Allow audio playback when the Ring/Silent switch is set to silent + [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil]; #endif FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@"fvp" binaryMessenger:messenger]; FvpPlugin* instance = [[FvpPlugin alloc] initWithRegistrar:registrar]; +#if TARGET_OS_OSX +#else + [registrar addApplicationDelegate:instance]; +#endif [registrar publish:instance]; [registrar addMethodCallDelegate:instance channel:channel]; - SetGlobalOption("MDK_KEY", "C03BFF5306AB39058A767105F82697F42A00FE970FB0E641D306DEFF3F220547E5E5377A3C504DC30D547890E71059BC023A4DD91A95474D1F33CA4C26C81B0FC73B00ACF954C6FA75898EFA07D9680B6A00FDF179C0A15381101D01124498AF55B069BD4B0156D5CF5A56DEDE782E5F3930AD47C8F40BFBA379231142E31B0F"); } - (instancetype)initWithRegistrar:(NSObject*)registrar { @@ -181,4 +186,11 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - (void)detachFromEngineForRegistrar:(NSObject *)registrar { players.clear(); } + +#if TARGET_OS_OSX +#else +- (void)applicationWillTerminate:(UIApplication *)application { + players.clear(); +} +#endif @end diff --git a/darwin/fvp.podspec b/darwin/fvp.podspec index d0faf60..426199f 100644 --- a/darwin/fvp.podspec +++ b/darwin/fvp.podspec @@ -5,7 +5,7 @@ # Pod::Spec.new do |s| s.name = 'fvp' - s.version = '0.27.0' + s.version = '0.36.1' s.summary = 'libmdk based Flutter video player plugin' s.description = <<-DESC Flutter video player plugin. @@ -25,10 +25,14 @@ Flutter video player plugin. s.osx.dependency 'FlutterMacOS' s.ios.deployment_target = '12.0' s.osx.deployment_target = '10.13' - s.dependency 'mdk', '~> 0.30.0' + s.dependency 'mdk', '~> 0.36.0' # s.platform = :osx, '10.11' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.resource_bundles = {'fvp_privacy' => ['PrivacyInfo.xcprivacy']} - s.swift_version = '5.0' +# s.swift_version = '5.0' + s.prepare_command = <<-CMD + FVP_VERSION=`grep 'version: ' ../pubspec.yaml | head -1 | awk '{print $2}'` + echo '#pragma once\n#define FVP_VERSION "'$FVP_VERSION'"' > ../lib/src/version.h + CMD end diff --git a/darwin/fvp/Package.swift b/darwin/fvp/Package.swift new file mode 100644 index 0000000..005870d --- /dev/null +++ b/darwin/fvp/Package.swift @@ -0,0 +1,54 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. +import PackageDescription + +let package = Package( + name: "fvp", + platforms: [ + .iOS("12.0"), + .macOS("10.13"), + ], + products: [ + .library(name: "fvp", type: .dynamic, targets: ["fvp"]), + ], + dependencies: [ + .package(name: "FlutterFramework", path: "../FlutterFramework"), + ], + targets: [ + .target( + name: "fvp", + dependencies: [ + .target(name: "mdk"), + .product(name: "FlutterFramework", package: "FlutterFramework"), + ], + path: ".", + sources: [ + "Sources/fvp/FvpPlugin.mm", + "Sources/fvp/callbacks.cpp", + ], + resources: [ + .process("Resources/PrivacyInfo.xcprivacy"), + ], + publicHeadersPath: "Sources/fvp", + cSettings: [ + .headerSearchPath("Sources/fvp"), + ], + cxxSettings: [ + .unsafeFlags(["-Wno-documentation"]), + ], + linkerSettings: [ + .linkedFramework("Flutter", .when(platforms: [.iOS])), + .linkedFramework("FlutterMacOS", .when(platforms: [.macOS])), + .linkedFramework("AVFoundation"), + .linkedFramework("CoreVideo"), + .linkedFramework("Metal"), + ] + ), + .binaryTarget( + name: "mdk", + url: "https://github.com/wang-bin/mdk-sdk/releases/download/v0.37.0/mdk-sdk-apple.zip", + checksum: "1f92b5318138fdf90dfc2424c0ff751d64f705413ae0d3c7f04aa3faec49c921" + ), + ], + cxxLanguageStandard: .cxx20 +) diff --git a/darwin/fvp/Resources/PrivacyInfo.xcprivacy b/darwin/fvp/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..a34b7e2 --- /dev/null +++ b/darwin/fvp/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/darwin/fvp/Sources/fvp/FvpPlugin.h b/darwin/fvp/Sources/fvp/FvpPlugin.h new file mode 120000 index 0000000..9e6320a --- /dev/null +++ b/darwin/fvp/Sources/fvp/FvpPlugin.h @@ -0,0 +1 @@ +../../../Classes/FvpPlugin.h \ No newline at end of file diff --git a/darwin/fvp/Sources/fvp/FvpPlugin.mm b/darwin/fvp/Sources/fvp/FvpPlugin.mm new file mode 120000 index 0000000..fb23041 --- /dev/null +++ b/darwin/fvp/Sources/fvp/FvpPlugin.mm @@ -0,0 +1 @@ +../../../Classes/FvpPlugin.mm \ No newline at end of file diff --git a/darwin/fvp/Sources/fvp/callbacks.cpp b/darwin/fvp/Sources/fvp/callbacks.cpp new file mode 120000 index 0000000..36e2f60 --- /dev/null +++ b/darwin/fvp/Sources/fvp/callbacks.cpp @@ -0,0 +1 @@ +../../../../lib/src/callbacks.cpp \ No newline at end of file diff --git a/darwin/fvp/Sources/fvp/callbacks.h b/darwin/fvp/Sources/fvp/callbacks.h new file mode 120000 index 0000000..2d2d759 --- /dev/null +++ b/darwin/fvp/Sources/fvp/callbacks.h @@ -0,0 +1 @@ +../../../../lib/src/callbacks.h \ No newline at end of file diff --git a/darwin/fvp/Sources/fvp/dart_api_types.h b/darwin/fvp/Sources/fvp/dart_api_types.h new file mode 120000 index 0000000..2cd0a1a --- /dev/null +++ b/darwin/fvp/Sources/fvp/dart_api_types.h @@ -0,0 +1 @@ +../../../../lib/src/dart_api_types.h \ No newline at end of file diff --git a/elinux/CMakeLists.txt b/elinux/CMakeLists.txt new file mode 100644 index 0000000..91998e0 --- /dev/null +++ b/elinux/CMakeLists.txt @@ -0,0 +1,52 @@ +cmake_minimum_required(VERSION 3.17) +set(PROJECT_NAME "fvp") +project(${PROJECT_NAME} LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# This value is used when generating builds using this plugin, so it must +# not be changed +set(PLUGIN_NAME "fvp_plugin") + +add_library(${PLUGIN_NAME} SHARED + "fvp_plugin.cc" + ../lib/src/callbacks.cpp +) +apply_standard_settings(${PLUGIN_NAME}) +set_target_properties(${PLUGIN_NAME} PROPERTIES + CXX_VISIBILITY_PRESET hidden + OUTPUT_NAME "fvp") +target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) +target_include_directories(${PLUGIN_NAME} INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) + +target_link_libraries(${PLUGIN_NAME} PRIVATE EGL GLESv2) +#target_link_libraries(${PLUGIN_NAME} PRIVATE epoxy) +# flutter in snap: linker will try to resolve symbols and dependencies in libmdk.so.0, dependencies found in host OS but not snap are compatible with snap glibc +target_link_libraries(${PLUGIN_NAME} INTERFACE -Wl,--unresolved-symbols=ignore-in-shared-libs) +target_link_options(${PLUGIN_NAME} PRIVATE -Wl,--enable-new-dtags -Wl,-z,origin) +# add runpath, shared libs of a release bundle is in lib dir, plugin must add $ORIGIN to runpath to find libmdk +set_target_properties(${PLUGIN_NAME} PROPERTIES + BUILD_RPATH_USE_ORIGIN ON + BUILD_RPATH "$ORIGIN" + INSTALL_RPATH "$ORIGIN" +) +target_compile_options(${PLUGIN_NAME} PRIVATE -fno-rtti -fno-exceptions -ffunction-sections -fdata-sections) +target_compile_options(${PLUGIN_NAME} PRIVATE -Wno-unused-function) # https://github.com/wang-bin/fvp/issues/49 +target_link_libraries(${PLUGIN_NAME} PRIVATE -Wl,--gc-sections) + +include(../cmake/deps.cmake) +fvp_setup_deps() +target_link_libraries(${PLUGIN_NAME} PRIVATE mdk) +# List of absolute paths to libraries that should be bundled with the plugin +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +get_filename_component(MDK_LIB_DIR ${MDK_LIBRARY} DIRECTORY) +set(fvp_bundled_libraries + ${MDK_RUNTIME} + ${MDK_PLUGINS} + ${MDK_FFMPEG} + ${MDK_LIB_DIR}/libc++.so.1 + PARENT_SCOPE +) diff --git a/elinux/fvp_plugin.cc b/elinux/fvp_plugin.cc new file mode 100644 index 0000000..40e36ba --- /dev/null +++ b/elinux/fvp_plugin.cc @@ -0,0 +1,282 @@ +/* + * Copyright (c) 2025 WangBin + */ +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#include "include/fvp/fvp_plugin.h" +#include +#include +#include +#if 1 +#include +#include +#include +#include +#else +#include +#include +#endif +#include +#include +#include +#include +#include +#include +#include "mdk/RenderAPI.h" +#include "mdk/Player.h" +#undef Success // X.h + +using namespace std; + +#define EGL_ENSURE(x, ...) EGL_RUN_CHECK(x, return __VA_ARGS__) +#define EGL_WARN(x, ...) EGL_RUN_CHECK(x) +#define EGL_RUN_CHECK(x, ...) do { \ + while (eglGetError() != EGL_SUCCESS) {} \ + (x); \ + const EGLint err = eglGetError(); \ + if (err != EGL_SUCCESS) { \ + std::cout << #x " EGL ERROR (" << std::hex << err << std::dec << ") @" << __LINE__ << __FUNCTION__ << std::endl; \ + __VA_ARGS__; \ + } \ + } while(false) + +#define GL_ENSURE(x, ...) GL_RUN_CHECK(x, return __VA_ARGS__) +#define GL_WARN(x, ...) GL_RUN_CHECK(x) +// GL_CONTEXT_LOST repeats. stop render loop? see qtbase c33faac32b +// https://www.khronos.org/webgl/wiki/HandlingContextLost +#define GL_RUN_CHECK(expr, ...) do { \ + while (true) { \ + const GLenum err = glGetError(); \ + if (err == GL_NO_ERROR) \ + break; \ + if (err == GL_CONTEXT_LOST_KHR) { \ + std::cout << "GL_CONTEXT_LOST" << std::endl; \ + break; \ + } \ + } \ + expr; \ + const GLenum err = glGetError(); \ + if (err != GL_NO_ERROR) { \ + std::cout << #expr " GL ERROR (" << std::hex << err << std::dec << ") @" << __FUNCTION__ << __LINE__ << std::endl; \ + __VA_ARGS__; \ + } \ + } while(false) + +namespace { +class CleanupTask { +public: + CleanupTask(function callback) : cb_(callback) {} + ~CleanupTask() { + cb_(); + } + + bool disposed = false; +private: + function cb_; +}; +static thread_local list> gCleanupTasks; + +class TexturePlayer final : public mdk::Player +{ +public: + TexturePlayer(int64_t handle, int width, int height, flutter::TextureRegistrar* texRegistrar) + : mdk::Player(reinterpret_cast(handle)) + , texture_registrar_(texRegistrar) + { + fltImg_->egl_image = EGL_NO_IMAGE_KHR; // TODO: + fltImg_->width = width; + fltImg_->height = height; + fltImg_->release_callback = [](void* release_context) { + }; + fltImg_->release_context = nullptr; // TODO: + fltTex_ = make_unique(flutter::EGLImageTexture( + [this](size_t width, size_t height, void* egl_display, void* egl_context) { + fltImg_->egl_image = ensureVideo(width, height, static_cast(egl_display), static_cast(egl_context)); + return fltImg_.get(); + } + )); + textureId = texRegistrar->RegisterTexture(fltTex_.get()); + + scale(1, -1); // y is flipped + setVideoSurfaceSize(width, height); + setRenderCallback([this, texRegistrar](void*) { + //renderVideo(); // need a gl context + texRegistrar->MarkTextureFrameAvailable(textureId); + }); + } + + template // use template to not instantiate false branch + bool unregisterIfGoodHeader(F&& f) { + if constexpr (requires(T* t){ t->UnregisterTexture(0, nullptr); }) { + texture_registrar_->UnregisterTexture(textureId, std::forward(f)); + return true; + } + return false; + } + + template // use template to not instantiate false branch + bool unregisterCanPostTask() { + return requires(T* t){ t->UnregisterTexture(0, nullptr); }; + } + + ~TexturePlayer() override { + if (task_) + task_->disposed = true; + setRenderCallback(nullptr); + // texture_registrar.h in flutter-elinux is outdated and results in crash. https://github.com/sony/flutter-embedded-linux/issues/438 + if (!unregisterIfGoodHeader(cleanup_)) { + texture_registrar_->UnregisterTexture(textureId); + } + setVideoSurfaceSize(-1, -1); // no gl context now, but gl resources will be released in raster thread later in ensureVideo() + } + + EGLImageKHR ensureVideo(size_t width, size_t height, EGLDisplay disp, EGLContext c) { + if (auto count = std::erase_if(gCleanupTasks, [](auto task) { return task->disposed; })) { + clog << std::to_string(count) + " cleanup tasks executed in raster thread " << this_thread::get_id() << endl; + } + if (fbo_ == 0) { + ctx_ = c; // fbo can not be shared + disp_ = disp; + draw_ = eglGetCurrentSurface(EGL_DRAW); + read_ = eglGetCurrentSurface(EGL_READ); + GL_WARN(glGenFramebuffers(1, &fbo_)); + GLint prevFbo = 0; + GL_WARN(glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFbo)); + GL_WARN(glBindFramebuffer(GL_FRAMEBUFFER, fbo_)); + GL_WARN(glGenTextures(1, &tex_)); + GL_WARN(glBindTexture(GL_TEXTURE_2D, tex_)); + GL_WARN(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr)); + GL_WARN(glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + 0, GL_TEXTURE_2D, tex_, 0)); + const GLenum err = glCheckFramebufferStatus(GL_FRAMEBUFFER); + GL_WARN(glBindFramebuffer(GL_FRAMEBUFFER, prevFbo)); + if (err != GL_FRAMEBUFFER_COMPLETE) { + //glDeleteFramebuffers(1, &fbo); + clog << fbo_ << " glFramebufferTexture2D " + std::to_string(tex_) + " error: " << err << endl; + return img_; + } + mdk::GLRenderAPI ra{}; + ra.fbo = fbo_; + setRenderAPI(&ra); + } + if (img_ == EGL_NO_IMAGE_KHR) { + if (!eglCreateImageKHR) { + eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR"); + eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR"); + } + EGL_WARN(img_ = eglCreateImageKHR(disp, c, EGL_GL_TEXTURE_2D_KHR, (EGLClientBuffer)(intptr_t)tex_, nullptr)); + if (img_ == EGL_NO_IMAGE_KHR) { + clog << "eglCreateImageKHR error" << endl; + } + clog << gCleanupTasks.size() << " tasks. created fbo: " + std::to_string(fbo_) + " tex: " + std::to_string(tex_) + " in raster thread " << this_thread::get_id() << endl; + + cleanup_ = [disp = disp_, img = img_, tex = tex_, fbo = fbo_, eglDestroyImageKHR = eglDestroyImageKHR]() { // called in raster thread and gl context is correct + clog << "delete fbo: " + std::to_string(fbo) + " tex: " + std::to_string(tex) << endl; + if (img != EGL_NO_IMAGE_KHR) + EGL_WARN(eglDestroyImageKHR(disp, img)); + if (tex) + GL_WARN(glDeleteTextures(1, &tex)); + if (fbo) + GL_WARN(glDeleteFramebuffers(1, &fbo)); + }; + if (!unregisterCanPostTask()) { + clog << "incompatible texture_registrar.h, see https://github.com/sony/flutter-embedded-linux/issues/438" << endl; + auto task = make_shared(cleanup_); + task_ = task.get(); + gCleanupTasks.push_back(std::move(task)); + } + } + + renderVideo(); + return img_; + } + + int64_t textureId; +private: + unique_ptr fltImg_ = make_unique(); + unique_ptr fltTex_; + flutter::TextureRegistrar* texture_registrar_ = nullptr; + CleanupTask* task_ = nullptr; + function cleanup_ = {}; + + PFNEGLCREATEIMAGEKHRPROC eglCreateImageKHR = nullptr; + PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR = nullptr; + EGLDisplay disp_ = EGL_NO_DISPLAY; + EGLContext ctx_ = EGL_NO_CONTEXT; + EGLSurface read_ = EGL_NO_SURFACE; + EGLSurface draw_ = EGL_NO_SURFACE; + EGLImageKHR img_ = EGL_NO_IMAGE_KHR; + GLuint tex_ = 0; // TODO: array + GLuint fbo_ = 0; +}; + + +class FvpPlugin final : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrar *registrar); + + FvpPlugin(flutter::TextureRegistrar* tr) + : texture_registrar_(tr) + {} + + private: + // Called when a method is called on this plugin's channel from Dart. + void HandleMethodCall( + const flutter::MethodCall &method_call, + std::unique_ptr> result); + + + flutter::TextureRegistrar* texture_registrar_ = nullptr; + std::unordered_map> players_; +}; + +// static +void FvpPlugin::RegisterWithRegistrar( + flutter::PluginRegistrar *registrar) { + auto channel = + std::make_unique>( + registrar->messenger(), "fvp", + &flutter::StandardMethodCodec::GetInstance()); + + auto plugin = std::make_unique(registrar->texture_registrar()); + + channel->SetMethodCallHandler( + [plugin_pointer = plugin.get()](const auto &call, auto result) { + plugin_pointer->HandleMethodCall(call, std::move(result)); + }); + + registrar->AddPlugin(std::move(plugin)); +} + +void FvpPlugin::HandleMethodCall( + const flutter::MethodCall &method_call, + std::unique_ptr> result) { + if (method_call.method_name() == "CreateRT") { + auto args = std::get(*method_call.arguments()); + const auto width = (int)args[flutter::EncodableValue("width")].LongValue(); + const auto height = (int)args[flutter::EncodableValue("height")].LongValue(); + const auto handle = args[flutter::EncodableValue("player")].LongValue(); + auto player = make_shared(handle, width, height, texture_registrar_); + result->Success(flutter::EncodableValue(player->textureId)); + players_[player->textureId] = player; + } else if (method_call.method_name() == "ReleaseRT") { + auto args = std::get(*method_call.arguments()); + const auto texId = args[flutter::EncodableValue("texture")].LongValue(); + if (auto it = players_.find(texId); it != players_.cend()) { + players_.erase(it); + } + result->Success(); + } else if (method_call.method_name() == "MixWithOthers") { + result->Success(); + } else { + result->NotImplemented(); + } +} + +} // namespace + +void FvpPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + FvpPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} diff --git a/elinux/include/fvp/fvp_plugin.h b/elinux/include/fvp/fvp_plugin.h new file mode 100644 index 0000000..bd873c1 --- /dev/null +++ b/elinux/include/fvp/fvp_plugin.h @@ -0,0 +1,23 @@ +#ifndef FLUTTER_PLUGIN_FVP_PLUGIN_H_ +#define FLUTTER_PLUGIN_FVP_PLUGIN_H_ + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default"))) +#else +#define FLUTTER_PLUGIN_EXPORT +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void FvpPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_FVP_PLUGIN_H_ diff --git a/example/.gitignore b/example/.gitignore index 24476c5..05a059d 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -5,10 +5,13 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ +.cxx/ # IntelliJ related *.iml diff --git a/example/android/gradle.properties b/example/android/gradle.properties index b9a9a24..73c3168 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true android.defaults.buildfeatures.buildconfig=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 8d838b4..f0b6fd9 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -4,4 +4,4 @@ distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists # 8.0 does not support compileSdk 34 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle index f6feeb0..02fc9a8 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version '8.5.0' apply false // compileSdk 34 - id "org.jetbrains.kotlin.android" version "1.7.10" apply false + id "com.android.application" version '8.7.3' apply false // compileSdk 34 + id "org.jetbrains.kotlin.android" version "2.0.20" apply false } include ":app" \ No newline at end of file diff --git a/example/windows/CMakeLists.txt b/example/windows/CMakeLists.txt index fe4df5b..09b2a60 100644 --- a/example/windows/CMakeLists.txt +++ b/example/windows/CMakeLists.txt @@ -8,7 +8,7 @@ set(BINARY_NAME "fvp_example") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. -cmake_policy(SET CMP0063 NEW) +cmake_policy(VERSION 3.14...3.25) # Define build configuration option. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) @@ -86,6 +86,12 @@ if(PLUGIN_BUNDLED_LIBRARIES) COMPONENT Runtime) endif() +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") diff --git a/lib/fvp.dart b/lib/fvp.dart index f2b1f12..1d5afc9 100644 --- a/lib/fvp.dart +++ b/lib/fvp.dart @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Wang Bin. All rights reserved. +// Copyright 2022-2025 Wang Bin. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -8,7 +8,7 @@ import 'src/video_player_mdk.dart' export 'src/controller.dart'; -/// Registers this plugin as the default instance of [VideoPlayerPlatform]. Then your [VideoPlayer] will support all platforms. +/// Registers this plugin as the default instance of VideoPlayerPlatform. Then your [VideoPlayer] will support all platforms. /// If registerWith is not called, the previous(usually official) implementation will be used when available. /// [options] can be @@ -23,13 +23,13 @@ export 'src/controller.dart'; /// /// 'lowLatency': int. default is 0. reduce network stream latency. 1: for vod. 2: for live stream, may drop frames to ensure the latest content is displayed /// -/// "player": backend player properties of type Map. See https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#void-setpropertyconst-stdstring-key-const-stdstring-value +/// "player": backend player properties of type [Map]. See https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#void-setpropertyconst-stdstring-key-const-stdstring-value /// -/// "global": backend global options of type Map. See https://github.com/wang-bin/mdk-sdk/wiki/Global-Options +/// "global": backend global options of type [Map]. See https://github.com/wang-bin/mdk-sdk/wiki/Global-Options /// /// "tunnel": android only, default is false. AMediacodec/MediaCodec decoder output to a SurfaceTexture surface directly without OpenGL. Maybe more efficient, but some features are not supported, e.g. HDR tone mapping, less codecs. /// -/// 'subtitleFontFile': default subtitle font file as the fallback. If not set, 'assets/subfont.ttf' will be used, you can add it in pubspec.yaml if you need it. +/// 'subtitleFontFile': default subtitle font file as the fallback, can be an http url. If not set, 'assets/subfont.ttf' will be used, you can add it in pubspec.yaml if you need it. /// subfont.ttf can be downloaded from https://github.com/mpv-android/mpv-android/raw/master/app/src/main/assets/subfont.ttf /// /// Example: @@ -47,12 +47,13 @@ void registerWith({dynamic options}) { MdkVideoPlayerPlatform.registerVideoPlayerPlatformsWith(options: options); } -/// Registers this plugin automatically by dart tooling. requires `implements: video_player` and `dartPluginClass: VideoPlayerRegistrant` in pubspec.yaml +/// Registers this plugin automatically by dart tooling. requires `dartPluginClass: VideoPlayerRegistrant` in pubspec.yaml class VideoPlayerRegistrant { static void registerWith() { MdkVideoPlayerPlatform.registerVideoPlayerPlatformsWith(); } } + /* bool isRegistered() { return VideoPlayerPlatform.instance.runtimeType == MdkVideoPlayerPlatform; diff --git a/lib/src/callbacks.cpp b/lib/src/callbacks.cpp index 611a74f..6ecba33 100644 --- a/lib/src/callbacks.cpp +++ b/lib/src/callbacks.cpp @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Wang Bin. All rights reserved. +// Copyright 2022-2026 Wang Bin. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -11,6 +11,9 @@ #include #include "dart_api_types.h" #include "callbacks.h" +#if __has_include("version.h") +#include "version.h" +#endif using namespace std; @@ -38,6 +41,18 @@ static unordered_map> players; // global callbacks static int gCallbackTypes = 0; +FVP_EXPORT +#if (__clang__ + 0) +__attribute__((disable_tail_calls)) +//[[clang::disable_tail_calls]] // msvc: C5030 + C2220. clang 15: can not mix gnu and c++ attributes +#endif +void MdkSetKey(const char* key) +{ + if (!key) + return; + mdk::SetGlobalOption("MDK_KEY", key); +} + FVP_EXPORT void MdkCallbacksRegisterPort(int64_t handle, void* post_c_object, int64_t send_port) { const auto postCObject = reinterpret_cast(post_c_object); @@ -79,6 +94,9 @@ FVP_EXPORT void MdkCallbacksRegisterPort(int64_t handle, void* post_c_object, in return; } }); +#ifdef FVP_VERSION + clog << "fvp plugin version: " FVP_VERSION << endl; +#endif return; } auto player = make_shared(handle); @@ -248,6 +266,72 @@ FVP_EXPORT void MdkCallbacksRegisterPort(int64_t handle, void* post_c_object, in return p->data[type].mediaStatus.ret; }); + player->onSubtitleText([=](double start, double end, const std::vector& texts){ + auto sp = wp.lock(); + if (!sp) + return; + auto p = sp.get(); + const auto type = int(CallbackType::SubtitleText); + if (!(p->callbackTypes & (1 << type))) + return; + + Dart_CObject t{ + .type = Dart_CObject_kInt64, + .value = { + .as_int64 = type, + } + }; + Dart_CObject v0{ + .type = Dart_CObject_kDouble, + .value = { + .as_double = start, + } + }; + Dart_CObject v1{ + .type = Dart_CObject_kDouble, + .value = { + .as_double = end, + } + }; + std::vector textObjs; + std::vector textObjPtrs; + textObjs.reserve(texts.size()); + textObjPtrs.reserve(texts.size()); + for (const auto& s : texts) { + Dart_CObject txt{ + .type = Dart_CObject_kString, + .value = { + .as_string = s.data(), + } + }; + textObjs.push_back(txt); + textObjPtrs.push_back(&textObjs.back()); + } + Dart_CObject textArray{ + .type = Dart_CObject_kArray, + .value = { + .as_array = { + .length = (int)textObjPtrs.size(), + .values = textObjPtrs.data(), + }, + } + }; + Dart_CObject* arr[] = { &t, &v0, &v1, &textArray }; + Dart_CObject msg { + .type = Dart_CObject_kArray, + .value = { + .as_array = { + .length = std::size(arr), + .values = arr, + }, + } + }; + if (!postCObject(send_port, &msg)) { + clog << __func__ << __LINE__ << "postCObject error" << endl; + return; + } + }); + } FVP_EXPORT void MdkCallbacksUnregisterPort(int64_t handle) @@ -428,7 +512,9 @@ FVP_EXPORT bool MdkSeek(int64_t handle, int64_t pos, int64_t seekFlags, void* po }); } -FVP_EXPORT bool MdkSnapshot(int64_t handle, int w, int h, void* post_c_object, int64_t send_port) +extern "C" void* MdkGetPlayerVid(int64_t texId); + +FVP_EXPORT bool MdkSnapshot(int64_t handle, int64_t texId, int w, int h, void* post_c_object, int64_t send_port) { const auto it = players.find(handle); if (it == players.cend()) { @@ -472,6 +558,10 @@ FVP_EXPORT bool MdkSnapshot(int64_t handle, int w, int h, void* post_c_object, i return {}; } return {}; - }); + } +#ifdef __ANDROID__ + , MdkGetPlayerVid(texId) +#endif + ); return true; } diff --git a/lib/src/callbacks.h b/lib/src/callbacks.h index 4357bd9..bb48c74 100644 --- a/lib/src/callbacks.h +++ b/lib/src/callbacks.h @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Wang Bin. All rights reserved. +// Copyright 2022-2026 Wang Bin. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -15,9 +15,10 @@ #ifdef _WIN32 #define FVP_EXPORT FVP_EXTERN_C __declspec(dllexport) #else -#define FVP_EXPORT FVP_EXTERN_C __attribute__((visibility("default"))) +#define FVP_EXPORT FVP_EXTERN_C __attribute__((visibility("default"))) // will be built with objc compiler, c++ attribute can not be used #endif +FVP_EXPORT void MdkSetKey(const char* key); FVP_EXPORT void MdkCallbacksRegisterPort(int64_t handle, void* post_c_object, int64_t send_port); FVP_EXPORT void MdkCallbacksUnregisterPort(int64_t handle); FVP_EXPORT void MdkCallbacksRegisterType(int64_t handle, int type, bool reply); @@ -25,7 +26,7 @@ FVP_EXPORT void MdkCallbacksUnregisterType(int64_t handle, int type); FVP_EXPORT void MdkCallbacksReplyType(int64_t handle, int type, const void* data); FVP_EXPORT bool MdkPrepare(int64_t handle, int64_t pos, int64_t seekFlag, void* post_c_object, int64_t send_port);// prepare() with a callback to post result to dart to set Completer FVP_EXPORT bool MdkSeek(int64_t handle, int64_t pos, int64_t seekFlag, void* post_c_object, int64_t send_port); -FVP_EXPORT bool MdkSnapshot(int64_t handle, int w, int h, void* post_c_object, int64_t send_port); +FVP_EXPORT bool MdkSnapshot(int64_t handle, int64_t texId, int w, int h, void* post_c_object, int64_t send_port); enum CallbackType { Event, // not a callback, no need to wait for reply @@ -36,6 +37,7 @@ enum CallbackType { Log, Seek, // no register, one time callback Snapshot, // no register, one time callback + SubtitleText, Count, }; diff --git a/lib/src/controller.dart b/lib/src/controller.dart index acffc6e..95c9443 100644 --- a/lib/src/controller.dart +++ b/lib/src/controller.dart @@ -1,11 +1,15 @@ // ignore_for_file: invalid_use_of_visible_for_testing_member // see https://github.com/ardera/flutter_packages/blob/main/packages/flutterpi_gstreamer_video_player/lib/src/controller.dart +import 'dart:io'; import 'dart:typed_data'; import 'package:video_player/video_player.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; +import 'media_info.dart' + if (dart.library.js_interop) 'media_info_dummy.dart' + if (dart.library.html) 'media_info_dummy.dart'; import 'video_player_mdk.dart' if (dart.library.js_interop) 'video_player_dummy.dart' if (dart.library.html) 'video_player_dummy.dart'; @@ -13,8 +17,9 @@ import 'video_player_mdk.dart' MdkVideoPlayerPlatform get _platform { if (VideoPlayerPlatform.instance is! MdkVideoPlayerPlatform) { throw StateError( - '`VideoPlayerPlatform.instance` have to be of `MdkVideoPlayerPlatform` to use advanced video player features.' - 'Make sure you\'ve called `fvp.registerWith()`'); + '`VideoPlayerPlatform.instance` have to be of `MdkVideoPlayerPlatform` to use advanced video player features.' + 'Make sure you\'ve called `fvp.registerWith()`', + ); } return VideoPlayerPlatform.instance as MdkVideoPlayerPlatform; } @@ -23,22 +28,56 @@ MdkVideoPlayerPlatform get _platform { /// /// All methods in this extension must be called after initialized, otherwise no effect. extension FVPControllerExtensions on VideoPlayerController { +/* + static int Function(VideoPlayerController)? _idGetter; + int _getId(VideoPlayerController controller) { + _idGetter ??= _getIdFunc(); + return _idGetter!(this); + } + int Function(VideoPlayerController) _getIdFunc() { + //final dynamic self = this; + try { // try to get textureId + //final _ = self.textureId; + final _ = (this as dynamic).textureId; + return (dynamic c) => c.textureId; + } on NoSuchMethodError { // since video_player 2.10.0 to support platform view + return (dynamic c) => c.playerId; + } + } + // extension can't override existing method, e.g. `dynamic noSuchMethod(Invocation invocation)` +*/ + static final int Function(VideoPlayerController c) _getId = () { + // prefer playerId when available(since video_player 2.10.0 to support platform view), fallback to textureId for older video_player versions + try { + // try to get playerId. static implies late, but can't access this + final _ = (VideoPlayerController.file(File('')) as dynamic).playerId; + return (dynamic c) => c.playerId as int; + } on NoSuchMethodError { + return (dynamic c) => c.textureId as int; + } + }(); + /// Indicates whether current media is a live stream or not bool isLive() { - return _platform.isLive(textureId); + return _platform.isLive(_getId(this)); + } + + /// Get current media info. + MediaInfo? getMediaInfo() { + return _platform.getMediaInfo(_getId(this)); } /// set additional properties /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#void-setpropertyconst-stdstring-key-const-stdstring-value void setProperty(String name, String value) { - _platform.setProperty(textureId, name, value); + _platform.setProperty(_getId(this), name, value); } /// Change video decoder list on the fly. /// NOTE: the default decoder list used by [VideoPlayerController] constructor MUST set via [registerWith]. /// Detail: https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#void-setdecodersmediatype-type-const-stdvectorstdstring-names void setVideoDecoders(List value) { - _platform.setVideoDecoders(textureId, value); + _platform.setVideoDecoders(_getId(this), value); } /// Start to record if [to] is not null. Stop recording if [to] is null. @@ -46,7 +85,7 @@ extension FVPControllerExtensions on VideoPlayerController { /// If not stopped by user, recording will be stopped when playback is finished. /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#void-recordconst-char-url--nullptr-const-char-format--nullptr void record({String? to, String? format}) { - _platform.record(textureId, to: to, format: format); + _platform.record(_getId(this), to: to, format: format); } /// Take a snapshot for current rendered frame. @@ -55,13 +94,13 @@ extension FVPControllerExtensions on VideoPlayerController { /// [height] snapshot height. if not set, result is `mediaInfo.video[current_track].codec.height` /// Return rgba data of image size [width]x[height], stride is `width*4` Future snapshot({int? width, int? height}) async { - return _platform.snapshot(textureId, width: width, height: height); + return _platform.snapshot(_getId(this), width: width, height: height); } /// Set position range in milliseconds. Can be used by A-B loop. /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#void-setrangeint64_t-a-int64_t-b--int64_max void setRange({required int from, int to = -1}) { - _platform.setRange(textureId, from: from, to: to); + _platform.setRange(_getId(this), from: from, to: to); } /// Set duration range(milliseconds) of buffered data. @@ -77,76 +116,105 @@ extension FVPControllerExtensions on VideoPlayerController { /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#void-setbufferrangeint64_t-minms-int64_t-maxms-bool-drop--false /// NOTE: default values are set in [VideoPlayerController] constructor if 'lowLatency' is enabled in [registerWith] void setBufferRange({int min = -1, int max = -1, bool drop = false}) { - _platform.setBufferRange(textureId, min: min, max: max, drop: drop); + _platform.setBufferRange(_getId(this), min: min, max: max, drop: drop); } /// fast seek to a key frame Future fastSeekTo(Duration position) async { - return _platform.fastSeekTo(textureId, position); + return _platform.fastSeekTo(_getId(this), position); } /// Step forward or backward. - /// Step forward if [frame] > 0, backward otherwise. + /// Step forward if [frames] > 0, backward otherwise. Future step({int frames = 1}) async { - return _platform.step(textureId, frames); + return _platform.step(_getId(this), frames); } /// set brightness. -1 <= [value] <= 1 void setBrightness(double value) { - _platform.setBrightness(textureId, value); + _platform.setBrightness(_getId(this), value); } /// set contrast. -1 <= [value] <= 1 void setContrast(double value) { - _platform.setContrast(textureId, value); + _platform.setContrast(_getId(this), value); } /// set hue. -1 <= [value] <= 1 void setHue(double value) { - _platform.setHue(textureId, value); + _platform.setHue(_getId(this), value); } /// set saturation. -1 <= [value] <= 1 void setSaturation(double value) { - _platform.setSaturation(textureId, value); + _platform.setSaturation(_getId(this), value); + } + + /// Set a program to play. used by mpegts programs or hls. + /// [programId] is the index in [MediaInfo.programs] + /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#void-setactivetracksmediatype-type-const-stdsetint-tracks + void setProgram(int programId) { + _platform.setProgram(_getId(this), programId); } /// Set active audio tracks. Other tracks will be disabled. /// The tracks can be from data source from [VideoPlayerController] constructor, or an external audio data source via [setExternalAudio] /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#void-setactivetracksmediatype-type-const-stdsetint-tracks void setAudioTracks(List value) { - _platform.setAudioTracks(textureId, value); + _platform.setAudioTracks(_getId(this), value); + } + + /// Get active audio tracks. + List? getActiveAudioTracks() { + return _platform.getActiveAudioTracks(_getId(this)); } /// Set active video tracks. Other tracks will be disabled. /// The tracks can be from data source from [VideoPlayerController] constructor, or an external video data source via [setExternalVideo] /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#void-setactivetracksmediatype-type-const-stdsetint-tracks void setVideoTracks(List value) { - _platform.setVideoTracks(textureId, value); + _platform.setVideoTracks(_getId(this), value); + } + + /// Get active video tracks. + List? getActiveVideoTracks() { + return _platform.getActiveVideoTracks(_getId(this)); } /// Set active subtitle tracks. Other tracks will be disabled. /// The tracks can be from data source from [VideoPlayerController] constructor, or an external subtitle data source via [setExternalSubtitle] /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#void-setactivetracksmediatype-type-const-stdsetint-tracks void setSubtitleTracks(List value) { - _platform.setSubtitleTracks(textureId, value); + _platform.setSubtitleTracks(_getId(this), value); + } + + /// Get active subtitle tracks. + List? getActiveSubtitleTracks() { + return _platform.getActiveSubtitleTracks(_getId(this)); } /// set an external audio data source /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#void-setmediaconst-char-url-mediatype-type void setExternalAudio(String uri) { - _platform.setExternalAudio(textureId, uri); + _platform.setExternalAudio(_getId(this), uri); } /// set an external video data source /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#void-setmediaconst-char-url-mediatype-type void setExternalVideo(String uri) { - _platform.setExternalVideo(textureId, uri); + _platform.setExternalVideo(_getId(this), uri); } /// set an external subtitle data source /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#void-setmediaconst-char-url-mediatype-type void setExternalSubtitle(String uri) { - _platform.setExternalSubtitle(textureId, uri); + _platform.setExternalSubtitle(_getId(this), uri); + } + + /// Set a callback to receive subtitle text when active subtitle track renders a new piece of text. + /// [start] and [end] are in seconds. [text] is the subtitle text lines. Pass null to disable. + void onSubtitleText( + void Function(double start, double end, List text)? callback) { + _platform.onSubtitleText(_getId(this), callback); } } diff --git a/lib/src/extensions.dart b/lib/src/extensions.dart index b4d77d2..e74aa89 100644 --- a/lib/src/extensions.dart +++ b/lib/src/extensions.dart @@ -17,6 +17,21 @@ extension PlatformEx on Platform { return Libfvp.isEmulator(); } + // TODO: check content /proc/device-tree/model? + static bool isRockchip() { + if (!Platform.isLinux) { + return false; + } + return File('/dev/mpp_service').existsSync(); + } + + static bool isRaspberryPi() { + if (!Platform.isLinux) { + return false; + } + return File('/dev/vchiq').existsSync(); + } + static String assetUri(String asset, {String? package}) { final key = asset; switch (Platform.operatingSystem) { @@ -34,6 +49,8 @@ extension PlatformEx on Platform { 'Frameworks', 'App.framework', 'flutter_assets', key); case 'android': return 'assets://flutter_assets/$key'; + case 'ohos': + return 'rawfile://flutter_assets/$key'; } return asset; } diff --git a/lib/src/generated_bindings.dart b/lib/src/generated_bindings.dart index a66a7c9..0697960 100644 --- a/lib/src/generated_bindings.dart +++ b/lib/src/generated_bindings.dart @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2024 WangBin +// Copyright (c) 2019-2025 WangBin // https://github.com/wang-bin/mdk-sdk // AUTO GENERATED FILE, DO NOT EDIT. // @@ -395,6 +395,72 @@ class NativeLibrary { bool Function( ffi.Pointer, ffi.Pointer)>(); + ffi.Pointer mdkAudioFrameAPI_new( + int format, + int channels, + int sampleRate, + int samples, + ) { + return _mdkAudioFrameAPI_new( + format, + channels, + sampleRate, + samples, + ); + } + + late final _mdkAudioFrameAPI_newPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Int32, ffi.Int, ffi.Int, ffi.Int)>>('mdkAudioFrameAPI_new'); + late final _mdkAudioFrameAPI_new = _mdkAudioFrameAPI_newPtr + .asFunction Function(int, int, int, int)>(); + + void mdkAudioFrameAPI_delete( + ffi.Pointer> arg0, + ) { + return _mdkAudioFrameAPI_delete( + arg0, + ); + } + + late final _mdkAudioFrameAPI_deletePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer>)>>( + 'mdkAudioFrameAPI_delete'); + late final _mdkAudioFrameAPI_delete = _mdkAudioFrameAPI_deletePtr + .asFunction>)>(); + + ffi.Pointer mdkAudioFrameAPI_ref( + ffi.Pointer p, + ) { + return _mdkAudioFrameAPI_ref( + p, + ); + } + + late final _mdkAudioFrameAPI_refPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('mdkAudioFrameAPI_ref'); + late final _mdkAudioFrameAPI_ref = _mdkAudioFrameAPI_refPtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); + + void mdkAudioFrameAPI_unref( + ffi.Pointer> pp, + ) { + return _mdkAudioFrameAPI_unref( + pp, + ); + } + + late final _mdkAudioFrameAPI_unrefPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer>)>>( + 'mdkAudioFrameAPI_unref'); + late final _mdkAudioFrameAPI_unref = _mdkAudioFrameAPI_unrefPtr + .asFunction>)>(); + ffi.Pointer mdkVideoFrameAPI_new( int width, int height, @@ -429,6 +495,36 @@ class NativeLibrary { late final _mdkVideoFrameAPI_delete = _mdkVideoFrameAPI_deletePtr .asFunction>)>(); + ffi.Pointer mdkVideoFrameAPI_ref( + ffi.Pointer p, + ) { + return _mdkVideoFrameAPI_ref( + p, + ); + } + + late final _mdkVideoFrameAPI_refPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('mdkVideoFrameAPI_ref'); + late final _mdkVideoFrameAPI_ref = _mdkVideoFrameAPI_refPtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); + + void mdkVideoFrameAPI_unref( + ffi.Pointer> pp, + ) { + return _mdkVideoFrameAPI_unref( + pp, + ); + } + + late final _mdkVideoFrameAPI_unrefPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer>)>>( + 'mdkVideoFrameAPI_unref'); + late final _mdkVideoFrameAPI_unref = _mdkVideoFrameAPI_unrefPtr + .asFunction>)>(); + void mdkVideoBufferPoolFree( ffi.Pointer> pool, ) { @@ -469,6 +565,23 @@ class NativeLibrary { late final _mdkPlayerAPI_delete = _mdkPlayerAPI_deletePtr .asFunction>)>(); + void mdkPlayerAPI_reset( + ffi.Pointer> arg0, + bool release, + ) { + return _mdkPlayerAPI_reset( + arg0, + release, + ); + } + + late final _mdkPlayerAPI_resetPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer>, + ffi.Bool)>>('mdkPlayerAPI_reset'); + late final _mdkPlayerAPI_reset = _mdkPlayerAPI_resetPtr.asFunction< + void Function(ffi.Pointer>, bool)>(); + void MDK_foreignGLContextDestroyed() { return _MDK_foreignGLContextDestroyed(); } @@ -547,6 +660,7 @@ abstract class MDKSeekFlag { static const int MDK_SeekFlag_FromNow = 4; static const int MDK_SeekFlag_Frame = 64; static const int MDK_SeekFlag_KeyFrame = 256; + static const int MDK_SeekFlag_AnyFrame = 512; static const int MDK_SeekFlag_Fast = 256; static const int MDK_SeekFlag_InCache = 1024; static const int MDK_SeekFlag_Backward = 65536; @@ -556,11 +670,14 @@ abstract class MDKSeekFlag { /// ! /// \brief VideoEffect /// per video renderer effect. set via Player.setVideoEffect(MDK_VideoEffect effect, const float*); +/// Only one(the last call) of ScaleChannels or ShiftChannels will be applied abstract class MDK_VideoEffect { static const int MDK_VideoEffect_Brightness = 0; static const int MDK_VideoEffect_Contrast = 1; static const int MDK_VideoEffect_Hue = 2; static const int MDK_VideoEffect_Saturation = 3; + static const int MDK_VideoEffect_ScaleChannels = 4; + static const int MDK_VideoEffect_ShiftChannels = 5; } abstract class MDK_ColorSpace { @@ -571,6 +688,7 @@ abstract class MDK_ColorSpace { static const int MDK_ColorSpace_ExtendedLinearDisplayP3 = 4; static const int MDK_ColorSpace_ExtendedSRGB = 5; static const int MDK_ColorSpace_ExtendedLinearSRGB = 6; + static const int MDK_ColorSpace_BT2100_HLG = 7; } abstract class MDK_LogLevel { @@ -736,7 +854,13 @@ final class mdkVideoCodecParameters extends ffi.Struct { @ffi.Float() external double par; - @ffi.Array.multi([128]) + @ffi.Int32() + external int color_space; + + @ffi.Uint8() + external int dovi_profile; + + @ffi.Array.multi([123]) external ffi.Array reserved; } @@ -860,6 +984,103 @@ final class mdkMediaInfo extends ffi.Struct { external int nb_programs; } +final class mdkAudioFrame extends ffi.Opaque {} + +abstract class MDK_SampleFormat { + static const int MDK_SampleFormat_Unknown = 0; + static const int MDK_SampleFormat_U8 = 1; + static const int MDK_SampleFormat_U8P = 2; + static const int MDK_SampleFormat_S16 = 3; + static const int MDK_SampleFormat_S16P = 4; + static const int MDK_SampleFormat_S32 = 5; + static const int MDK_SampleFormat_S32P = 6; + static const int MDK_SampleFormat_F32 = 7; + static const int MDK_SampleFormat_F32P = 8; + static const int MDK_SampleFormat_F64 = 9; + static const int MDK_SampleFormat_F64P = 10; +} + +final class mdkAudioFrameAPI extends ffi.Struct { + external ffi.Pointer object; + + external ffi + .Pointer)>> + planeCount; + + external ffi.Pointer< + ffi.NativeFunction)>> + sampleFormat; + + external ffi.Pointer< + ffi.NativeFunction)>> + channelMask; + + external ffi + .Pointer)>> + channels; + + external ffi + .Pointer)>> + sampleRate; + + external ffi.Pointer< + ffi.NativeFunction< + ffi.Bool Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size, + ffi.Int, + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer>)>>)>> addBuffer; + + external ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer>, ffi.Int)>> setBuffers; + + external ffi.Pointer< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer, ffi.Int)>> bufferData; + + external ffi + .Pointer)>> + bytesPerPlane; + + external ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, ffi.Int)>> + setSamplesPerChannel; + + external ffi + .Pointer)>> + samplesPerChannel; + + external ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, ffi.Double)>> + setTimestamp; + + external ffi.Pointer< + ffi.NativeFunction)>> + timestamp; + + external ffi.Pointer< + ffi.NativeFunction)>> + duration; + + external ffi.Pointer< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer, ffi.Int32, ffi.Int, ffi.Int)>> to; + + @ffi.Array.multi([8]) + external ffi.Array> reserved; +} + final class mdkDX11Resource extends ffi.Struct { @ffi.Int() external int size; @@ -868,10 +1089,18 @@ final class mdkDX11Resource extends ffi.Struct { @ffi.Int() external int subResource; + + @ffi.Array.multi([4]) + external ffi.Array> plane; + + @ffi.Int() + external int planeCount; } final class ID3D11DeviceChild extends ffi.Opaque {} +final class ID3D11Texture2D extends ffi.Opaque {} + final class mdkDX9Resource extends ffi.Struct { @ffi.Int() external int size; @@ -881,6 +1110,28 @@ final class mdkDX9Resource extends ffi.Struct { final class IDirect3DSurface9 extends ffi.Opaque {} +final class mdkVAAPIResource extends ffi.Struct { + @ffi.Int() + external int size; + + @VASurfaceID() + external int surface; + + external VADisplay display; + + external ffi.Pointer x11Display; + + external ffi.Pointer opaque; + + external ffi.Pointer< + ffi.NativeFunction opaque)>> + unref; +} + +typedef VASurfaceID = ffi.UnsignedInt; +typedef DartVASurfaceID = int; +typedef VADisplay = ffi.Pointer; + final class mdkVideoBufferPool extends ffi.Opaque {} final class mdkVideoFrame extends ffi.Opaque {} @@ -916,6 +1167,36 @@ abstract class MDK_PixelFormat { static const int MDK_PixelFormat_BGRAF32 = 25; } +final class mdkCUDAResource extends ffi.Struct { + @ffi.Int() + external int size; + + @ffi.Array.multi([4]) + external ffi.Array> ptr; + + @ffi.Int() + external int width; + + @ffi.Int() + external int height; + + @ffi.Array.multi([4]) + external ffi.Array stride; + + @ffi.Int32() + external int format; + + external ffi.Pointer context; + + external ffi.Pointer stream; + + external ffi.Pointer opaque; + + external ffi.Pointer< + ffi.NativeFunction opaque)>> + unref; +} + final class mdkVideoFrameAPI extends ffi.Struct { external ffi.Pointer object; @@ -1011,21 +1292,55 @@ final class mdkVideoFrameAPI extends ffi.Struct { ffi.Int, ffi.Int)>> fromDX9; - external ffi.Pointer> fromDX12; + external ffi.Pointer< + ffi.NativeFunction< + ffi.Bool Function( + ffi.Pointer, + ffi.Pointer>, + ffi.Pointer, + ffi.Int, + ffi.Int)>> fromVAAPI; - external ffi.Pointer> fromMetal; + external ffi.Pointer< + ffi.NativeFunction< + ffi.Bool Function( + ffi.Pointer, + ffi.Pointer>, + ffi.Pointer, + ffi.Int, + ffi.Int)>> fromCUDA; + + external ffi.Pointer< + ffi.NativeFunction< + ffi.Bool Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>> getDX11; + + external ffi + .Pointer)>> + rotation; + + external ffi.Pointer< + ffi.NativeFunction< + ffi.Pointer Function(ffi.Pointer, + ffi.Pointer, ffi.Pointer)>> metadata; external ffi.Pointer> fromVk; external ffi.Pointer> fromGL; + external ffi.Pointer> fromDX12; + external ffi.Pointer< ffi.NativeFunction)>> toHost; - @ffi.Array.multi([12]) + @ffi.Array.multi([8]) external ffi.Array> reserved; } +final class ID3D11Device extends ffi.Opaque {} + abstract class MDK_RenderAPI { static const int MDK_RenderAPI_Invalid = 0; static const int MDK_RenderAPI_OpenGL = 1; @@ -1037,8 +1352,6 @@ abstract class MDK_RenderAPI { final class mdkRenderAPI extends ffi.Opaque {} -final class mdkAudioFrame extends ffi.Opaque {} - final class mdkPlayer extends ffi.Opaque {} abstract class MDK_SurfaceType { @@ -1080,6 +1393,9 @@ final class mdkRenderCallback extends ffi.Struct { } final class mdkVideoCallback extends ffi.Struct { + /// ! + /// \brief cb + /// \return pending number of frames. For most filters, 1 input frame generates 1 output frame, then return 0. external ffi.Pointer< ffi.NativeFunction< ffi.Int Function(ffi.Pointer> pFrame, @@ -1088,6 +1404,15 @@ final class mdkVideoCallback extends ffi.Struct { external ffi.Pointer opaque; } +final class mdkAudioCallback extends ffi.Struct { + external ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer> pFrame, + ffi.Int track, ffi.Pointer opaque)>> cb; + + external ffi.Pointer opaque; +} + final class mdkSeekCallback extends ffi.Struct { external ffi.Pointer< ffi.NativeFunction< @@ -1167,6 +1492,24 @@ final class mdkSyncCallback extends ffi.Struct { external ffi.Pointer opaque; } +final class mdkSubtitleCallback extends ffi.Struct { + external ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer text, ffi.Pointer opaque)>> cb; + + external ffi.Pointer opaque; + + external ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function( + ffi.Double start, + ffi.Double end, + ffi.Pointer> texts, + ffi.Int textCount, + ffi.Pointer opaque)>> cb2; +} + final class mdkPlayerAPI extends ffi.Struct { external ffi.Pointer object; @@ -1414,9 +1757,9 @@ final class mdkPlayerAPI extends ffi.Struct { ffi.NativeFunction< ffi.Void Function(ffi.Pointer, mdkVideoCallback)>> onVideo; - external ffi - .Pointer)>> - onAudio; + external ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, mdkAudioCallback)>> onAudio; external ffi.Pointer< ffi.NativeFunction< @@ -1721,6 +2064,32 @@ final class mdkPlayerAPI extends ffi.Struct { ffi.Int Function( ffi.Pointer, ffi.Pointer, ffi.Int)>> bufferedTimeRanges; + + external ffi.Pointer< + ffi.NativeFunction< + ffi.Bool Function(ffi.Pointer, ffi.Pointer, + ffi.Size, ffi.Int)>> appendBuffer; + + external ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, ffi.Double, ffi.Int, + mdkSubtitleCallback)>> subtitleText; + + /// ! + /// \brief setAudioMix + /// Audio channel map or remix. + /// \param mat map or mix coefficients matrix. matrix[i + cols * o] is the weight of input channel i in output channel o. + /// \param rows mat rows, output channel count + /// \param cols mat colums, input channel count to use + external ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, ffi.Pointer, + ffi.Int, ffi.Int)>> setAudioMix; + + external ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, mdkSubtitleCallback, + ffi.Bool, ffi.Pointer)>> onSubtitleText; } final class SwitchBitrateCallback extends ffi.Struct { @@ -1754,8 +2123,8 @@ final class UnnamedUnion2 extends ffi.Union { const int MDK_MAJOR = 0; -const int MDK_MINOR = 28; +const int MDK_MINOR = 35; const int MDK_MICRO = 0; -const int MDK_VERSION = 7168; +const int MDK_VERSION = 8960; diff --git a/lib/src/global.dart b/lib/src/global.dart index b3159e9..7c50a70 100644 --- a/lib/src/global.dart +++ b/lib/src/global.dart @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Wang Bin. All rights reserved. +// Copyright 2022-2025 Wang Bin. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:ffi'; @@ -148,10 +148,39 @@ enum ColorSpace { unknown(MDK_ColorSpace.MDK_ColorSpace_Unknown), bt709(MDK_ColorSpace.MDK_ColorSpace_BT709), bt2100PQ(MDK_ColorSpace.MDK_ColorSpace_BT2100_PQ), + scrgb(MDK_ColorSpace.MDK_ColorSpace_scRGB), + bt2100hlg(MDK_ColorSpace.MDK_ColorSpace_BT2100_HLG), ; final int rawValue; const ColorSpace(this.rawValue); + + factory ColorSpace.from(int rawValue) { + switch (rawValue) { + case MDK_ColorSpace.MDK_ColorSpace_Unknown: + return unknown; + case MDK_ColorSpace.MDK_ColorSpace_BT709: + return bt709; + case MDK_ColorSpace.MDK_ColorSpace_BT2100_PQ: + return bt2100PQ; + case MDK_ColorSpace.MDK_ColorSpace_scRGB: + return scrgb; + case MDK_ColorSpace.MDK_ColorSpace_BT2100_HLG: + return bt2100hlg; + default: + return unknown; + } + } +} + +/// https://github.com/wang-bin/mdk-sdk/wiki/Types#enum-mapdirection +enum MapDirection { + frameToViewport(MDK_MapDirection.MDK_MapDirection_FrameToViewport), + viewportToFrame(MDK_MapDirection.MDK_MapDirection_ViewportToFrame), + ; + + final int rawValue; + const MapDirection(this.rawValue); } enum LogLevel { diff --git a/lib/src/lib.dart b/lib/src/lib.dart index 026f631..c8cebf3 100644 --- a/lib/src/lib.dart +++ b/lib/src/lib.dart @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Wang Bin. All rights reserved. +// Copyright 2022-2026 Wang Bin. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:ffi'; @@ -18,6 +18,7 @@ abstract class Libmdk { case 'linux': name = 'libmdk.so.0'; case 'android': + case 'ohos': name = 'libmdk.so'; default: throw Exception( @@ -38,11 +39,13 @@ abstract class Libfvp { static DynamicLibrary _load() { String name; if (Platform.isWindows) { - name = 'fvp_plugin.dll'; + name = 'fvp.dll'; } else if (Platform.isIOS || Platform.isMacOS) { name = 'fvp.framework/fvp'; - } else if (Platform.isAndroid || Platform.isLinux) { - name = 'libfvp_plugin.so'; + } else if (Platform.isAndroid || + Platform.isLinux || + Platform.operatingSystem == 'ohos') { + name = 'libfvp.so'; } else { throw Exception( 'Unsupported operating system: ${Platform.operatingSystem}.', @@ -56,6 +59,8 @@ abstract class Libfvp { } static final instance = _load(); + static final setKey = instance.lookupFunction), + void Function(Pointer)>('MdkSetKey'); static final registerPort = instance.lookupFunction< Void Function(Int64, Pointer, Int64), void Function(int, Pointer, int)>('MdkCallbacksRegisterPort'); @@ -78,8 +83,10 @@ abstract class Libfvp { Bool Function(Int64, Int64, Int64, Pointer, Int64), bool Function(int, int, int, Pointer, int)>('MdkSeek'); static final snapshot = instance.lookupFunction< - Bool Function(Int64, Int, Int, Pointer, Int64), - bool Function(int, int, int, Pointer, int)>('MdkSnapshot'); + Bool Function(Int64, Int64, Int, Int, Pointer, Int64), + bool Function(int, int, int, int, Pointer, int)>('MdkSnapshot'); static final isEmulator = instance .lookupFunction('MdkIsEmulator'); + static final getVid = instance.lookupFunction Function(Int64), + Pointer Function(int)>('MdkGetPlayerVid'); } diff --git a/lib/src/media_info.dart b/lib/src/media_info.dart index 2f799bb..888736f 100644 --- a/lib/src/media_info.dart +++ b/lib/src/media_info.dart @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Wang Bin. All rights reserved. +// Copyright 2022-2025 Wang Bin. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:ffi'; @@ -7,6 +7,7 @@ import 'package:ffi/ffi.dart'; import 'generated_bindings.dart'; import 'lib.dart'; +import 'global.dart'; class CodecParameters { /// codec name @@ -118,6 +119,9 @@ class VideoCodecParameters extends CodecParameters { /// pixel aspect ratio double par = 1.0; + var colorSpace = ColorSpace.unknown; + var doviProfile = 0; + VideoCodecParameters(); VideoCodecParameters._from(mdkVideoCodecParameters cp) { @@ -138,6 +142,8 @@ class VideoCodecParameters extends CodecParameters { if (cp.par > 0) { par = cp.par; } + colorSpace = ColorSpace.from(cp.color_space); + doviProfile = cp.dovi_profile; } @override @@ -292,7 +298,10 @@ class MediaInfo { var startTime = 0; /// duration in milliseconds. may be 0, for example live stream. + /// duration may change when playing a stream being recorded var duration = 0; + + /// when stream is loaded, the value from container. when playing, it's updated to the realtime value var bitRate = 0; /// format or container name, for example mp4, flv diff --git a/lib/src/media_info_dummy.dart b/lib/src/media_info_dummy.dart new file mode 100644 index 0000000..53314fb --- /dev/null +++ b/lib/src/media_info_dummy.dart @@ -0,0 +1,2 @@ +/// A dummy class for web +class MediaInfo {} diff --git a/lib/src/player.dart b/lib/src/player.dart index d01a67c..ae77f87 100644 --- a/lib/src/player.dart +++ b/lib/src/player.dart @@ -1,8 +1,9 @@ -// Copyright 2022-2024 Wang Bin. All rights reserved. +// Copyright 2022-2026 Wang Bin. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:ffi'; +import 'dart:io'; import 'dart:isolate'; import 'dart:math'; import 'dart:ui' as ui; @@ -37,8 +38,8 @@ class Player { final category = message[2] as String; final detail = message[3] as String; final ev = MediaEvent(error, category, detail); - for (final cb in _eventCb) { - cb(ev); + if (_eventCb.hasListener) { + _eventCb.add(ev); } } case 1: @@ -46,8 +47,11 @@ class Player { // state final oldValue = message[1] as int; final newValue = message[2] as int; - for (final cb in _stateCb) { - cb(PlaybackState.from(oldValue), PlaybackState.from(newValue)); + if (_stateCb.hasListener) { + _stateCb.add(( + oldValue: PlaybackState.from(oldValue), + newValue: PlaybackState.from(newValue) + )); } Libfvp.replyType(nativeHandle, type, nullptr); } @@ -57,8 +61,11 @@ class Player { final oldValue = message[1] as int; final newValue = message[2] as int; bool ret = true; - for (var cb in _statusCb) { - ret = cb(MediaStatus(oldValue), MediaStatus(newValue)) && ret; + if (_statusCb.hasListener) { + _statusCb.add(( + oldValue: MediaStatus(oldValue), + newValue: MediaStatus(newValue) + )); } rep.ref.mediaStatus.ret = ret; Libfvp.replyType(nativeHandle, type, rep.cast()); @@ -107,16 +114,26 @@ class Player { } _snapshot = null; } + case 8: + { + final start = message[1] as double; + final end = message[2] as double; + final texts = (message[3] as List).cast(); + _subtitleCb?.call(start, end, texts); + } } calloc.free(rep); }); Libfvp.registerPort(nativeHandle, NativeApi.postCObject.cast(), _receivePort.sendPort.nativePort); - onStateChanged((oldValue, newValue) { - _state = newValue; + onStateChanged.listen((event) { + _state = event.newValue; }); - onMediaStatus((oldValue, newValue) { + Libfvp.registerType(nativeHandle, 1, false); + onMediaStatus.listen((event) { + final oldValue = event.oldValue; + final newValue = event.newValue; if (!oldValue.test(MediaStatus.loaded) && newValue.test(MediaStatus.loaded)) { _setVideoSize(); @@ -139,9 +156,9 @@ class Player { _videoSize.complete(null); } } - return true; }); - onEvent((e) { + Libfvp.registerType(nativeHandle, 2, false); + onEvent.listen((e) { if (_videoSize.isCompleted) { return; } @@ -149,31 +166,37 @@ class Player { _setVideoSize(); } }); + Libfvp.registerType(nativeHandle, 0, false); } /// Release resources void dispose() async { if (_pp == nullptr) { + textureId.dispose(); return; } // await: ensure no player ref in fvp plugin before mdkPlayerAPI_delete() in dart await updateTexture(width: -1); state = PlaybackState.stopped; Libfvp.unregisterPort(nativeHandle); - onEvent(null); - onStateChanged(null); - onMediaStatus(null); + _eventCb.close(); + Libfvp.unregisterType(nativeHandle, 0); + _stateCb.close(); + Libfvp.unregisterType(nativeHandle, 1); + _statusCb.close(); + Libfvp.unregisterType(nativeHandle, 2); _receivePort.close(); Libmdk.instance.mdkPlayerAPI_delete(_pp); calloc.free(_pp); _pp = nullptr; + textureId.dispose(); } /// Release current texture then create a new one for current [media], and update [textureId]. /// - /// Texture will be created when media is loaded and [mediaInfo.video] is not empty. + /// Texture will be created when media is loaded and mediaInfo.video is not empty. /// If both [width] and [height] are null, texture size is video frame size, otherwise is requested size. Future updateTexture( {int? width, int? height, bool? tunnel, bool? fit}) async { @@ -577,13 +600,13 @@ class Player { void setVideoSurfaceSize(int width, int height) => _player.ref.setVideoSurfaceSize.asFunction< void Function(Pointer, int, int, Pointer)>()( - _player.ref.object, width, height, Pointer.fromAddress(0)); + _player.ref.object, width, height, _getVid()); void setVideoViewport(double x, double y, double width, double height) => _player.ref.setVideoViewport.asFunction< void Function(Pointer, double, double, double, double, Pointer)>()( - _player.ref.object, x, y, width, height, Pointer.fromAddress(0)); + _player.ref.object, x, y, width, height, _getVid()); /// Set video content aspect ratio. No effect if texture width/height == original video frame width/height. /// [value] can be [ignoreAspectRatio], [keepAspectRatio], [keepAspectRatioCrop] and other desired ratio = width/height @@ -591,36 +614,84 @@ class Player { /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#void-setaspectratiofloat-value-void-vo_opaque--nullptr void setAspectRatio(double value) => _player.ref.setAspectRatio.asFunction< void Function(Pointer, double, Pointer)>()( - _player.ref.object, value, Pointer.fromAddress(0)); + _player.ref.object, value, _getVid()); + + /// Set region-of-interest mapping for video display + /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#void-setpointmapconst-float-videoroi-const-float-viewroi--nullptr-int-count--2-void-vo_opaque--nullptr + void setPointMap( + {List? videoRoi, List? viewRoi, int count = 2}) { + final cv = (videoRoi != null) + ? () { + final p = calloc(count * 2); + for (int i = 0; i < videoRoi.length; ++i) { + p[i] = videoRoi[i]; + } + return p; + }() + : nullptr; + final cw = (viewRoi != null) + ? () { + final p = calloc(count * 2); + for (int i = 0; i < viewRoi.length; ++i) { + p[i] = viewRoi[i]; + } + return p; + }() + : nullptr; + _player.ref.setPointMap.asFunction< + void Function(Pointer, Pointer, Pointer, + int, Pointer)>()( + _player.ref.object, cv.cast(), cw.cast(), count, _getVid()); + if (cv != nullptr) { + calloc.free(cv); + } + if (cw != nullptr) { + calloc.free(cw); + } + } - // TODO: mapPoint( List) + /// Map a point between viewport coordinates and video frame coordinates. + /// Returns the mapped coordinates as a record `({double x, double y})`. + /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#void-mappointmapdirection-dir-float-x-float-y-float-z--nullptr-void-vo_opaque--nullptr + ({double x, double y}) mapPoint( + MapDirection direction, { + required double x, + required double y, + }) { + final cx = calloc(); + final cy = calloc(); + final cz = calloc(); + cx.value = x; + cy.value = y; + _player.ref.mapPoint.asFunction< + void Function(Pointer, int, Pointer, + Pointer, Pointer, Pointer)>()( + _player.ref.object, direction.rawValue, cx, cy, cz, _getVid()); + final result = (x: cx.value, y: cy.value); + calloc.free(cx); + calloc.free(cy); + calloc.free(cz); + return result; + } /// rotate video content around the center. [degree] can be 0, 90, 180, 270 in counterclockwise. /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#void-rotateint-degree-void-vo_opaque--nullptr void rotate(int degree) => _player.ref.rotate .asFunction, int, Pointer)>()( - _player.ref.object, degree, Pointer.fromAddress(0)); + _player.ref.object, degree, _getVid()); /// scale video content. 1.0 is no scale. /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#void-scalefloat-x-float-y-void-vo_opaque--nullptr void scale(double x, double y) => _player.ref.scale.asFunction< void Function(Pointer, double, double, Pointer)>()( - _player.ref.object, x, y, Pointer.fromAddress(0)); + _player.ref.object, x, y, _getVid()); /// Set background color. /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#void-setbackgroundcolorfloat-r-float-g-float-b-float-a-void-vo_opaque--nullptr void setBackgroundColor(double r, double g, double b, double a) => _player.ref.setBackgroundColor.asFunction< - void Function(Pointer, double, double, double, double, - Pointer)>()( - _player.ref.object, r, g, b, a, Pointer.fromAddress(0)); - - /// Set background color. - /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#void-setbackgroundcolorfloat-r-float-g-float-b-float-a-void-vo_opaque--nullptr - void setBackground(ui.Color c) => _player.ref.setBackgroundColor.asFunction< void Function(Pointer, double, double, double, double, - Pointer)>()(_player.ref.object, c.red / 255, c.green / 255, - c.blue / 255, c.alpha / 255, Pointer.fromAddress(0)); + Pointer)>()(_player.ref.object, r, g, b, a, _getVid()); /// Set a built-in video effect. /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#player-setvideoeffect-effect-const-float-values-void-vo_opaque--nullptr @@ -632,7 +703,7 @@ class Player { _player.ref.setVideoEffect.asFunction< void Function( Pointer, int, Pointer, Pointer)>()( - _player.ref.object, effect.rawValue, cv.cast(), Pointer.fromAddress(0)); + _player.ref.object, effect.rawValue, cv.cast(), _getVid()); calloc.free(cv); } @@ -641,13 +712,13 @@ class Player { /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#player-setcolorspace-value-void-vo_opaque--nullptr void setColorSpace(ColorSpace value) => _player.ref.setColorSpace .asFunction, int, Pointer)>()( - _player.ref.object, value.rawValue, Pointer.fromAddress(0)); + _player.ref.object, value.rawValue, _getVid()); /// Draw the current video frame and return frame timestamp in seconds. /// Usually NOT used in dart. double renderVideo() => _player.ref.renderVideo .asFunction, Pointer)>()( - _player.ref.object, Pointer.fromAddress(0)); + _player.ref.object, _getVid()); /// Take a snapshot for current rendered frame. /// @@ -659,54 +730,41 @@ class Player { _snapshot?.complete(null); } _snapshot = Completer(); - if (!Libfvp.snapshot(nativeHandle, width ?? 0, height ?? 0, - NativeApi.postCObject.cast(), _receivePort.sendPort.nativePort)) { + if (!Libfvp.snapshot( + nativeHandle, + textureId.value ?? -1, + width ?? 0, + height ?? 0, + NativeApi.postCObject.cast(), + _receivePort.sendPort.nativePort)) { _snapshot!.complete(null); } return _snapshot!.future; } // callbacks - /// Set [MediaEvent] callback. + /// Get a [MediaEvent] stream. /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#player-oneventstdfunctionboolconst-mediaevent-cb-callbacktoken-token--nullptr - void onEvent(void Function(MediaEvent)? callback) { - if (callback == null) { - _eventCb.clear(); - Libfvp.unregisterType(nativeHandle, 0); - } else { - _eventCb.add(callback); - Libfvp.registerType(nativeHandle, 0, false); - } - } + Stream get onEvent => _eventCb.stream; - /// Set a [PlaybackState] change callback. + /// Get a [PlaybackState] change stream. /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#player-onstatechangedstdfunctionvoidstate-cb -// reply: true to let native code wait for dart callback result - void onStateChanged( - void Function(PlaybackState oldValue, PlaybackState newValue)? callback, - {bool reply = false}) { - if (callback == null) { - _stateCb.clear(); - Libfvp.unregisterType(nativeHandle, 1); - } else { - _stateCb.add(callback); - Libfvp.registerType(nativeHandle, 1, reply); - } - } + Stream<({PlaybackState oldValue, PlaybackState newValue})> + get onStateChanged => _stateCb.stream; - /// Add a [MediaStatus] callback or remove all callbacks. + /// Get a [MediaStatus] change stream. /// https://github.com/wang-bin/mdk-sdk/wiki/Player-APIs#player-onmediastatusstdfunctionboolmediastatus-oldvalue-mediastatus-newvalue-cb-callbacktoken-token--nullptr // reply: true to let native code wait for dart callback result, may result in dead lock because when native waiting main isolate reply, main isolate may execute another task(e.g. frequent seekTo) which also acquire the same lock in native -// only the last callback reply parameter works - void onMediaStatus( - bool Function(MediaStatus oldValue, MediaStatus newValue)? callback, - {bool reply = false}) { + Stream<({MediaStatus oldValue, MediaStatus newValue})> get onMediaStatus => + _statusCb.stream; + + void onSubtitleText( + void Function(double start, double end, List text)? callback) { + _subtitleCb = callback; if (callback == null) { - _statusCb.clear(); - Libfvp.unregisterType(nativeHandle, 2); + Libfvp.unregisterType(nativeHandle, 8); } else { - _statusCb.add(callback); - Libfvp.registerType(nativeHandle, 2, reply); + Libfvp.registerType(nativeHandle, 8, false); } } @@ -715,10 +773,19 @@ class Player { // loading=>loaded, then frame decoded return; } - final vc = mediaInfo.video?[0].codec; + var v = mediaInfo.video?[0]; + // we don't support dynamic texture size change, so use the max video codec width + if (v != null) { + for (final i in mediaInfo.video!) { + if (i.codec.width > v!.codec.width) { + v = i; + } + } + } // if no video stream, create a dummy texture of size 16x16 double w = 16; double h = 16; + final vc = v?.codec; if (vc != null) { if (vc.width <= 0 || vc.height <= 0) { // failed to parse video size, e.g. small probesize @@ -726,7 +793,7 @@ class Player { } w = vc.width.toDouble(); h = (vc.height.toDouble() / vc.par).roundToDouble(); - if (mediaInfo.video![0].rotation % 180 == 90) { + if (v!.rotation % 180 == 90) { (w, h) = (h, w); } } @@ -734,6 +801,14 @@ class Player { _videoSize.complete(size); } + Pointer _getVid() { + // currently only android vo_opaque is not null, and may change + if (Platform.isAndroid) { + return Libfvp.getVid(textureId.value ?? -1); + } + return Pointer.fromAddress(0); + } + final _player = Libmdk.instance.mdkPlayerAPI_new(); var _pp = calloc>(); @@ -744,10 +819,12 @@ class Player { Completer? _seeked; final _receivePort = ReceivePort(); - final _eventCb = []; - final _stateCb = []; - final _statusCb = - []; + final _eventCb = StreamController.broadcast(); + final _stateCb = StreamController< + ({PlaybackState oldValue, PlaybackState newValue})>.broadcast(); + final _statusCb = StreamController< + ({MediaStatus oldValue, MediaStatus newValue})>.broadcast(); + Function(double start, double end, List text)? _subtitleCb; Future Function()? _prepareCb; bool _mute = false; diff --git a/lib/src/video_player_dummy.dart b/lib/src/video_player_dummy.dart index e571d30..a04127a 100644 --- a/lib/src/video_player_dummy.dart +++ b/lib/src/video_player_dummy.dart @@ -1,51 +1,75 @@ import 'dart:typed_data'; +import 'media_info_dummy.dart'; + /// A dummy class for web class MdkVideoPlayerPlatform { static void registerVideoPlayerPlatformsWith({dynamic options}) {} // for FVPController - bool isLive(int textureId) { + bool isLive(int playerId) { return false; } - void setProperty(int textureId, String name, String value) {} + MediaInfo? getMediaInfo(int playerId) { + return null; + } + + void setProperty(int playerId, String name, String value) {} - void setAudioDecoders(int textureId, List value) {} + void setAudioDecoders(int playerId, List value) {} - void setVideoDecoders(int textureId, List value) {} + void setVideoDecoders(int playerId, List value) {} - void record(int textureId, {String? to, String? format}) {} + void record(int playerId, {String? to, String? format}) {} - Future snapshot(int textureId, {int? width, int? height}) async { + Future snapshot(int playerId, {int? width, int? height}) async { return null; } - void setRange(int textureId, {required int from, int to = -1}) {} + void setRange(int playerId, {required int from, int to = -1}) {} - void setBufferRange(int textureId, + void setBufferRange(int playerId, {int min = -1, int max = -1, bool drop = false}) {} - Future fastSeekTo(int textureId, Duration position) async {} + Future fastSeekTo(int playerId, Duration position) async {} + + Future step(int playerId, int frames) async {} + + void setBrightness(int playerId, double value) {} + + void setContrast(int playerId, double value) {} + + void setHue(int playerId, double value) {} - Future step(int textureId, int frames) async {} + void setSaturation(int playerId, double value) {} - void setBrightness(int textureId, double value) {} + void setProgram(int playerId, int programId) {} - void setContrast(int textureId, double value) {} + void setAudioTracks(int playerId, List value) {} - void setHue(int textureId, double value) {} + List? getActiveAudioTracks(int playerId) { + return null; + } + + void setVideoTracks(int playerId, List value) {} + + List? getActiveVideoTracks(int playerId) { + return null; + } - void setSaturation(int textureId, double value) {} - void setAudioTracks(int textureId, List value) {} + void setSubtitleTracks(int playerId, List value) {} - void setVideoTracks(int textureId, List value) {} + List? getActiveSubtitleTracks(int playerId) { + return null; + } - void setSubtitleTracks(int textureId, List value) {} + void setExternalAudio(int playerId, String uri) {} - void setExternalAudio(int textureId, String uri) {} + void setExternalVideo(int playerId, String uri) {} - void setExternalVideo(int textureId, String uri) {} + void setExternalSubtitle(int playerId, String uri) {} - void setExternalSubtitle(int textureId, String uri) {} + void onSubtitleText(int playerId, + void Function(double start, double end, List text)? callback) {} } diff --git a/lib/src/video_player_mdk.dart b/lib/src/video_player_mdk.dart index b7258b1..dfcba9d 100644 --- a/lib/src/video_player_mdk.dart +++ b/lib/src/video_player_mdk.dart @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Wang Bin. All rights reserved. +// Copyright 2022-2026 Wang Bin. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -8,40 +8,52 @@ import 'package:flutter/widgets.dart'; // import 'package:flutter/services.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; import 'package:logging/logging.dart'; +import 'package:http/http.dart' as http; +import 'package:path_provider/path_provider.dart'; +import 'package:ffi/ffi.dart'; import 'fvp_platform_interface.dart'; import 'extensions.dart'; +import 'lib.dart'; +import 'media_info.dart'; import '../mdk.dart' as mdk; final _log = Logger('fvp'); +// Default MDK_KEY bundled with this plugin. Users can override it by passing +// {'global': {'MDK_KEY': ''}} to registerWith(). +const _kDefaultMdkKey = + '4DF316BC71206BCEECD01559CC0FEDAF32DF8ECAE656BD9999CB821B67DB5B33' + '2B323F24539BA7172746B8F5A64CA67AF342B16BF4B418F76718B821F77BEA07' + '22F71DBC8EDF9431132FEAA633F0125072DF8DAC90268C62D0E4BA7D6DF9A6C4' + '976AB1146EC55FFD1945CAB4125B20D5C77976CF1BCB77B14C563868EA00EA07'; + class MdkVideoPlayer extends mdk.Player { final streamCtl = StreamController(); bool _initialized = false; @override void dispose() { - onMediaStatus(null); - onEvent(null); - onStateChanged(null); streamCtl.close(); _initialized = false; super.dispose(); } MdkVideoPlayer() : super() { - onMediaStatus((oldValue, newValue) { + onMediaStatus.listen((event) { + final oldValue = event.oldValue; + final newValue = event.newValue; _log.fine( '$hashCode player$nativeHandle onMediaStatus: $oldValue => $newValue'); if (!oldValue.test(mdk.MediaStatus.loaded) && newValue.test(mdk.MediaStatus.loaded)) { // initialized event must be sent only once. keep_open=1 is another solution - //if ((textureId.value ?? -1) >= 0) { - // return true; // prepared callback is invoked before MediaStatus.loaded, so textureId can be a valid value here + //if ((playerId.value ?? -1) >= 0) { + // return true; // prepared callback is invoked before MediaStatus.loaded, so playerId can be a valid value here //} if (_initialized) { _log.fine('$hashCode player$nativeHandle already initialized'); - return true; + return; } _initialized = true; textureSize.then((size) { @@ -64,10 +76,9 @@ class MdkVideoPlayer extends mdk.Player { newValue.test(mdk.MediaStatus.buffered)) { streamCtl.add(VideoEvent(eventType: VideoEventType.bufferingEnd)); } - return true; }); - onEvent((ev) { + onEvent.listen((ev) { _log.fine( '$hashCode player$nativeHandle onEvent: ${ev.category} - ${ev.detail} - ${ev.error}'); if (ev.category == "reader.buffering") { @@ -81,17 +92,17 @@ class MdkVideoPlayer extends mdk.Player { } }); - onStateChanged((oldValue, newValue) { + onStateChanged.listen((event) { _log.fine( - '$hashCode player$nativeHandle onPlaybackStateChanged: $oldValue => $newValue'); - if (newValue == mdk.PlaybackState.stopped) { + '$hashCode player$nativeHandle onPlaybackStateChanged: ${event.oldValue} => ${event.newValue}'); + if (event.newValue == mdk.PlaybackState.stopped) { // FIXME: keep_open no stopped streamCtl.add(VideoEvent(eventType: VideoEventType.completed)); return; } streamCtl.add(VideoEvent( eventType: VideoEventType.isPlayingStateUpdate, - isPlaying: newValue == mdk.PlaybackState.playing)); + isPlaying: event.newValue == mdk.PlaybackState.playing)); }); } } @@ -125,8 +136,11 @@ class MdkVideoPlayerPlatform extends VideoPlayerPlatform { final platforms = options['platforms']; if (platforms is List) { if (!platforms.contains(Platform.operatingSystem)) { - if (_prevImpl != null) { + if (_prevImpl != null && + VideoPlayerPlatform.instance is MdkVideoPlayerPlatform) { // null if it's the 1st time to call registerWith() including current platform + // if current is not MdkVideoPlayerPlatform, another plugin may set instance + // if current is MdkVideoPlayerPlatform, we have to restore instance, _prevImpl is correct and no one changed instance VideoPlayerPlatform.instance = _prevImpl!; } return; @@ -143,22 +157,50 @@ class MdkVideoPlayerPlatform extends VideoPlayerPlatform { _tunnel = options["tunnel"]; _playerOpts = options['player']; _globalOpts = options['global']; + // TODO: _env => putenv _decoders = options['video.decoders']; _subtitleFontFile = options['subtitleFontFile']; } if (_decoders == null && !PlatformEx.isAndroidEmulator()) { // prefer hardware decoders - const vd = { - 'windows': ['MFT:d3d=11', "D3D11", "DXVA", 'CUDA', 'FFmpeg'], - 'macos': ['VT', 'FFmpeg'], - 'ios': ['VT', 'FFmpeg'], - 'linux': ['VAAPI', 'CUDA', 'VDPAU', 'FFmpeg'], - 'android': ['AMediaCodec', 'FFmpeg'], + const vdRk = ['rockchip', 'rkmpp', 'FFmpeg', 'dav1d']; + const vdPi = ['V4L2M2M', 'FFmpeg:hwcontext=drm', 'FFmpeg', 'dav1d']; + final vdLinux = PlatformEx.isRockchip() + ? vdRk + : (PlatformEx.isRaspberryPi() + ? vdPi + : ['VAAPI', 'CUDA', 'VDPAU', 'hap', 'FFmpeg', 'dav1d']); + final vd = { + 'windows': [ + 'MFT:d3d=11', + "D3D11", + "DXVA", + 'CUDA', + 'hap', + 'FFmpeg', + 'dav1d' + ], + 'macos': ['VT', 'hap', 'FFmpeg', 'dav1d'], + 'ios': ['VT', 'FFmpeg', 'dav1d'], + 'linux': vdLinux, + 'android': ['AMediaCodec', 'FFmpeg', 'dav1d'], + 'ohos': ['OH', 'FFmpeg', 'dav1d'], }; _decoders = vd[Platform.operatingSystem]; } +// delay: ensure log handler is set in main(), blank window if run with debugger. +// registerWith() can be invoked by dart_plugin_registrant.dart before main. when debugging, won't enter main if posting message from native to dart(new native log message) before main? + Future.delayed(const Duration(milliseconds: 0), () { + _setupMdk(); + }); + + _prevImpl ??= VideoPlayerPlatform.instance; + VideoPlayerPlatform.instance = MdkVideoPlayerPlatform(); + } + + static void _setupMdk() { mdk.setLogHandler((level, msg) { if (msg.endsWith('\n')) { msg = msg.substring(0, msg.length - 1); @@ -181,23 +223,45 @@ class MdkVideoPlayerPlatform extends VideoPlayerPlatform { // mdk.setGlobalOptions('plugins', 'mdk-braw'); mdk.setGlobalOption("log", "all"); mdk.setGlobalOption('d3d11.sync.cpu', 1); - mdk.setGlobalOption('subtitle.fonts.file', - PlatformEx.assetUri(_subtitleFontFile ?? 'assets/subfont.ttf')); + if (_subtitleFontFile?.startsWith('http') ?? false) { + final fileName = _subtitleFontFile!.split('/').last; + getApplicationCacheDirectory().then((dir) { + final fontPath = '${dir.path}/$fileName'; + _log.fine('check font path: $fontPath'); + if (File(fontPath).existsSync()) { + mdk.setGlobalOption('subtitle.fonts.file', fontPath); + return; + } + _log.fine('downloading font file: $_subtitleFontFile'); + http.get(Uri.parse(_subtitleFontFile!)).then((response) { + if (response.statusCode == 200) { + _log.fine('save font file: $fontPath'); + File(fontPath).writeAsBytes(response.bodyBytes).then((_) { + mdk.setGlobalOption('subtitle.fonts.file', fontPath); + }); + } + }); + }); + } else { + mdk.setGlobalOption('subtitle.fonts.file', + PlatformEx.assetUri(_subtitleFontFile ?? 'assets/subfont.ttf')); + } _globalOpts?.forEach((key, value) { + if (key == 'MDK_KEY') return; // handled separately below mdk.setGlobalOption(key, value); }); - - // if VideoPlayerPlatform.instance.runtimeType.toString() != '_PlaceholderImplementation' ? - _prevImpl ??= VideoPlayerPlatform.instance; - VideoPlayerPlatform.instance = MdkVideoPlayerPlatform(); + final mdkKey = _globalOpts?['MDK_KEY'] as String? ?? _kDefaultMdkKey; + final k = mdkKey.toNativeUtf8(); + Libfvp.setKey(k.cast()); + malloc.free(k); } @override Future init() async {} @override - Future dispose(int textureId) async { - _players.remove(textureId)?.dispose(); + Future dispose(int playerId) async { + _players.remove(playerId)?.dispose(); } @override @@ -209,11 +273,17 @@ class MdkVideoPlayerPlatform extends VideoPlayerPlatform { //player.setProperty("keep_open", "1"); player.setProperty('video.decoder', 'shader_resource=0'); player.setProperty('avformat.strict', 'experimental'); + player.setProperty('avformat.safe', '0'); player.setProperty('avio.reconnect', '1'); player.setProperty('avio.reconnect_delay_max', '7'); - player.setProperty('avio.protocol_whitelist', - 'file,rtmp,http,https,tls,rtp,tcp,udp,crypto,httpproxy,data,concatf,concat,subfile'); player.setProperty('avformat.rtsp_transport', 'tcp'); + player.setProperty('avformat.extension_picky', '0'); + player.setProperty('avformat.allowed_segment_extensions', 'ALL'); + if (dataSource.sourceType != DataSourceType.network) { + // for m3u8 local file etc. + player.setProperty('avio.protocol_whitelist', + 'file,ftp,rtmp,http,https,tls,rtp,tcp,udp,crypto,httpproxy,data,concatf,concat,subfile'); + } _playerOpts?.forEach((key, value) { player.setProperty(key, value); }); @@ -268,47 +338,46 @@ class MdkVideoPlayerPlatform extends VideoPlayerPlatform { //player.dispose(); return -hashCode; } - _log.fine('$hashCode player${player.nativeHandle} textureId=$tex'); + _log.fine('$hashCode player${player.nativeHandle} textureId/playerId=$tex'); _players[tex] = player; return tex; } @override - Future setLooping(int textureId, bool looping) async { - final player = _players[textureId]; - if (player != null) { - player.loop = looping ? -1 : 0; - } + Future setLooping(int playerId, bool looping) async { + final player = _players[playerId]; + if (player == null) return; + player.loop = looping ? -1 : 0; } @override - Future play(int textureId) async { - _players[textureId]?.state = mdk.PlaybackState.playing; + Future play(int playerId) async { + _players[playerId]?.state = mdk.PlaybackState.playing; } @override - Future pause(int textureId) async { - _players[textureId]?.state = mdk.PlaybackState.paused; + Future pause(int playerId) async { + _players[playerId]?.state = mdk.PlaybackState.paused; } @override - Future setVolume(int textureId, double volume) async { - _players[textureId]?.volume = volume; + Future setVolume(int playerId, double volume) async { + _players[playerId]?.volume = volume; } @override - Future setPlaybackSpeed(int textureId, double speed) async { - _players[textureId]?.playbackRate = speed; + Future setPlaybackSpeed(int playerId, double speed) async { + _players[playerId]?.playbackRate = speed; } @override - Future seekTo(int textureId, Duration position) async { - return _seekToWithFlags(textureId, position, mdk.SeekFlag(_seekFlags)); + Future seekTo(int playerId, Duration position) async { + return _seekToWithFlags(playerId, position, mdk.SeekFlag(_seekFlags)); } @override - Future getPosition(int textureId) async { - final player = _players[textureId]; + Future getPosition(int playerId) async { + final player = _players[playerId]; if (player == null) { return Duration.zero; } @@ -326,17 +395,17 @@ class MdkVideoPlayerPlatform extends VideoPlayerPlatform { } @override - Stream videoEventsFor(int textureId) { - final player = _players[textureId]; + Stream videoEventsFor(int playerId) { + final player = _players[playerId]; if (player != null) { return player.streamCtl.stream; } - throw Exception('No Stream for textureId: $textureId.'); + throw Exception('No Stream for textureId/playerId: $playerId.'); } @override - Widget buildView(int textureId) { - return Texture(textureId: textureId); + Widget buildView(int playerId) { + return Texture(textureId: playerId); } @override @@ -345,110 +414,124 @@ class MdkVideoPlayerPlatform extends VideoPlayerPlatform { } // more apis for fvp controller - bool isLive(int textureId) { - return _players[textureId]?.isLive ?? false; + bool isLive(int playerId) { + return _players[playerId]?.isLive ?? false; } - //MediaInfo getMediaInfo() { - // - //} - void setProperty(int textureId, String name, String value) { - _players[textureId]?.setProperty(name, value); + MediaInfo? getMediaInfo(int playerId) { + return _players[playerId]?.mediaInfo; } - void setAudioDecoders(int textureId, List value) { - _players[textureId]?.audioDecoders = value; + void setProperty(int playerId, String name, String value) { + _players[playerId]?.setProperty(name, value); } - void setVideoDecoders(int textureId, List value) { - _players[textureId]?.videoDecoders = value; + void setAudioDecoders(int playerId, List value) { + _players[playerId]?.audioDecoders = value; } - void record(int textureId, {String? to, String? format}) { - _players[textureId]?.record(to: to, format: format); + void setVideoDecoders(int playerId, List value) { + _players[playerId]?.videoDecoders = value; } - Future snapshot(int textureId, {int? width, int? height}) async { - Uint8List? data; - final player = _players[textureId]; - if (player == null) { - return data; - } - return _players[textureId]?.snapshot(width: width, height: height); + void record(int playerId, {String? to, String? format}) { + _players[playerId]?.record(to: to, format: format); } - void setRange(int textureId, {required int from, int to = -1}) { - _players[textureId]?.setRange(from: from, to: to); + Future snapshot(int playerId, {int? width, int? height}) async { + return _players[playerId]?.snapshot(width: width, height: height); } - void setBufferRange(int textureId, + void setRange(int playerId, {required int from, int to = -1}) { + _players[playerId]?.setRange(from: from, to: to); + } + + void setBufferRange(int playerId, {int min = -1, int max = -1, bool drop = false}) { - _players[textureId]?.setBufferRange(min: min, max: max, drop: drop); + _players[playerId]?.setBufferRange(min: min, max: max, drop: drop); } - Future fastSeekTo(int textureId, Duration position) async { + Future fastSeekTo(int playerId, Duration position) async { return _seekToWithFlags( - textureId, position, mdk.SeekFlag(_seekFlags | mdk.SeekFlag.keyFrame)); + playerId, position, mdk.SeekFlag(_seekFlags | mdk.SeekFlag.keyFrame)); } - Future step(int textureId, int frames) async { - final player = _players[textureId]; - if (player == null) { - return; - } + Future step(int playerId, int frames) async { + final player = _players[playerId]; + if (player == null) return; player.seek( position: frames, flags: const mdk.SeekFlag(mdk.SeekFlag.frame | mdk.SeekFlag.fromNow)); } - void setBrightness(int textureId, double value) { - _players[textureId]?.setVideoEffect(mdk.VideoEffect.brightness, [value]); + void setBrightness(int playerId, double value) { + _players[playerId]?.setVideoEffect(mdk.VideoEffect.brightness, [value]); + } + + void setContrast(int playerId, double value) { + _players[playerId]?.setVideoEffect(mdk.VideoEffect.contrast, [value]); } - void setContrast(int textureId, double value) { - _players[textureId]?.setVideoEffect(mdk.VideoEffect.contrast, [value]); + void setHue(int playerId, double value) { + _players[playerId]?.setVideoEffect(mdk.VideoEffect.hue, [value]); } - void setHue(int textureId, double value) { - _players[textureId]?.setVideoEffect(mdk.VideoEffect.hue, [value]); + void setSaturation(int playerId, double value) { + _players[playerId]?.setVideoEffect(mdk.VideoEffect.saturation, [value]); } - void setSaturation(int textureId, double value) { - _players[textureId]?.setVideoEffect(mdk.VideoEffect.saturation, [value]); + void setProgram(int playerId, int programId) { + _players[playerId]?.setActiveTracks(mdk.MediaType.unknown, [programId]); } // embedded tracks, can be main data source from create(), or external media source via setExternalAudio - void setAudioTracks(int textureId, List value) { - _players[textureId]?.activeAudioTracks = value; + void setAudioTracks(int playerId, List value) { + _players[playerId]?.activeAudioTracks = value; + } + + List? getActiveAudioTracks(int playerId) { + return _players[playerId]?.activeAudioTracks; } - void setVideoTracks(int textureId, List value) { - _players[textureId]?.activeVideoTracks = value; + void setVideoTracks(int playerId, List value) { + _players[playerId]?.activeVideoTracks = value; } - void setSubtitleTracks(int textureId, List value) { - _players[textureId]?.activeSubtitleTracks = value; + List? getActiveVideoTracks(int playerId) { + return _players[playerId]?.activeVideoTracks; + } + + void setSubtitleTracks(int playerId, List value) { + _players[playerId]?.activeSubtitleTracks = value; + } + + List? getActiveSubtitleTracks(int playerId) { + return _players[playerId]?.activeSubtitleTracks; } // external track. can select external tracks via setAudioTracks() - void setExternalAudio(int textureId, String uri) { - _players[textureId]?.setMedia(uri, mdk.MediaType.audio); + void setExternalAudio(int playerId, String uri) { + _players[playerId]?.setMedia(uri, mdk.MediaType.audio); } - void setExternalVideo(int textureId, String uri) { - _players[textureId]?.setMedia(uri, mdk.MediaType.video); + void setExternalVideo(int playerId, String uri) { + _players[playerId]?.setMedia(uri, mdk.MediaType.video); } - void setExternalSubtitle(int textureId, String uri) { - _players[textureId]?.setMedia(uri, mdk.MediaType.subtitle); + void setExternalSubtitle(int playerId, String uri) { + _players[playerId]?.setMedia(uri, mdk.MediaType.subtitle); + } + + void onSubtitleText(int playerId, + void Function(double start, double end, List text)? callback) { + _players[playerId]?.onSubtitleText(callback); } Future _seekToWithFlags( - int textureId, Duration position, mdk.SeekFlag flags) async { - final player = _players[textureId]; - if (player == null) { - return; - } + int playerId, Duration position, mdk.SeekFlag flags) async { + final player = _players[playerId]; + if (player == null) return; + if (player.isLive) { final bufMax = player.buffered(); final pos = player.position; diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 8fe9e91..5c9c0ca 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -1,7 +1,7 @@ # The Flutter tooling requires that developers have CMake 3.10 or later # installed. You should not increase this version, as doing so will cause # the plugin to fail to compile for some customers of the plugin. -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.15) # Project-level configuration. set(PROJECT_NAME "fvp") @@ -31,7 +31,8 @@ apply_standard_settings(${PLUGIN_NAME}) # between plugins. This should not be removed; any symbols that should be # exported should be explicitly exported with the FLUTTER_PLUGIN_EXPORT macro. set_target_properties(${PLUGIN_NAME} PROPERTIES - CXX_VISIBILITY_PRESET hidden) + CXX_VISIBILITY_PRESET hidden + OUTPUT_NAME "fvp") target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) # Source include directories and library dependencies. Add any plugin-specific @@ -42,10 +43,13 @@ target_link_libraries(${PLUGIN_NAME} PRIVATE flutter) target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK) # flutter in snap: linker will try to resolve symbols and dependencies in libmdk.so.0, dependencies found in host OS but not snap are compatible with snap glibc target_link_libraries(${PLUGIN_NAME} INTERFACE -Wl,--unresolved-symbols=ignore-in-shared-libs) - +target_link_options(${PLUGIN_NAME} PRIVATE -Wl,--enable-new-dtags -Wl,-z,origin) # add runpath, shared libs of a release bundle is in lib dir, plugin must add $ORIGIN to runpath to find libmdk -set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--enable-new-dtags -Wl,-z,origin -Wl,-rpath,\\$ORIGIN") -set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--enable-new-dtags -Wl,-z,origin -Wl,-rpath,\\$ORIGIN") +set_target_properties(${PLUGIN_NAME} PROPERTIES + BUILD_RPATH_USE_ORIGIN ON + BUILD_RPATH "$ORIGIN" + INSTALL_RPATH "$ORIGIN" +) target_compile_options(${PLUGIN_NAME} PRIVATE -fno-rtti -fno-exceptions -ffunction-sections -fdata-sections) target_compile_options(${PLUGIN_NAME} PRIVATE -Wno-unused-function) # https://github.com/wang-bin/fvp/issues/49 target_link_libraries(${PLUGIN_NAME} PRIVATE -Wl,--gc-sections) @@ -59,6 +63,7 @@ target_link_libraries(${PLUGIN_NAME} PRIVATE mdk) get_filename_component(MDK_LIB_DIR ${MDK_LIBRARY} DIRECTORY) set(fvp_bundled_libraries ${MDK_RUNTIME} + ${MDK_PLUGINS} ${MDK_FFMPEG} ${MDK_LIB_DIR}/libc++.so.1 PARENT_SCOPE diff --git a/linux/fvp_plugin.cc b/linux/fvp_plugin.cc index 08f91fc..632b47b 100644 --- a/linux/fvp_plugin.cc +++ b/linux/fvp_plugin.cc @@ -1,16 +1,23 @@ /* - * Copyright (c) 2023 WangBin + * Copyright (c) 2023-2025 WangBin */ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "include/fvp/fvp_plugin.h" +#include #include #include #include +#include #include +#include #include +#include +#include +#include +#include #include #include @@ -26,6 +33,29 @@ G_DECLARE_FINAL_TYPE(PlayerTexture, player_texture, FL, PLAYER_TEXTURE, FlTextur #define PLAYER_TEXTURE(obj) \ (G_TYPE_CHECK_INSTANCE_CAST((obj), player_texture_get_type(), PlayerTexture)) + +class CleanupTask { +public: + CleanupTask(GdkGLContext* ctx, function&& callback) : ctx_(ctx), cb_(std::move(callback)) {} + ~CleanupTask() { + auto ctx = gdk_gl_context_get_current(); + if (ctx_ != ctx) { + clog << "gdk gl context change: " << ctx_ << " => " << ctx << endl; + gdk_gl_context_make_current(ctx_); + } + cb_(); + if (ctx_ != ctx) { + gdk_gl_context_make_current(ctx); + } + } + + bool disposed = false; +private: + GdkGLContext* ctx_; + function cb_; +}; +static thread_local list> gCleanupTasks; + struct _PlayerTexture { FlTextureGL parent_instance; @@ -34,11 +64,11 @@ struct _PlayerTexture { GLuint fbo; TexturePlayer* player; + CleanupTask* cleanup; }; G_DEFINE_TYPE(PlayerTexture, player_texture, fl_texture_gl_get_type()) - class TexturePlayer final : public mdk::Player { public: @@ -83,24 +113,37 @@ class TexturePlayer final : public mdk::Player PlayerTexture* flTex; // hold ref }; -static unordered_map> players; - // called in a current gl context static gboolean player_texture_populate(FlTextureGL *texture, uint32_t *target, uint32_t *name, uint32_t *width, uint32_t *height, GError **error) { + // cleanup ASAP before drawing the next frame + if (auto count = std::erase_if(gCleanupTasks, [](auto task) { return task->disposed; })) { + clog << std::to_string(count) + " cleanup tasks executed in raster thread " << this_thread::get_id() << endl; + } PlayerTexture *self = PLAYER_TEXTURE(texture); if (self->fbo == 0) { self->ctx = gdk_gl_context_get_current(); // fbo can not be shared glGenFramebuffers(1, &self->fbo); + assert(self->texture_id == 0); + GLint prevFbo = 0; + glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFbo); glBindFramebuffer(GL_FRAMEBUFFER, self->fbo); - } - if (self->texture_id == 0) { glGenTextures(1, &self->texture_id); + clog << "created fbo: " + std::to_string(self->fbo) + " tex: " + std::to_string(self->texture_id) + " in raster thread " << this_thread::get_id() << endl; + auto task = make_shared(self->ctx, [tex = self->texture_id, fbo = self->fbo]() { + clog << "delete fbo: " + std::to_string(fbo) + " tex: " + std::to_string(tex) << endl; + glDeleteTextures(1, &tex); + glDeleteFramebuffers(1, &fbo); + }); + self->cleanup = task.get(); + gCleanupTasks.push_back(std::move(task)); + glBindTexture(GL_TEXTURE_2D, self->texture_id); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, self->player->width, self->player->height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + 0, GL_TEXTURE_2D, self->texture_id, 0); const GLenum err = glCheckFramebufferStatus(GL_FRAMEBUFFER); + glBindFramebuffer(GL_FRAMEBUFFER, prevFbo); if (err != GL_FRAMEBUFFER_COMPLETE) { //glDeleteFramebuffers(1, &fbo); clog << "glFramebufferTexture2D error" << endl; @@ -124,12 +167,17 @@ static gboolean player_texture_populate(FlTextureGL *texture, uint32_t *target, static void player_texture_dispose(GObject* obj) { G_OBJECT_CLASS(player_texture_parent_class)->dispose(obj); auto self = PLAYER_TEXTURE(obj); - auto ctx = gdk_gl_context_get_current(); - gdk_gl_context_make_current(self->ctx); - glDeleteTextures(1, &self->texture_id); - glDeleteFramebuffers(1, &self->fbo); - if (ctx) - gdk_gl_context_make_current(ctx); + if (!self->texture_id && !self->fbo) { + clog << "texture and fbo are not created yet" << endl; + return; + } + if (self->cleanup) { + self->cleanup->disposed = true; + clog << "try to cleanup gl resources in dispose thread " << this_thread::get_id() << endl; + if (auto count = std::erase_if(gCleanupTasks, [](auto task) { return task->disposed; })) { + clog << std::to_string(count) + " cleanup tasks executed in dispose thread " << this_thread::get_id() << endl; + } + } } static void player_texture_class_init(PlayerTextureClass* klass) { @@ -143,6 +191,7 @@ static void player_texture_init(PlayerTexture* self) { self->fbo = 0; self->player = nullptr; self->ctx = nullptr; + self->cleanup = nullptr; } @@ -150,10 +199,12 @@ static void player_texture_init(PlayerTexture* self) { (G_TYPE_CHECK_INSTANCE_CAST((obj), fvp_plugin_get_type(), \ FvpPlugin)) +using PlayerMap = unordered_map>; struct _FvpPlugin { GObject parent_instance; FlTextureRegistrar* tex_registrar; + PlayerMap players; }; G_DEFINE_TYPE(FvpPlugin, fvp_plugin, g_object_get_type()) @@ -162,6 +213,7 @@ G_DEFINE_TYPE(FvpPlugin, fvp_plugin, g_object_get_type()) static void fvp_plugin_handle_method_call( FvpPlugin* self, FlMethodCall* method_call) { + // static PlayerMap players; // here is also fine, will be destroyed earlier than libmdk global objects(Context::current() map) g_autoptr(FlMethodResponse) response = nullptr; const gchar* method = fl_method_call_get_name(method_call); @@ -173,14 +225,14 @@ static void fvp_plugin_handle_method_call( const auto height = (int)fl_value_get_int(fl_value_lookup_string(args, "height")); auto tex = PLAYER_TEXTURE(g_object_new(player_texture_get_type(), nullptr)); auto player = make_shared(handle, tex, width, height, self->tex_registrar); - players[player->textureId] = player; + self->players[player->textureId] = player; g_autoptr(FlValue) result = fl_value_new_int(player->textureId); response = FL_METHOD_RESPONSE(fl_method_success_response_new(result)); } else if (strcmp(method, "ReleaseRT") == 0) { const auto args = fl_method_call_get_args(method_call); const auto texId = fl_value_get_int(fl_value_lookup_string(args, "texture")); - if (auto it = players.find(texId); it != players.cend()) { - players.erase(it); + if (auto it = self->players.find(texId); it != self->players.cend()) { + self->players.erase(it); } g_autoptr(FlValue) result = fl_value_new_null(); response = FL_METHOD_RESPONSE(fl_method_success_response_new(result)); @@ -194,7 +246,9 @@ static void fvp_plugin_handle_method_call( fl_method_call_respond(method_call, response, nullptr); } -static void fvp_plugin_dispose(GObject* object) { +static void fvp_plugin_dispose(GObject* object) { // seems never be invoked + auto self = FVP_PLUGIN(object); + self->players.~PlayerMap(); G_OBJECT_CLASS(fvp_plugin_parent_class)->dispose(object); } @@ -204,6 +258,7 @@ static void fvp_plugin_class_init(FvpPluginClass* klass) { static void fvp_plugin_init(FvpPlugin* self) { self->tex_registrar = nullptr; + new(&self->players) PlayerMap; } static void method_call_cb(FlMethodChannel* channel, FlMethodCall* method_call, @@ -231,8 +286,9 @@ void fvp_plugin_register_with_registrar(FlPluginRegistrar* registrar) { auto gdisp = gdk_display_get_default(); if (GDK_IS_X11_DISPLAY(gdisp)) { mdk::SetGlobalOption("X11Display", GDK_DISPLAY_XDISPLAY(gdisp)); + } else if (GDK_IS_WAYLAND_DISPLAY(gdisp)) { + mdk::SetGlobalOption("wl_display*", gdk_wayland_display_get_wl_display(gdisp)); } - mdk::SetGlobalOption("MDK_KEY", "980B9623276F746C5FBB5EC5120D4A99A0B58B635592EAEE41F6817FDF3B28B96AC4A49866257726C19B246863B5ADAF5D17464E86D72A90634E8AE8418F810967F469DCD8908B93A044A13AEDF2B566E0B5810523E2B59E2D83E616B1B807B66253E1607A79BC86AEDE1AEF46F79AA60F36BE44DDEE47B84E165AF2788F8109"); } __attribute__((constructor, used)) diff --git a/ohos/build-profile.json5 b/ohos/build-profile.json5 new file mode 100644 index 0000000..82df827 --- /dev/null +++ b/ohos/build-profile.json5 @@ -0,0 +1,31 @@ +{ + "apiType": "stageMode", + "buildOption": { + "externalNativeOptions": { + "path": "./src/main/cpp/CMakeLists.txt", + "arguments": "", + "abiFilters": ["arm64-v8a"], + "targets": ["fvp_plugin"] + } + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + "obfuscation": { + "ruleOptions": { + "enable": false, + "files": [ + "./obfuscation-rules.txt" + ] + } + } + } + } + ], + "targets": [ + { + "name": "default" + } + ] +} diff --git a/ohos/hvigorfile.ts b/ohos/hvigorfile.ts new file mode 100644 index 0000000..8132322 --- /dev/null +++ b/ohos/hvigorfile.ts @@ -0,0 +1,6 @@ +import { harTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: harTasks, + plugins: [] +} diff --git a/ohos/index.ets b/ohos/index.ets new file mode 100644 index 0000000..0168ee3 --- /dev/null +++ b/ohos/index.ets @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2025 WangBin + */ +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import FvpPlugin from './src/main/ets/components/plugin/FvpPlugin'; +export default FvpPlugin; diff --git a/ohos/oh-package.json5 b/ohos/oh-package.json5 new file mode 100644 index 0000000..24a2946 --- /dev/null +++ b/ohos/oh-package.json5 @@ -0,0 +1,11 @@ +{ + "name": "fvp", + "version": "1.0.0", + "description": "fvp flutter plugin for OpenHarmony", + "main": "index.ets", + "author": "", + "license": "BSD-3-Clause", + "dependencies": { + "@ohos/flutter_ohos": "file:./har/flutter.har" + } +} diff --git a/ohos/src/main/cpp/CMakeLists.txt b/ohos/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000..9a56233 --- /dev/null +++ b/ohos/src/main/cpp/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.17) +set(PROJECT_NAME "fvp") +project(${PROJECT_NAME} LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# This value is used when generating builds using this plugin, so it must +# not be changed. +set(PLUGIN_NAME "fvp_plugin") + +add_library(${PLUGIN_NAME} SHARED + "fvp_plugin.cpp" + ../../../../lib/src/callbacks.cpp +) + +set_target_properties(${PLUGIN_NAME} PROPERTIES + CXX_VISIBILITY_PRESET hidden + OUTPUT_NAME "fvp") +target_include_directories(${PLUGIN_NAME} INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include") + +target_compile_options(${PLUGIN_NAME} PRIVATE -fno-rtti -fno-exceptions -ffunction-sections -fdata-sections) +target_link_options(${PLUGIN_NAME} PRIVATE -Wl,--gc-sections) + +target_link_libraries(${PLUGIN_NAME} PRIVATE + libace_napi.z.so + libace_ndk.z.so + libnative_window.so + librawfile.z.so +) + +include(../../../../cmake/deps.cmake) +fvp_setup_deps() +target_link_libraries(${PLUGIN_NAME} PRIVATE mdk) diff --git a/ohos/src/main/cpp/fvp_plugin.cpp b/ohos/src/main/cpp/fvp_plugin.cpp new file mode 100644 index 0000000..dd917ff --- /dev/null +++ b/ohos/src/main/cpp/fvp_plugin.cpp @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2026 WangBin + */ +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#include "include/fvp/fvp_plugin.h" +#include +#include +#include +#include +#include +#include + +using namespace std; + +class TexturePlayer final : public mdk::Player { +public: + explicit TexturePlayer(int64_t handle) + : mdk::Player(reinterpret_cast(handle)) + {} + + int width = 0; + int height = 0; + OHNativeWindow* window = nullptr; +}; + +static unordered_map> players; + +// nativeSetSurface(playerHandle: number, texId: number, surfaceId: number, w: number, h: number): void +static napi_value NativeSetSurface(napi_env env, napi_callback_info info) +{ + size_t argc = 5; + napi_value args[5]; + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + + int64_t playerHandle = 0; + napi_get_value_int64(env, args[0], &playerHandle); + + int64_t texId = 0; + napi_get_value_int64(env, args[1], &texId); + + int64_t surfaceId = 0; + napi_get_value_int64(env, args[2], &surfaceId); + + int32_t w = 0, h = 0; + napi_get_value_int32(env, args[3], &w); + napi_get_value_int32(env, args[4], &h); + + if (!playerHandle || !surfaceId) { + if (auto it = players.find(texId); it != players.end()) { + auto& player = it->second; + player->updateNativeSurface(nullptr); + if (player->window) { + OH_NativeWindow_DestroyNativeWindow(player->window); + player->window = nullptr; + } + players.erase(it); + } else { + clog << "FvpPlugin: player not found (already removed?) for texId " << texId << endl; + } + return nullptr; + } + + auto player = make_shared(playerHandle); + player->width = w; + player->height = h; + + OHNativeWindow* window = nullptr; + int32_t ret = OH_NativeWindow_CreateNativeWindowFromSurfaceId(static_cast(surfaceId), &window); + if (ret != 0 || !window) { + clog << "FvpPlugin: OH_NativeWindow_CreateNativeWindowFromSurfaceId failed: " << ret << endl; + return nullptr; + } + + player->window = window; + player->updateNativeSurface(window, w, h); + players[texId] = player; + + return nullptr; +} + +// nativeSetResourceManager(resourceManager: object): void +// Converts an OHOS ResourceManager object to native pointer and passes it to MDK. +static napi_value NativeSetResourceManager(napi_env env, napi_callback_info info) +{ + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + + NativeResourceManager* nativeRm = OH_ResourceManager_InitNativeResourceManager(env, args[0]); + if (nativeRm) { + mdk::SetGlobalOption("resourceManager", nativeRm); + } + return nullptr; +} + +static napi_value Init(napi_env env, napi_value exports) +{ + napi_property_descriptor desc[] = { + { "nativeSetSurface", nullptr, NativeSetSurface, nullptr, nullptr, nullptr, napi_default, nullptr }, + { "nativeSetResourceManager", nullptr, NativeSetResourceManager, nullptr, nullptr, nullptr, napi_default, nullptr }, + }; + napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); + + return exports; +} + +NAPI_MODULE(fvp, Init) + +extern "C" bool MdkIsEmulator() +{ + return false; +} + +extern "C" void* MdkGetPlayerVid(int64_t tex_id) +{ + if (tex_id < 0) + return nullptr; + if (const auto it = players.find(tex_id); it != players.end()) { + return it->second->window; + } + return nullptr; +} diff --git a/ohos/src/main/cpp/include/fvp/fvp_plugin.h b/ohos/src/main/cpp/include/fvp/fvp_plugin.h new file mode 100644 index 0000000..1e3796b --- /dev/null +++ b/ohos/src/main/cpp/include/fvp/fvp_plugin.h @@ -0,0 +1,24 @@ +#ifndef FLUTTER_PLUGIN_FVP_PLUGIN_H_ +#define FLUTTER_PLUGIN_FVP_PLUGIN_H_ + +#include +#include + +#if defined(_WIN32) +#define FVP_EXPORT __declspec(dllexport) +#else +#define FVP_EXPORT __attribute__((visibility("default"))) +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FVP_EXPORT bool MdkIsEmulator(); +FVP_EXPORT void* MdkGetPlayerVid(int64_t tex_id); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_FVP_PLUGIN_H_ diff --git a/ohos/src/main/ets/components/plugin/FvpPlugin.ets b/ohos/src/main/ets/components/plugin/FvpPlugin.ets new file mode 100644 index 0000000..3ece817 --- /dev/null +++ b/ohos/src/main/ets/components/plugin/FvpPlugin.ets @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2026 WangBin + */ +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import { + FlutterPlugin, + FlutterPluginBinding, + MethodCall, + MethodCallHandler, + MethodChannel, + MethodResult, + SurfaceTextureEntry, +} from '@ohos/flutter_ohos'; +import fvpNative from 'libfvp.so'; + +const TAG = 'FvpPlugin'; + +/** FvpPlugin */ +export default class FvpPlugin implements FlutterPlugin, MethodCallHandler { + private channel: MethodChannel | null = null; + private binding: FlutterPluginBinding | null = null; + private textures: Map = new Map(); + + getUniqueClassName(): string { + return 'FvpPlugin'; + } + + onAttachedToEngine(binding: FlutterPluginBinding): void { + this.binding = binding; + this.channel = new MethodChannel(binding.getBinaryMessenger(), 'fvp'); + this.channel.setMethodCallHandler(this); + fvpNative.nativeSetResourceManager(binding.getApplicationContext().resourceManager()); + } + + onDetachedFromEngine(binding: FlutterPluginBinding): void { + this.textures.forEach((_entry, texId) => { + fvpNative.nativeSetSurface(0, texId, 0, 0, 0); + }); + this.textures.clear(); + this.channel?.setMethodCallHandler(null); + this.channel = null; + this.binding = null; + } + + onMethodCall(call: MethodCall, result: MethodResult): void { + if (call.method === 'CreateRT') { + const args = call.args as Map; + const handle: number = args.get('player') as number; + const width: number = args.get('width') as number; + const height: number = args.get('height') as number; + + const textureRegistry = this.binding?.getTextureRegistry(); + if (!textureRegistry) { + result.error('NO_TEXTURE_REGISTRY', 'TextureRegistry not available', null); + return; + } + + const texId = textureRegistry.getTextureId(); + const entry = textureRegistry.registerTexture(texId); + const surfaceId = entry.getSurfaceId(); + textureRegistry.setTextureBufferSize(texId, width, height); + + fvpNative.nativeSetSurface(handle, texId, surfaceId, width, height); + + this.textures.set(texId, entry); + result.success(texId); + } else if (call.method === 'ReleaseRT') { + const args = call.args as Map; + const texId: number = args.get('texture') as number; + + fvpNative.nativeSetSurface(0, texId, 0, 0, 0); + this.binding?.getTextureRegistry()?.unregisterTexture(texId); + this.textures.delete(texId); + result.success(null); + } else if (call.method === 'MixWithOthers') { + result.success(null); + } else { + result.notImplemented(); + } + } +} diff --git a/ohos/src/main/module.json5 b/ohos/src/main/module.json5 new file mode 100644 index 0000000..b4f00c7 --- /dev/null +++ b/ohos/src/main/module.json5 @@ -0,0 +1,10 @@ +{ + "module": { + "name": "fvp", + "type": "har", + "deviceTypes": [ + "default", + "tablet" + ] + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 69d52ac..1fa4f48 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,33 +1,35 @@ name: fvp description: video_player plugin and backend APIs. Support all desktop/mobile platforms with hardware decoders, optimal renders. Supports most formats via FFmpeg -version: 0.28.0 +version: 0.37.3 homepage: https://github.com/wang-bin/fvp topics: - video - player - video-player - - audio-player - - videoplayer + - ohos + - elinux environment: - sdk: '>=3.0.0 <4.0.0' + sdk: ^3.0.0 flutter: ">=3.0.0" -dependencies: - ffi: ^2.1.0 +dependencies: # TODO: limit max version? + ffi: any flutter: sdk: flutter - logging: ^1.2.0 - path: ^1.8.0 - plugin_platform_interface: ^2.0.0 - video_player: ^2.6.0 - video_player_platform_interface: ^6.2.0 + logging: any + path: any + plugin_platform_interface: any + video_player: any + video_player_platform_interface: any + path_provider: any + http: any dev_dependencies: flutter_test: sdk: flutter flutter_lints: any - ffigen: '>=9.0.0' + ffigen: '>=9.0.0 <13.0.0' lints: any # For information on the generic Dart part of this file, see the @@ -46,7 +48,7 @@ flutter: # All these are used by the tooling to maintain consistency when # adding or updating assets for this project. plugin: - implements: video_player + #implements: video_player # endorsed. flutter 3.27+ can only select 1 implementation platforms: android: package: com.mediadevkit.fvp @@ -55,7 +57,7 @@ flutter: pluginClass: FvpPlugin sharedDarwinSource: true linux: - dartPluginClass: VideoPlayerRegistrant + dartPluginClass: VideoPlayerRegistrant # auto registered in .dart_tool/flutter_build/dart_plugin_registrant.dart pluginClass: FvpPlugin macos: pluginClass: FvpPlugin @@ -63,6 +65,12 @@ flutter: windows: dartPluginClass: VideoPlayerRegistrant pluginClass: FvpPluginCApi + elinux: + dartPluginClass: VideoPlayerRegistrant # auto registered in .dart_tool/flutter_build/dart_plugin_registrant.dart + pluginClass: FvpPlugin + ohos: + dartPluginClass: VideoPlayerRegistrant + pluginClass: FvpPlugin # To add assets to your plugin package, add an assets section, like this: # assets: @@ -70,10 +78,10 @@ flutter: # - images/a_dot_ham.jpeg # # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages + # https://flutter.dev/to/asset-from-package # # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware + # https://flutter.dev/to/resolution-aware-images # To add custom fonts to your plugin package, add a fonts section here, # in this "flutter" section. Each entry in this list should have a @@ -93,4 +101,4 @@ flutter: # weight: 700 # # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages + # https://flutter.dev/to/font-from-package diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 7e57a8d..4bde588 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -2,11 +2,11 @@ # installed that includes CMake 3.14 or later. You should not increase this # version, as doing so will cause the plugin to fail to compile for some # customers of the plugin. -cmake_minimum_required(VERSION 3.14) +cmake_minimum_required(VERSION 3.17) # Project-level configuration. set(PROJECT_NAME "fvp") -project(${PROJECT_NAME} VERSION 0.28.0 +project(${PROJECT_NAME} VERSION 0.37.3 DESCRIPTION "Flutter video_player plugin via libmdk") set(PROJECT_VERSION_TWEAK 0) set(CMAKE_CXX_STANDARD 20) @@ -46,7 +46,8 @@ apply_standard_settings(${PLUGIN_NAME}) # between plugins. This should not be removed; any symbols that should be # exported should be explicitly exported with the FLUTTER_PLUGIN_EXPORT macro. set_target_properties(${PLUGIN_NAME} PROPERTIES - CXX_VISIBILITY_PRESET hidden) + CXX_VISIBILITY_PRESET hidden + OUTPUT_NAME "fvp") target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) #target_compile_definitions(${PLUGIN_NAME} PUBLIC DART_SHARED_LIB) # all platforms? @@ -70,6 +71,7 @@ get_filename_component(MDK_LIB_DIR ${MDK_LIBRARY} DIRECTORY) string(REPLACE "/lib/" "/bin/" MDK_BIN_DIR ${MDK_LIB_DIR}) set(fvp_bundled_libraries ${MDK_RUNTIME} + ${MDK_PLUGINS} ${MDK_FFMPEG} ${MDK_LIBASS} PARENT_SCOPE diff --git a/windows/fvp_plugin.cpp b/windows/fvp_plugin.cpp index 7add44a..1a9af74 100644 --- a/windows/fvp_plugin.cpp +++ b/windows/fvp_plugin.cpp @@ -124,7 +124,6 @@ void FvpPlugin::RegisterWithRegistrar( }); registrar->AddPlugin(std::move(plugin)); - SetGlobalOption("MDK_KEY", "980B9623276F746C5FBB5EC5120D4A99A0B58B635592EAEE41F6817FDF3B28B96AC4A49866257726C19B246863B5ADAF5D17464E86D72A90634E8AE8418F810967F469DCD8908B93A044A13AEDF2B566E0B5810523E2B59E2D83E616B1B807B66253E1607A79BC86AEDE1AEF46F79AA60F36BE44DDEE47B84E165AF2788F8109"); } FvpPlugin::FvpPlugin(flutter::TextureRegistrar* tr, IDXGIAdapter* adapter)