diff --git a/media_kit/doc/native_assets.md b/media_kit/doc/native_assets.md index 2b055e8f5..745d1b11f 100644 --- a/media_kit/doc/native_assets.md +++ b/media_kit/doc/native_assets.md @@ -6,8 +6,9 @@ dependency and override. There are no replacement container packages. This branch requires Dart 3.10 or newer and Flutter 3.38 or newer. Both packages must come from the same checkout or migrated revision; previously published -versions do not implement this protocol. Kazumi uses adjacent local checkouts -until these changes are published. +versions do not implement this protocol. Git consumers should pin both package +paths (`media_kit` and `media_kit_video`) to the same immutable commit. Kazumi +uses the OHOS adaptation in the `tignioj/media-kit` fork this way. ## Ownership @@ -92,11 +93,35 @@ it. Apple requires Xcode (including `lipo`, `otool`, `install_name_tool`, `codes Windows 7z extraction uses the system `tar.exe`; other archives use ZIP or tar. All native targets require a C compiler for the small lifecycle bridge. -An OHOS artifact is retained in the catalog, but **OHOS builds are not supported -by the upstream `code_assets`/C toolchain used here**. An OHOS Flutter/Dart port -must implement the hook target, compiler configuration and runtime asset mapping -before this branch can ship on OHOS. Keeping the former binary-only package would -not implement those requirements. The ArkTS video renderer remains in the tree. +OHOS arm64 is supported with a Flutter-OH SDK that implements Native Assets +(tested with 3.41.9+ohos-1.0.2, Dart 3.11.5). Flutter-OH currently reports a Linux +target to hosted hooks, so the hook identifies its OHOS compiler SDK, selects +`ohos_arm64`, and compiles the event-loop bridge with the OHOS target triple and +sysroot. It does not use Linux binaries. The catalog's primary filename matches +the archive and SONAME: `libmpv.so`. + +The OHOS ArkTS renderer still supplies the Flutter texture and native surface. +The hook also bundles the SDK's shared C++ runtime required by libmpv (unless a +local bundle supplies it). Use a native SDK compatible with the consuming +application's minimum HarmonyOS API level. +Only arm64 has a pinned OHOS bundle; other OHOS architectures fail explicitly. +Emulators are not supported by the renderer; validate on a physical device. + +The compiler supplied by Flutter-OH normally locates the SDK automatically. +If it is unavailable, configure the consuming application's OHOS builds only: + +```yaml +hooks: + user_defines: + media_kit: + target: ohos + ohos-sdk: /absolute/path/to/openharmony/native +``` + +`OHOS_NDK_HOME` and `HOS_SDK_HOME` are also accepted for explicit OHOS targets. +Setting these environment variables alone does not redirect normal Linux builds. +The dependency ranges support Flutter-OH's pinned `meta 1.17` as well as newer +hook/code-assets versions, without overriding Flutter SDK dependencies. Apple deployment minimums are also constrained by the selected Flutter SDK's native-asset framework generator. Flutter 3.47.5 emits iOS 15 / macOS 13 minimums; @@ -134,3 +159,28 @@ unchanged. Linux and Apple release archives were downloaded and SHA-256 verified Apple framework metadata and architecture selections were inspected. Their application builds and runtime behavior still require Linux/macOS hosts. No APK was installed, overwritten or uninstalled on the connected Android device. + + +OHOS validation (2026-10-17): the hook/core suite passed 19 tests on macOS; +Kazumi passed 213 Flutter tests and built both smoke and normal Debug HAPs with +Flutter-OH 3.41.9+ohos-1.0.2. The HAP contains the OHOS libmpv, C event-loop +bridge, shared C++ runtime and their `ohos_arm64` Native Assets mappings. +On a MatePad mini (HarmonyOS API 26), two runs each completed 12 render/dispose +cycles across software/hardware decoding and synchronous/asynchronous player +modes, plus 3 disposals during initialization. Hardware decoding reported +`ohcodec`. Concurrent/repeated disposal completed, the native handle became +null, and disposal took 69–136 ms across both runs. A device screenshot confirmed +the test video was displayed through the Flutter texture. The normal-entry +Kazumi HAP was then installed and launched successfully. OHOS emulators and other OHOS architectures were not validated. + + +Release AOT follow-up (2026-10-17): after installing Rosetta for Flutter-OH's +x86_64 macOS snapshot compiler, both Kazumi smoke and normal-entry Release HAPs +built and passed CLI signing/verification. Both contain arm64 `libapp.so`, have +`debug=false`, and omit `kernel_blob.bin`. On the same MatePad mini, two Release +runs each passed 12 render/dispose cycles and 3 disposals during initialization +(24 and 6 total). Software (`no`) and OHOS hardware (`ohcodec`) decoding worked +in synchronous and asynchronous modes; concurrent/repeated disposal completed +with a null native handle in 76–160 ms. A device screenshot confirmed actual +hardware-decoded texture rendering. The normal-entry Release HAP was then +installed in place of the smoke build. diff --git a/media_kit/hook/build.dart b/media_kit/hook/build.dart index 0545b69dd..37e492f4f 100644 --- a/media_kit/hook/build.dart +++ b/media_kit/hook/build.dart @@ -7,18 +7,26 @@ import 'package:path/path.dart' as p; import 'src/apple.dart'; import 'src/bundles.dart'; +import 'src/ohos.dart'; void main(List arguments) async { await build(arguments, (input, output) async { if (!input.config.buildCodeAssets) return; final os = input.config.code.targetOS; final architecture = input.config.code.targetArchitecture; + final ohos = OhosBuildConfig.detect( + targetOS: os, + architecture: architecture, + compiler: input.config.code.cCompiler?.compiler, + sdkPath: input.userDefines.path('ohos-sdk')?.toFilePath(), + explicitlyOhos: input.userDefines['target'] == 'ohos', + ); final catalogFile = File.fromUri( input.packageRoot.resolve('hook/native_bundles.json'), ); final catalog = await Bundle.read(catalogFile); output.dependencies.add(catalogFile.uri); - final target = '${os.name}_${architecture.name}'; + final target = '${ohos == null ? os.name : 'ohos'}_${architecture.name}'; final bundle = catalog[target]; if (bundle == null) { throw UnsupportedError('No media-kit native bundle for $target'); @@ -26,7 +34,7 @@ void main(List arguments) async { final source = input.userDefines['source'] ?? 'bundled'; if (source == 'system') { - if (os != OS.linux && os != OS.windows) { + if (ohos != null || (os != OS.linux && os != OS.windows)) { throw UnsupportedError( 'System libmpv is supported on Linux and Windows only', ); @@ -77,6 +85,12 @@ void main(List arguments) async { input.config.code.iOS.targetSdk == IOSSdk.iPhoneSimulator, ) : await selectLibraries(directory, bundle.library); + if (ohos != null && + !libraries.any( + (file) => p.basename(file.path) == 'libc++_shared.so', + )) { + libraries.add(ohos.sharedCxxRuntime); + } if (os == OS.android && !libraries.any( (file) => p.basename(file.path) == 'libmediakitandroidhelper.so', @@ -98,6 +112,10 @@ void main(List arguments) async { output.dependencies.add(library.uri); } } + if (ohos != null) { + await ohos.buildBridge(input, output); + return; + } await CBuilder.library( name: 'media_kit_event_loop', assetName: 'src/player/native/core/native_event_loop_bindings.dart', diff --git a/media_kit/hook/native_bundles.json b/media_kit/hook/native_bundles.json index 41ae06489..35b09f999 100644 --- a/media_kit/hook/native_bundles.json +++ b/media_kit/hook/native_bundles.json @@ -85,7 +85,7 @@ "url": "https://github.com/ErBWs/libmpv-ohos-build/releases/download/20260811/libmpv_aarch64.zip", "sha256": "2bfb9844a7552c450694581b315b26cdf6202eaa57724e9f70f3facfad75163b", "format": "zip", - "library": "libmpv.so.2" + "library": "libmpv.so" } } } diff --git a/media_kit/hook/src/ohos.dart b/media_kit/hook/src/ohos.dart new file mode 100644 index 000000000..f39b6ff86 --- /dev/null +++ b/media_kit/hook/src/ohos.dart @@ -0,0 +1,130 @@ +import 'dart:io'; + +import 'package:code_assets/code_assets.dart'; +import 'package:hooks/hooks.dart'; +import 'package:path/path.dart' as p; + +import 'process.dart'; + +/// Flutter-OH currently reports Linux with an OHOS compiler to hosted hooks. +/// Identify the target SDK instead of assuming that Linux binaries will work. +final class OhosBuildConfig { + OhosBuildConfig._(this.nativeSdk); + + final String nativeSdk; + + File get sharedCxxRuntime { + final file = File( + p.join( + nativeSdk, + 'llvm', + 'lib', + 'aarch64-linux-ohos', + 'libc++_shared.so', + ), + ); + if (!file.existsSync()) { + throw StateError('OHOS libmpv requires the SDK runtime: ${file.path}'); + } + return file; + } + + static OhosBuildConfig? detect({ + required OS targetOS, + required Architecture architecture, + Uri? compiler, + String? sdkPath, + bool explicitlyOhos = false, + Map? environment, + }) { + if (explicitlyOhos && targetOS != OS.linux && targetOS.name != 'ohos') { + throw UnsupportedError('An explicit OHOS target requires Flutter-OH'); + } + final compilerSdk = compiler == null + ? null + : _findNativeSdk( + p.dirname(p.dirname(p.dirname(compiler.toFilePath()))), + ); + if (!explicitlyOhos && + targetOS.name != 'ohos' && + !(targetOS == OS.linux && compilerSdk != null)) { + return null; + } + if (architecture != Architecture.arm64) { + throw UnsupportedError('media-kit currently bundles OHOS arm64 only'); + } + final env = environment ?? Platform.environment; + for (final candidate in [ + ?sdkPath, + ?compilerSdk, + ?env['OHOS_NDK_HOME'], + ?env['HOS_SDK_HOME'], + ]) { + final sdk = _findNativeSdk(candidate); + if (sdk != null) return OhosBuildConfig._(sdk); + } + throw StateError( + 'OpenHarmony native SDK not found. Set OHOS_NDK_HOME or ' + 'hooks.user_defines.media_kit.ohos-sdk to the native SDK directory.', + ); + } + + static String? _findNativeSdk(String root) { + for (final candidate in [ + root, + p.join(root, 'native'), + p.join(root, 'openharmony', 'native'), + p.join(root, 'default', 'openharmony', 'native'), + ]) { + if (File( + p.join(candidate, 'build', 'cmake', 'ohos.toolchain.cmake'), + ).existsSync()) { + return p.normalize(p.absolute(candidate)); + } + } + return null; + } + + /// CBuilder's Linux target supplies a GNU triple; compile this bridge with + /// the OHOS triple and sysroot while keeping Flutter-OH's asset protocol. + Future buildBridge(BuildInput input, BuildOutputBuilder output) async { + final suffix = Platform.isWindows ? '.exe' : ''; + final include = File( + Platform.resolvedExecutable, + ).parent.parent.uri.resolve('include/'); + final sources = [ + 'src/native_event_loop.c', + 'src/native_library.c', + ].map((source) => input.packageRoot.resolve(source)).toList(); + final library = input.outputDirectory.resolve('libmedia_kit_event_loop.so'); + await Directory.fromUri(input.outputDirectory).create(recursive: true); + await runNativeTool(p.join(nativeSdk, 'llvm', 'bin', 'clang$suffix'), [ + '--target=aarch64-linux-ohos', + '--sysroot=${p.join(nativeSdk, 'sysroot')}', + '-shared', + '-fPIC', + '-O2', + '-Wl,-z,defs', + '-Wl,-soname,libmedia_kit_event_loop.so', + '-I${include.toFilePath()}', + ...sources.map((source) => source.toFilePath()), + '-o', + library.toFilePath(), + '-ldl', + ]); + output.assets.code.add( + CodeAsset( + package: input.packageName, + name: 'src/player/native/core/native_event_loop_bindings.dart', + linkMode: DynamicLoadingBundled(), + file: library, + ), + ); + output.dependencies.addAll([ + ...sources, + include.resolve('dart_native_api.h'), + include.resolve('dart_api.h'), + File(p.join(nativeSdk, 'oh-uni-package.json')).uri, + ]); + } +} diff --git a/media_kit/pubspec.yaml b/media_kit/pubspec.yaml index 825424086..1c222ed7d 100644 --- a/media_kit/pubspec.yaml +++ b/media_kit/pubspec.yaml @@ -35,9 +35,10 @@ dependencies: archive: ^4.0.0 crypto: ^3.0.3 xml: ^6.5.0 - code_assets: ^2.1.0 - hooks: ^2.2.0 - native_toolchain_c: ^0.19.5 + # Flutter-OH 3.41 pins meta 1.17. These hooks also support that toolchain. + code_assets: '>=1.2.1 <3.0.0' + hooks: '>=2.0.2 <3.0.0' + native_toolchain_c: '>=0.19.1 <0.20.0' collection: ^1.17.0 http: ">=0.13.0 <2.0.0" image: ^4.0.17 diff --git a/media_kit/test/hook/native_bundles_test.dart b/media_kit/test/hook/native_bundles_test.dart index b57e42a61..cf3e6f89e 100644 --- a/media_kit/test/hook/native_bundles_test.dart +++ b/media_kit/test/hook/native_bundles_test.dart @@ -28,12 +28,14 @@ void main() { 'linux_x64', 'ios_arm64', 'macos_arm64', + 'ohos_arm64', ]), ); for (final bundle in catalog.values) { expect(bundle.url.scheme, 'https'); expect(bundle.checksum, matches(r'^[a-f0-9]{64}$')); } + expect(catalog['ohos_arm64']!.library, 'libmpv.so'); }); test('rejects traversal, absolute and drive archive paths', () { diff --git a/media_kit/test/hook/ohos_test.dart b/media_kit/test/hook/ohos_test.dart new file mode 100644 index 000000000..41562e9ff --- /dev/null +++ b/media_kit/test/hook/ohos_test.dart @@ -0,0 +1,105 @@ +@TestOn('vm') +library; + +import 'dart:io'; + +import 'package:code_assets/code_assets.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +import '../../hook/src/ohos.dart'; + +void main() { + late Directory root; + late Directory sdk; + + setUp(() async { + root = await Directory.systemTemp.createTemp('media-kit-ohos-'); + sdk = Directory(p.join(root.path, 'default', 'openharmony', 'native')); + final toolchain = File( + p.join(sdk.path, 'build/cmake/ohos.toolchain.cmake'), + ); + await toolchain.parent.create(recursive: true); + await toolchain.writeAsString(''); + }); + tearDown(() => root.delete(recursive: true)); + + test('Flutter-OH Linux compatibility input selects the OHOS SDK', () { + final config = OhosBuildConfig.detect( + targetOS: OS.linux, + architecture: Architecture.arm64, + compiler: sdk.uri.resolve('llvm/bin/clang'), + environment: {}, + ); + expect(config?.nativeSdk, sdk.path); + }); + + test('an OHOS environment does not turn a normal Linux build into OHOS', () { + expect( + OhosBuildConfig.detect( + targetOS: OS.linux, + architecture: Architecture.arm64, + compiler: Uri.file('/usr/bin/clang'), + environment: {'HOS_SDK_HOME': root.path}, + ), + isNull, + ); + }); + + test('explicit OHOS target accepts the command-line SDK layout', () { + expect( + OhosBuildConfig.detect( + targetOS: OS.linux, + architecture: Architecture.arm64, + explicitlyOhos: true, + environment: {'HOS_SDK_HOME': root.path}, + )?.nativeSdk, + sdk.path, + ); + }); + + test('a future OHOS target name is recognized without the Linux shim', () { + expect( + OhosBuildConfig.detect( + targetOS: OS.fromString('ohos'), + architecture: Architecture.arm64, + sdkPath: sdk.path, + environment: {}, + )?.nativeSdk, + sdk.path, + ); + }); + + test('explicit OHOS fails on other host targets and missing SDKs', () { + expect( + () => OhosBuildConfig.detect( + targetOS: OS.macOS, + architecture: Architecture.arm64, + explicitlyOhos: true, + sdkPath: sdk.path, + ), + throwsUnsupportedError, + ); + expect( + () => OhosBuildConfig.detect( + targetOS: OS.linux, + architecture: Architecture.arm64, + explicitlyOhos: true, + environment: {}, + ), + throwsStateError, + ); + }); + + test('OHOS architectures without a bundled mpv fail explicitly', () { + expect( + () => OhosBuildConfig.detect( + targetOS: OS.linux, + architecture: Architecture.x64, + compiler: sdk.uri.resolve('llvm/bin/clang'), + environment: {}, + ), + throwsUnsupportedError, + ); + }); +}