Commit 0f31a59a authored by zhangying's avatar zhangying

IR001-SR002

parents
*.iml
.kotlin
.gradle
**/build/
xcuserdata
!src/**/build/
local.properties
.idea
.DS_Store
captures
.externalNativeBuild
.cxx
test_execution.log
*.xcodeproj/*
!*.xcodeproj/project.pbxproj
!*.xcodeproj/xcshareddata/
!*.xcodeproj/project.xcworkspace/
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings
node_modules/
test-reports/
includeTemp/
\ No newline at end of file
Initialized native services in: /Users/liluo/.gradle/native
Initialized jansi services in: /Users/liluo/.gradle/native
Found daemon DaemonInfo{pid=75162, address=[e11487d2-e870-4c5f-ae14-3a7baa5589cd port:59816, addresses:[/127.0.0.1]], state=Idle, lastBusy=1766641409813, context=DefaultDaemonContext[uid=bcf53fd5-f12c-4c95-b6d9-e8c751f4393f,javaHome=/Applications/Android Studio.app/Contents/jbr/Contents/Home,javaVersion=21,javaVendor=JetBrains s.r.o.,daemonRegistryDir=/Users/liluo/.gradle/daemon,pid=75162,idleTimeout=10800000,priority=NORMAL,applyInstrumentationAgent=true,nativeServicesMode=ENABLED,daemonOpts=-Xmx4096M,-Dfile.encoding=UTF-8,-Duser.country=CN,-Duser.language=zh,-Duser.variant]} however its context does not match the desired criteria.
JVM is incompatible.
Wanted: DaemonRequestContext{jvmCriteria=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home (no JDK specified, using current Java home), daemonOpts=[-Xmx4096M, -Dfile.encoding=UTF-8, -Duser.country=CN, -Duser.language=zh, -Duser.variant], applyInstrumentationAgent=true, nativeServicesMode=ENABLED, priority=NORMAL}
Actual: DefaultDaemonContext[uid=bcf53fd5-f12c-4c95-b6d9-e8c751f4393f,javaHome=/Applications/Android Studio.app/Contents/jbr/Contents/Home,javaVersion=21,javaVendor=JetBrains s.r.o.,daemonRegistryDir=/Users/liluo/.gradle/daemon,pid=75162,idleTimeout=10800000,priority=NORMAL,applyInstrumentationAgent=true,nativeServicesMode=ENABLED,daemonOpts=-Xmx4096M,-Dfile.encoding=UTF-8,-Duser.country=CN,-Duser.language=zh,-Duser.variant]
Looking for a different daemon...
The client will now receive all logging from the daemon (pid: 52727). The daemon log file: /Users/liluo/.gradle/daemon/8.14.3/daemon-52727.out.log
Starting 57th build in daemon [uptime: 5 hrs 37 mins 29.876 secs, performance: 100%, GC rate: 0.00/s, heap usage: 4% of 4 GiB]
Using 14 worker leases.
Now considering [/Users/liluo/mine/projects/kmp/kmp-capi] as hierarchies to watch
Reusing configuration cache.
Now considering [/Users/liluo/mine/projects/kmp/kmp-capi] as hierarchies to watch
Watching the file system is configured to be enabled if available
File system watching is active
Using local directory build cache for the root build (location = /Users/liluo/.gradle/caches/build-cache-1, remove unused entries = after 7 days).
Tasks to be executed: [task ':composeApp:kmpPartiallyResolvedDependenciesChecker', task ':composeApp:checkKotlinGradlePluginConfigurationErrors', task ':composeApp:downloadKotlinNativeDistribution', task ':composeApp:compileKotlinOhosArm64']
Tasks that were excluded: []
Resolve mutations for :composeApp:kmpPartiallyResolvedDependenciesChecker (Thread[Execution worker,5,main]) started.
Resolve mutations for :composeApp:downloadKotlinNativeDistribution (Thread[Execution worker Thread 2,5,main]) started.
:composeApp:kmpPartiallyResolvedDependenciesChecker (Thread[Execution worker,5,main]) started.
:composeApp:downloadKotlinNativeDistribution (Thread[Execution worker Thread 2,5,main]) started.
> Task :composeApp:kmpPartiallyResolvedDependenciesChecker
Caching disabled for task ':composeApp:kmpPartiallyResolvedDependenciesChecker' because:
This task is not intended for execution
Task ':composeApp:kmpPartiallyResolvedDependenciesChecker' is not up-to-date because:
Task has not declared any outputs despite executing actions.
Execution stopped by some action with message: null
Resolve mutations for :composeApp:checkKotlinGradlePluginConfigurationErrors (Thread[Execution worker,5,main]) started.
Build 3d72bc78-a843-4599-a7a1-31ffbd23ec35 is started
:composeApp:checkKotlinGradlePluginConfigurationErrors (Thread[Execution worker,5,main]) started.
> Task :composeApp:checkKotlinGradlePluginConfigurationErrors SKIPPED
Skipping task ':composeApp:checkKotlinGradlePluginConfigurationErrors' as task onlyIf 'errorDiagnostics are present' is false.
> Task :composeApp:downloadKotlinNativeDistribution UP-TO-DATE
Kotlin Native Bundle: Acquire lock: /Users/liluo/.konan/kotlin-native-prebuilt-macos-aarch64-2.2.21-ohos-01/.lock ...
Kotlin Native Bundle: Lock acquired: /Users/liluo/.konan/kotlin-native-prebuilt-macos-aarch64-2.2.21-ohos-01/.lock
Kotlin Native Bundle: Lock released: /Users/liluo/.konan/kotlin-native-prebuilt-macos-aarch64-2.2.21-ohos-01/.lock
Build cache key for task ':composeApp:downloadKotlinNativeDistribution' is 9273d577ee8b60b158bbec6fa2f7f1c1
Skipping task ':composeApp:downloadKotlinNativeDistribution' as it is up-to-date.
Resolve mutations for :composeApp:compileKotlinOhosArm64 (Thread[Execution worker Thread 2,5,main]) started.
:composeApp:compileKotlinOhosArm64 (Thread[Execution worker Thread 2,5,main]) started.
> Task :composeApp:compileKotlinOhosArm64
Kotlin Native Bundle: Acquire lock: /Users/liluo/.konan/kotlin-native-prebuilt-macos-aarch64-2.2.21-ohos-01/.lock ...
Kotlin Native Bundle: Lock acquired: /Users/liluo/.konan/kotlin-native-prebuilt-macos-aarch64-2.2.21-ohos-01/.lock
Kotlin Native Bundle: Lock released: /Users/liluo/.konan/kotlin-native-prebuilt-macos-aarch64-2.2.21-ohos-01/.lock
Build cache key for task ':composeApp:compileKotlinOhosArm64' is 76a41cd61dab48a66dffb84e476394f4
Task ':composeApp:compileKotlinOhosArm64' is not up-to-date because:
Task has failed previously.
file or directory '/Users/liluo/mine/projects/kmp/kmp-capi/composeApp/src/nativeMain/kotlin', not found
file or directory '/Users/liluo/mine/projects/kmp/kmp-capi/composeApp/src/nativeMain/kotlin', not found
file or directory '/Users/liluo/mine/projects/kmp/kmp-capi/composeApp/src/nativeMain/kotlin', not found
Stored cache entry for task ':composeApp:compileKotlinOhosArm64' with cache key 76a41cd61dab48a66dffb84e476394f4
Build 3d72bc78-a843-4599-a7a1-31ffbd23ec35 is closed
BUILD SUCCESSFUL in 742ms
3 actionable tasks: 2 executed, 1 up-to-date
Watched directory hierarchies: [/Users/liluo/mine/projects/kmp/kmp-capi]
Configuration cache entry reused.
# KMP-CAPI HarmonyOS Interop 示例项目
本项目是一个 Kotlin Multiplatform (KMP) 工程,展示了如何在 HarmonyOS (OpenHarmony) 平台上,通过 Kotlin/Native 与 HarmonyOS 的 C-API (ArkUI Native Node API & Custom Dialog API) 进行深度交互。
核心特色:**在 Kotlin/C++ 侧直接构建原生 UI 并控制系统组件,无需完全依赖 ArkTS。**
## 1. 项目结构
* **`composeApp/`**: KMP 共享代码模块。
* `src/commonMain`: 跨平台通用业务逻辑。
* `src/ohosArm64Main`: **[核心]** HarmonyOS 特有的 Kotlin/Native 实现。包含与 C-API 交互的绑定(`cinterop`)和导出给 C++ 的接口(`OhosExports.kt`)。
* **`harmonyApp/`**: HarmonyOS 原生工程(DevEco Studio 工程)。
* `entry/src/main/cpp`: **[核心]** C++ 桥接层(NAPI)。负责实现具体的 UI 构建逻辑(`entry_module.cpp`),并将 Kotlin 接口暴露给 ArkTS。
* `entry/src/main/ets`: ArkTS UI 界面,作为应用入口。
## 2. 环境要求
* **IDE**:
* IntelliJ IDEA / Android Studio (用于编写 Kotlin 代码)
* DevEco Studio (用于编译和运行 HarmonyOS 应用)
* **SDK**:
* HarmonyOS SDK (API 12+ / OpenHarmony 5.0+) - **必须支持 ArkUI Native Node API**
* **语言**:
* Kotlin 1.9+
* Java 17+
* C++ (CMake)
## 3. 如何启动运行
### 步骤 1: 编译 Kotlin/Native 产物
在项目根目录下运行以下命令,将 Kotlin 代码编译为动态库(`libkn.so`)并发布到 HarmonyOS 工程中:
```bash
./gradlew publishDebugBinariesToHarmonyApp
```
*此任务会自动将生成的 `.so` 文件和头文件复制到 `harmonyApp/entry/libs` 和 `harmonyApp/entry/src/main/cpp/include` 目录。*
### 步骤 2: 运行 HarmonyOS 应用
1. 使用 **DevEco Studio** 打开 `harmonyApp` 目录。
2. 等待 Sync 完成。
3. 连接 HarmonyOS 真机或启动模拟器(API 12+)。
4. 点击 **Run 'entry'**
---
## 4. 核心功能详解:Kotlin 调用 HarmonyOS Native 弹窗
本项目成功实现了一个复杂的场景:**在 C++ 层构建 UI 节点树,传递给 Kotlin 层,再由 Kotlin 层调用 ArkUI C-API 显示原生自定义弹窗。**
### 实现原理
1. **C++ (ArkUI Node API)**: 动态创建 UI 组件树(Column, Row, Text, Button)。
2. **Kotlin/Native (Interop)**: 接收 C++ 创建的 Node 指针,封装为 `ArkUI_CustomDialogOptions`
3. **ArkUI Dialog API**: 调用 `OH_ArkUI_CustomDialog_OpenDialog` 显示弹窗。
### 详细实现步骤
#### 1. C++ 侧构建复杂 UI (`entry_module.cpp`)
我们在 C++ 中使用 `ArkUI_NativeNodeAPI_1` 手动构建一个包含标题栏、内容区和底部按钮的复杂布局。
```cpp
// 示例:创建复杂的弹窗内容节点
ArkUI_NodeHandle root = api->createNode(ARKUI_NODE_COLUMN);
// 设置 Padding
ArkUI_NumberValue paddingValue[] = {{.f32 = 12.0f}, {.f32 = 12.0f}, ...};
api->setAttribute(root, NODE_PADDING, &paddingItem);
// 创建标题栏 (Row)
ArkUI_NodeHandle titleRow = api->createNode(ARKUI_NODE_ROW);
ArkUI_NodeHandle titleText = CreateTextNode(api, "标题", 20.0f);
ArkUI_NodeHandle closeBtn = CreateTextNode(api, "X", 20.0f);
// 注册点击事件 (关闭弹窗)
api->registerNodeEvent(closeBtn, NODE_ON_CLICK, 0, nullptr);
api->addNodeEventReceiver(closeBtn, OnCloseDialog); // OnCloseDialog 调用 closeBasicDialog()
// ... 组装节点树 ...
api->addChild(root, titleRow);
api->addChild(root, contentText);
api->addChild(root, buttonRow);
// 将生成的根节点指针转换为 long 传递给 Kotlin
openBasicDialog(reinterpret_cast<int64_t>(root));
```
#### 2. Kotlin 侧定义弹窗逻辑 (`OhosExports.kt`)
Kotlin 侧通过 `@CName` 暴露接口给 C++,接收 Node 指针,并配置弹窗样式。
```kotlin
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
@CName("openBasicDialog")
fun openBasicDialog(nodeAddress: Long): Int {
// 1. 将 long 转换回 C 指针
val node = if (nodeAddress == 0L) null else nodeAddress.toCPointer<COpaque>()
if (node == null) return -31
// 2. 创建弹窗选项 (Options)
val options = OH_ArkUI_CustomDialog_CreateOptions(node)
// 3. 配置弹窗样式 (去除默认大圆角,设置白底)
OH_ArkUI_CustomDialog_SetModalMode(options, true)
OH_ArkUI_CustomDialog_SetCornerRadius(options, 10f, 10f, 10f, 10f) // 设置 10vp 圆角
OH_ArkUI_CustomDialog_SetBackgroundColor(options, 0xFFFFFFFF.toUInt()) // 白色背景
// 4. 定义回调 (DialogId 用于关闭)
val cb = staticCFunction { dialogId: Int ->
lastDialogId = dialogId // 保存 ID 用于后续关闭
Unit
}
// 5. 打开弹窗
val code = OH_ArkUI_CustomDialog_OpenDialog(options, cb)
return code
}
```
#### 3. C++ 侧事件处理与关闭
当用户点击 C++ 创建的按钮时,触发回调,调用 Kotlin 暴露的关闭函数。
```cpp
// entry_module.cpp
void OnCloseDialog(ArkUI_NodeEvent* event) {
closeBasicDialog(); // 调用 Kotlin 暴露的关闭函数
}
// OhosExports.kt
@CName("closeBasicDialog")
fun closeBasicDialog(): Int {
return OH_ArkUI_CustomDialog_CloseDialog(lastDialogId)
}
```
#### 4. ArkTS 入口调用
最终在 ArkTS 侧,只需要简单地调用 NAPI 暴露的方法即可触发整个流程。
```typescript
// Index.ets
import { openBasicDialog } from 'libentry.so';
Button("Options样式弹窗")
.onClick(() => {
// 触发 C++ -> Kotlin -> ArkUI Native 的调用链
openBasicDialog();
})
```
## 5. 常见问题排查
* **Crash (SIGSEGV)**: 通常是因为传递了无效的 Node 指针给 `OH_ArkUI_CustomDialog_CreateOptions`。请确保 Node 已正确创建且未被释放。
* **弹窗样式异常(大圆角/透明)**: 默认系统样式可能带有较大的圆角。使用 `OH_ArkUI_CustomDialog_SetCornerRadius``OH_ArkUI_CustomDialog_SetBackgroundColor` 进行显式配置。
* **Error -31**: 自定义错误码,表示传入的 Node 指针为空。
---
Happy Coding with KMP & HarmonyOS!
plugins {
// this is necessary to avoid the plugins to be loaded multiple times
// in each subproject's classloader
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
}
\ No newline at end of file
import android.databinding.tool.ext.capitalizeUS
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidApplication)
}
kotlin {
androidTarget {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
listOf(
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "ComposeApp"
isStatic = true
}
}
ohosArm64 {
binaries.sharedLib {
baseName = "kn"
// linkerOpts(
// "-lnative_drawing",
// "-lpixelmap",
// "-lpixelmap_ndk.z",
// "-lace_ndk.z",
// "-lace_napi.z",
// "-lhilog_ndk.z",
// "-limage_source",
// "-lhitrace_ndk.z",
// "-luv",
// "-lnative_window",
// "-lunwind",
// )
// // 在debug和release模式,让gradle都打包带符号表的so
// // release包在鸿蒙应用打包时strip掉so中的符号表
// // 打包出的符号表可以对现网崩溃进行 addr2line
// debuggable = true
}
compilations.all {
compilerOptions.configure {
// uncomment below to enable address asnitizer
// freeCompilerArgs.add("-Xbinary=sanitizer=address")
}
}
val main by compilations.getting
}
sourceSets {
androidMain.dependencies {}
commonMain.dependencies {
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
val ohosArm64Main by getting
}
}
android {
namespace = "com.example.kmpmultiplatform"
compileSdk = libs.versions.android.compileSdk.get().toInt()
defaultConfig {
applicationId = "com.example.kmpmultiplatform"
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 1
versionName = "1.0"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {}
arrayOf("debug", "release").forEach { type ->
tasks.register<Copy>("publish${type.capitalizeUS()}BinariesToHarmonyApp") {
group = "harmony"
dependsOn("link${type.capitalizeUS()}SharedOhosArm64")
into(rootProject.file("harmonyApp"))
from("build/bin/ohosArm64/${type}Shared/libkn_api.h") {
into("entry/src/main/cpp/include/")
}
from(project.file("build/bin/ohosArm64/${type}Shared/libkn.so")) {
into("entry/libs/arm64-v8a/")
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity
android:exported="true"
android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
\ No newline at end of file
package com.example.kmpmultiplatform
import android.app.Activity
import android.os.Bundle
import android.graphics.Color
import android.graphics.Typeface
import android.view.Gravity
import android.widget.FrameLayout
import android.widget.TextView
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val content = App()
val tv = TextView(this)
tv.text = content.text
tv.setTextColor(Color.parseColor(content.colorHex))
tv.textSize = content.sizeSp
tv.typeface = if (content.bold) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
val container = FrameLayout(this)
val lp = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT
)
lp.gravity = Gravity.CENTER
container.addView(tv, lp)
setContentView(container)
}
}
package com.example.kmpmultiplatform
import android.os.Build
class AndroidPlatform : Platform {
override val name: String = "Android ${Build.VERSION.SDK_INT}"
}
actual fun getPlatform(): Platform = AndroidPlatform()
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
\ No newline at end of file
<resources>
<string name="app_name">KMPMultiplatform</string>
</resources>
\ No newline at end of file
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="450dp"
android:height="450dp"
android:viewportWidth="64"
android:viewportHeight="64">
<path
android:pathData="M56.25,18V46L32,60 7.75,46V18L32,4Z"
android:fillColor="#6075f2"/>
<path
android:pathData="m41.5,26.5v11L32,43V60L56.25,46V18Z"
android:fillColor="#6b57ff"/>
<path
android:pathData="m32,43 l-9.5,-5.5v-11L7.75,18V46L32,60Z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="23.131"
android:centerY="18.441"
android:gradientRadius="42.132"
android:type="radial">
<item android:offset="0" android:color="#FF5383EC"/>
<item android:offset="0.867" android:color="#FF7F52FF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M22.5,26.5 L32,21 41.5,26.5 56.25,18 32,4 7.75,18Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="44.172"
android:startY="4.377"
android:endX="17.973"
android:endY="34.035"
android:type="linear">
<item android:offset="0" android:color="#FF33C3FF"/>
<item android:offset="0.878" android:color="#FF5383EC"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m32,21 l9.526,5.5v11L32,43 22.474,37.5v-11z"
android:fillColor="#000000"/>
</vector>
\ No newline at end of file
package com.example.kmpmultiplatform
data class AppText(
val text: String,
val colorHex: String,
val sizeSp: Float,
val bold: Boolean
)
fun App(): AppText = AppText(
text = "Hello World",
colorHex = "#181818",
sizeSp = 32f,
bold = true
)
package com.example.kmpmultiplatform
class Greeting {
private val platform = getPlatform()
fun greet(): String {
return "Hello, ${platform.name}!"
}
}
\ No newline at end of file
package com.example.kmpmultiplatform
interface Platform {
val name: String
}
expect fun getPlatform(): Platform
\ No newline at end of file
package com.example.kmpmultiplatform
import kotlin.test.Test
import kotlin.test.assertEquals
class ComposeAppCommonTest {
@Test
fun example() {
assertEquals(3, 1 + 2)
}
}
\ No newline at end of file
package com.example.kmpmultiplatform
import platform.UIKit.UIColor
import platform.UIKit.UIViewController
import platform.UIKit.UILabel
import platform.UIKit.UITextAlignmentCenter
import platform.UIKit.UIFont
import platform.CoreGraphics.CGRectMake
import platform.CoreGraphics.CGPointMake
private fun uiColorFromHex(hex: String): UIColor {
val clean = if (hex.startsWith("#")) hex.substring(1) else hex
val value = clean.toLong(16)
val r = ((value shr 16) and 0xFF).toDouble() / 255.0
val g = ((value shr 8) and 0xFF).toDouble() / 255.0
val b = (value and 0xFF).toDouble() / 255.0
return UIColor(red = r, green = g, blue = b, alpha = 1.0)
}
private class MainViewControllerImpl : UIViewController() {
private val label = UILabel(frame = CGRectMake(0.0, 0.0, 300.0, 80.0))
override fun viewDidLoad() {
super.viewDidLoad()
val content = App()
view.backgroundColor = UIColor.whiteColor
label.text = content.text
label.textAlignment = UITextAlignmentCenter
label.textColor = uiColorFromHex(content.colorHex)
label.font = if (content.bold) UIFont.boldSystemFontOfSize(content.sizeSp.toDouble())
else UIFont.systemFontOfSize(content.sizeSp.toDouble())
label.sizeToFit()
view.addSubview(label)
}
override fun viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
label.center = CGPointMake(view.center.x, view.center.y)
}
}
fun MainViewController(): UIViewController {
return MainViewControllerImpl()
}
package com.example.kmpmultiplatform
import platform.UIKit.UIDevice
class IOSPlatform: Platform {
override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}
actual fun getPlatform(): Platform = IOSPlatform()
\ No newline at end of file
#ifndef GLOBAL_RAW_FILE_H
#define GLOBAL_RAW_FILE_H
#ifdef __cplusplus
extern "C" {
#endif
struct RawFile;
typedef struct RawFile RawFile;
int OH_ResourceManager_ReadRawFile(const RawFile *rawFile, void *buf, size_t length);
long OH_ResourceManager_GetRawFileSize(RawFile *rawFile);
void OH_ResourceManager_CloseRawFile(RawFile *rawFile);
#ifdef __cplusplus
};
#endif
/** @} */
#endif // GLOBAL_RAW_FILE_H
#ifndef GLOBAL_NATIVE_RESOURCE_MANAGER_H
#define GLOBAL_NATIVE_RESOURCE_MANAGER_H
#include "napi/native_api.h"
#include "raw_file.h"
#ifdef __cplusplus
extern "C" {
#endif
struct NativeResourceManager;
typedef struct NativeResourceManager NativeResourceManager;
NativeResourceManager *OH_ResourceManager_InitNativeResourceManager(napi_env env, napi_value jsResMgr);
void OH_ResourceManager_ReleaseNativeResourceManager(NativeResourceManager *resMgr);
RawFile *OH_ResourceManager_OpenRawFile(const NativeResourceManager *mgr, const char *fileName);
#ifdef __cplusplus
};
#endif
/** @} */
#endif // GLOBAL_NATIVE_RESOURCE_MANAGER_H
package = platform.resource
headers = raw_file_manager.h raw_file.h
\ No newline at end of file
package = sanTest
headers = san_test.h
package com.example.kmpmultiplatform
class OHOSPlatform : Platform {
override val name: String = "OpenHarmony"
}
actual fun getPlatform(): Platform = OHOSPlatform()
package com.example.test.AVCodecKit.AVCapability
import kotlinx.cinterop.*
import kotlin.experimental.ExperimentalNativeApi
import platform.AVCodecKit.AVCapability.*
import platform.AVCodecKit.Core.*
import com.example.test.common.ApiGuard
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcGetName")
fun avcGetName(mime: String, isEncoder: UInt): String {
return ApiGuard.guardString {
val cap = OH_AVCodec_GetCapability(mime, isEncoder != 0u)
val p = OH_AVCapability_GetName(cap)
p?.toKString() ?: "error"
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcGetMaxSupportedInstances")
fun avcGetMaxSupportedInstances(mime: String, isEncoder: UInt): Int {
return ApiGuard.guardInt {
val cap = OH_AVCodec_GetCapability(mime, isEncoder != 0u)
OH_AVCapability_GetMaxSupportedInstances(cap)
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcIsHardware")
fun avcIsHardware(mime: String, isEncoder: UInt): Int {
return ApiGuard.guardInt {
val cap = OH_AVCodec_GetCapability(mime, isEncoder != 0u)
if (OH_AVCapability_IsHardware(cap)) 1 else 0
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcGetEncoderBitrateRange")
fun avcGetEncoderBitrateRange(mime: String): String {
return ApiGuard.guardString {
val cap = OH_AVCodec_GetCapability(mime, true)
memScoped {
val range = alloc<OH_AVRange>()
val code = OH_AVCapability_GetEncoderBitrateRange(cap, range.ptr)
if (code == 0u) "${range.minVal},${range.maxVal}" else "error"
}
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcGetVideoWidthRange")
fun avcGetVideoWidthRange(mime: String, isEncoder: UInt): String {
return ApiGuard.guardString {
val cap = OH_AVCodec_GetCapability(mime, isEncoder != 0u)
memScoped {
val range = alloc<OH_AVRange>()
val code = OH_AVCapability_GetVideoWidthRange(cap, range.ptr)
if (code == 0u) "${range.minVal},${range.maxVal}" else "error"
}
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcGetVideoHeightRange")
fun avcGetVideoHeightRange(mime: String, isEncoder: UInt): String {
return ApiGuard.guardString {
val cap = OH_AVCodec_GetCapability(mime, isEncoder != 0u)
memScoped {
val range = alloc<OH_AVRange>()
val code = OH_AVCapability_GetVideoHeightRange(cap, range.ptr)
if (code == 0u) "${range.minVal},${range.maxVal}" else "error"
}
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcIsVideoSizeSupported")
fun avcIsVideoSizeSupported(mime: String, isEncoder: UInt, width: Int, height: Int): Int {
return ApiGuard.guardInt {
val cap = OH_AVCodec_GetCapability(mime, isEncoder != 0u)
if (OH_AVCapability_IsVideoSizeSupported(cap, width, height)) 1 else 0
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcGetVideoFrameRateRange")
fun avcGetVideoFrameRateRange(mime: String, isEncoder: UInt): String {
return ApiGuard.guardString {
val cap = OH_AVCodec_GetCapability(mime, isEncoder != 0u)
memScoped {
val range = alloc<OH_AVRange>()
val code = OH_AVCapability_GetVideoFrameRateRange(cap, range.ptr)
if (code == 0u) "${range.minVal},${range.maxVal}" else "error"
}
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcGetNameByCategory")
fun avcGetNameByCategory(mime: String, isEncoder: UInt, category: UInt): String {
return ApiGuard.guardString {
val cap = OH_AVCodec_GetCapabilityByCategory(mime, isEncoder != 0u, category)
val p = OH_AVCapability_GetName(cap)
p?.toKString() ?: "error"
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcGetVideoWidthAlignment")
fun avcGetVideoWidthAlignment(mime: String, isEncoder: UInt): Int {
return ApiGuard.guardInt {
val cap = OH_AVCodec_GetCapability(mime, isEncoder != 0u)
memScoped {
val align = alloc<IntVar>()
val code = OH_AVCapability_GetVideoWidthAlignment(cap, align.ptr)
if (code == 0u) align.value else -1
}
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcGetVideoHeightAlignment")
fun avcGetVideoHeightAlignment(mime: String, isEncoder: UInt): Int {
return ApiGuard.guardInt {
val cap = OH_AVCodec_GetCapability(mime, isEncoder != 0u)
memScoped {
val align = alloc<IntVar>()
val code = OH_AVCapability_GetVideoHeightAlignment(cap, align.ptr)
if (code == 0u) align.value else -1
}
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcGetVideoWidthRangeForHeight")
fun avcGetVideoWidthRangeForHeight(mime: String, isEncoder: UInt, height: Int): String {
return ApiGuard.guardString {
val cap = OH_AVCodec_GetCapability(mime, isEncoder != 0u)
memScoped {
val range = alloc<OH_AVRange>()
val code = OH_AVCapability_GetVideoWidthRangeForHeight(cap, height, range.ptr)
if (code == 0u) "${range.minVal},${range.maxVal}" else "error"
}
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcGetVideoHeightRangeForWidth")
fun avcGetVideoHeightRangeForWidth(mime: String, isEncoder: UInt, width: Int): String {
return ApiGuard.guardString {
val cap = OH_AVCodec_GetCapability(mime, isEncoder != 0u)
memScoped {
val range = alloc<OH_AVRange>()
val code = OH_AVCapability_GetVideoHeightRangeForWidth(cap, width, range.ptr)
if (code == 0u) "${range.minVal},${range.maxVal}" else "error"
}
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcGetVideoFrameRateRangeForSize")
fun avcGetVideoFrameRateRangeForSize(mime: String, isEncoder: UInt, width: Int, height: Int): String {
return ApiGuard.guardString {
val cap = OH_AVCodec_GetCapability(mime, isEncoder != 0u)
memScoped {
val range = alloc<OH_AVRange>()
val code = OH_AVCapability_GetVideoFrameRateRangeForSize(cap, width, height, range.ptr)
if (code == 0u) "${range.minVal},${range.maxVal}" else "error"
}
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcAreVideoSizeAndFrameRateSupported")
fun avcAreVideoSizeAndFrameRateSupported(mime: String, isEncoder: UInt, width: Int, height: Int, frameRate: Int): Int {
return ApiGuard.guardInt {
val cap = OH_AVCodec_GetCapability(mime, isEncoder != 0u)
if (OH_AVCapability_AreVideoSizeAndFrameRateSupported(cap, width, height, frameRate)) 1 else 0
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcIsFeatureSupported")
fun avcIsFeatureSupported(mime: String, isEncoder: UInt, feature: UInt): Int {
return ApiGuard.guardInt {
val cap = OH_AVCodec_GetCapability(mime, isEncoder != 0u)
if (OH_AVCapability_IsFeatureSupported(cap, feature)) 1 else 0
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcGetFeaturePropertiesDump")
fun avcGetFeaturePropertiesDump(mime: String, isEncoder: UInt, feature: UInt): String {
return ApiGuard.guardString {
val cap = OH_AVCodec_GetCapability(mime, isEncoder != 0u)
val fmt = OH_AVCapability_GetFeatureProperties(cap, feature)
val s = OH_AVFormat_DumpInfo(fmt)
val ret = s?.toKString() ?: "error"
OH_AVFormat_Destroy(fmt)
ret
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("getAVCapabilityEnumValuesJson")
fun getAVCapabilityEnumValuesJson(): String {
return """
{
"AVCodecCategory": {
"HARDWARE": $HARDWARE,
"SOFTWARE": $SOFTWARE
},
"AVCapabilityFeature": {
"VIDEO_ENCODER_TEMPORAL_SCALABILITY": $VIDEO_ENCODER_TEMPORAL_SCALABILITY,
"VIDEO_ENCODER_LONG_TERM_REFERENCE": $VIDEO_ENCODER_LONG_TERM_REFERENCE,
"VIDEO_LOW_LATENCY": $VIDEO_LOW_LATENCY,
"VIDEO_ENCODER_B_FRAME": $VIDEO_ENCODER_B_FRAME
}
}
""".trimIndent()
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcGetAudioSupportedSampleRates")
fun avcGetAudioSupportedSampleRates(mime: String, isEncoder: UInt): String {
return ApiGuard.guardString {
val cap = OH_AVCodec_GetCapability(mime, isEncoder != 0u)
memScoped {
val ratesPtr = alloc<CPointerVar<IntVar>>()
val num = alloc<UIntVar>()
val code = OH_AVCapability_GetAudioSupportedSampleRates(cap, ratesPtr.ptr, num.ptr)
if (code == 0u) {
val count = num.value.toInt()
val arr = ratesPtr.value
if (arr != null && count > 0) {
buildString {
for (i in 0 until count) {
val v = arr[i]
append(v)
if (i != count - 1) append(',')
}
}
} else {
"error"
}
} else {
"error"
}
}
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcGetAudioSupportedSampleRateRanges")
fun avcGetAudioSupportedSampleRateRanges(mime: String, isEncoder: UInt): String {
return ApiGuard.guardString {
val cap = OH_AVCodec_GetCapability(mime, isEncoder != 0u)
memScoped {
val rangesPtr = alloc<CPointerVar<OH_AVRange>>()
val num = alloc<UIntVar>()
val code = OH_AVCapability_GetAudioSupportedSampleRateRanges(cap, rangesPtr.ptr, num.ptr)
if (code == 0u) {
num.value.toString()
} else {
"error"
}
}
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcGetAudioChannelCountRange")
fun avcGetAudioChannelCountRange(mime: String, isEncoder: UInt): String {
return ApiGuard.guardString {
val cap = OH_AVCodec_GetCapability(mime, isEncoder != 0u)
memScoped {
val range = alloc<OH_AVRange>()
val code = OH_AVCapability_GetAudioChannelCountRange(cap, range.ptr)
if (code == 0u) "${range.minVal},${range.maxVal}" else "error"
}
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcGetVideoSupportedPixelFormats")
fun avcGetVideoSupportedPixelFormats(mime: String, isEncoder: UInt): String {
return ApiGuard.guardString {
val cap = OH_AVCodec_GetCapability(mime, isEncoder != 0u)
memScoped {
val fmtsPtr = alloc<CPointerVar<IntVar>>()
val num = alloc<UIntVar>()
val code = OH_AVCapability_GetVideoSupportedPixelFormats(cap, fmtsPtr.ptr, num.ptr)
if (code == 0u) {
num.value.toString()
} else {
"error"
}
}
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcGetSupportedProfiles")
fun avcGetSupportedProfiles(mime: String, isEncoder: UInt): String {
return ApiGuard.guardString {
val cap = OH_AVCodec_GetCapability(mime, isEncoder != 0u)
memScoped {
val profPtr = alloc<CPointerVar<IntVar>>()
val num = alloc<UIntVar>()
val code = OH_AVCapability_GetSupportedProfiles(cap, profPtr.ptr, num.ptr)
if (code == 0u) {
num.value.toString()
} else {
"error"
}
}
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcGetSupportedLevelsForProfile")
fun avcGetSupportedLevelsForProfile(mime: String, isEncoder: UInt, profile: Int): String {
return ApiGuard.guardString {
val cap = OH_AVCodec_GetCapability(mime, isEncoder != 0u)
memScoped {
val lvlPtr = alloc<CPointerVar<IntVar>>()
val num = alloc<UIntVar>()
val code = OH_AVCapability_GetSupportedLevelsForProfile(cap, profile, lvlPtr.ptr, num.ptr)
if (code == 0u) {
num.value.toString()
} else {
"error"
}
}
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcAreProfileAndLevelSupported")
fun avcAreProfileAndLevelSupported(mime: String, isEncoder: UInt, profile: Int, level: Int): Int {
return ApiGuard.guardInt {
val cap = OH_AVCodec_GetCapability(mime, isEncoder != 0u)
if (OH_AVCapability_AreProfileAndLevelSupported(cap, profile, level)) 1 else 0
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcIsEncoderBitrateModeSupported")
fun avcIsEncoderBitrateModeSupported(mime: String, bitrateMode: UInt): Int {
return ApiGuard.guardInt {
val cap = OH_AVCodec_GetCapability(mime, true)
if (OH_AVCapability_IsEncoderBitrateModeSupported(cap, bitrateMode)) 1 else 0
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcGetEncoderQualityRange")
fun avcGetEncoderQualityRange(mime: String): String {
return ApiGuard.guardString {
val cap = OH_AVCodec_GetCapability(mime, true)
memScoped {
val range = alloc<OH_AVRange>()
val code = OH_AVCapability_GetEncoderQualityRange(cap, range.ptr)
if (code == 0u) "${range.minVal},${range.maxVal}" else "error"
}
}
}
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, ExperimentalNativeApi::class)
@CName("avcGetEncoderComplexityRange")
fun avcGetEncoderComplexityRange(mime: String): String {
return ApiGuard.guardString {
val cap = OH_AVCodec_GetCapability(mime, true)
memScoped {
val range = alloc<OH_AVRange>()
val code = OH_AVCapability_GetEncoderComplexityRange(cap, range.ptr)
if (code == 0u) "${range.minVal},${range.maxVal}" else "error"
}
}
}
package com.example.test.common
object ApiGuard {
const val CODE_API_VERSION_TOO_HIGH = -10001
const val STR_API_VERSION_TOO_HIGH = "API_VERSION_TOO_HIGH"
inline fun <T> guard(block: () -> T, illegalStateValue: T, otherErrorValue: T): T {
println("ApiGuard: 12")
return try {
println("ApiGuard: 22")
block()
} catch (e: IllegalStateException) {
println("ApiGuard: 当前API版本过高,请升级系统后再进行操作")
illegalStateValue
} catch (_: Throwable) {
println("ApiGuard: 其他错误")
otherErrorValue
}
}
inline fun guardInt(block: () -> Int): Int =
guard(block, CODE_API_VERSION_TOO_HIGH, -1)
inline fun guardLong(block: () -> Long): Long =
guard(block, CODE_API_VERSION_TOO_HIGH.toLong(), -1L)
inline fun guardDouble(block: () -> Double): Double =
guard(block, CODE_API_VERSION_TOO_HIGH.toDouble(), -1.0)
inline fun guardString(block: () -> String): String =
guard(block, STR_API_VERSION_TOO_HIGH, "error")
inline fun guardBoolean(block: () -> Boolean): Boolean =
guard(block, false, false)
}
#Kotlin
kotlin.code.style=official
kotlin.daemon.jvmargs=-Xmx3072M
#Gradle
org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8
org.gradle.configuration-cache=true
org.gradle.caching=true
#Android
android.nonTransitiveRClass=true
android.useAndroidX=true
\ No newline at end of file
[versions]
agp = "8.11.2"
android-compileSdk = "36"
android-minSdk = "24"
android-targetSdk = "36"
androidx-activity = "1.12.2"
androidx-appcompat = "1.7.1"
androidx-core = "1.17.0"
androidx-espresso = "3.7.0"
androidx-lifecycle = "2.9.6"
androidx-testExt = "1.3.0"
junit = "4.13.2"
kotlin = "2.2.21-ez-101"
[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
junit = { module = "junit:junit", version.ref = "junit" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
androidx-testExt-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-testExt" }
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
/node_modules
/oh_modules
/local.properties
/.idea
**/build
/.hvigor
.cxx
/.clangd
/.clang-format
/.clang-tidy
**/.test
/.appanalyzer
\ No newline at end of file
{
"app": {
"bundleName": "com.example.harmonyapp",
"vendor": "example",
"versionCode": 1000000,
"versionName": "1.0.0",
"icon": "$media:layered_image",
"label": "$string:app_name"
}
}
{
"string": [
{
"name": "app_name",
"value": "harmonyApp"
}
]
}
{
"layered-image":
{
"background" : "$media:background",
"foreground" : "$media:foreground"
}
}
\ No newline at end of file
# OpenHarmony 自动化测试执行器
## 功能特性
1. **灵活执行策略**
- 执行所有测试用例
- 通过配置文件执行部分测试用例
2. **详细的执行结果**
- 每个用例的执行状态(通过/失败/跳过/错误)
- 每个用例的执行时间
- 总执行时间和统计信息
3. **多种报告格式**
- JSON 格式(机器可读)
- HTML 格式(可视化报告)
- JUnit XML 格式
4. **高级功能**
- 超时控制
- 日志记录
## 单元测试的命名规则`run${file_name}UnitTest`
## 安装python依赖
```bash
pip3 install pyyaml
```
## 执行所有测试
```bash
python3 test_runner.py
```
## 命令
```bash
# 列出所有可用的测试
python3 test_runner.py --list-tests
```
## Test Config 使用说明
- 配置文件位置优先级(不传 --config 时自动加载):
- 1) test-config.json
- 2) test-config.yaml 或 test-config.yml
- 若同时存在 JSON 与 YAML,则优先使用 JSON
- 所有相对路径以 test_runner.py 所在目录为基准
### 字段
- test_directories
- 类型: string[]
- 范围: 任意有效目录路径(相对或绝对)
- output_dir
- 类型: string
- 范围: 任意有效目录路径
- report_formats
- 类型: string[]
- 可选: json, text, xml, html(任意组合)
- device_id
- 类型: string
- 示例: 127.0.0.1:5555(模拟器)或真机序列号
- tests(数组,每项支持以下结构)
- { "file": "Ability.test.ets" } 或 { "pattern": "Ability" }:按文件名子串筛选,自动映射 describe 套件
- { "class": "runAbilityUnitTest" }:直接指定 describe 套件
- { "class": "runAbilityUnitTest", "method": "assertContain" }:指定单个 it,用于精确过滤
- { "kit": "PerformanceAnalysisKit" }:按目录子串筛选范围(例如仅执行该目录下的所有测试)
- { "kit": "PerformanceAnalysisKit", "component": "HiLog" }:按 kit 下子目录筛选(例如仅执行 HiLog 组件下的所有测试)
- 备注:tests 为 [] 或 {} 表示执行全部测试(不做额外筛选)
### 示例(JSON)
```json
{
"test_directories": ["./entry/src/ohosTest/ets/test/"],
"output_dir": "./test-reports",
"report_formats": ["json", "text", "xml", "html"],
"device_id": "127.0.0.1:5555",
"tests": [
{ "class": "runOhLogPrintMsgUnitTest", "method": "runOhLogPrintMsgUnitTest_withValidParams" },
{ "kit": "PerformanceAnalysisKit", "component": "HiLog" }
]
}
```
### 示例(YAML)
```yaml
test_directories:
- "./entry/src/ohosTest/ets/test/"
output_dir: "./test-reports"
report_formats: ["json", "text", "xml", "html"]
device_id: "127.0.0.1:5555"
tests:
- class: "runOhLogPrintMsgUnitTest"
method: "runOhLogPrintMsgUnitTest_withValidParams"
- kit: "PerformanceAnalysisKit"
component: "HiLog"
```
### 执行(DevEco侧)
- 自动加载配置:
```bash
python3 test_runner.py
```
- 指定配置路径:
```bash
python3 test_runner.py --config test-config.yaml
```
{
"app": {
"signingConfigs": [],
"products": [
{
"name": "default",
"signingConfig": "default",
"targetSdkVersion": "6.0.1(21)",
"compatibleSdkVersion": "5.0.5(17)",
"runtimeOS": "HarmonyOS",
"buildOption": {
"strictMode": {
"caseSensitiveCheck": true,
"useNormalizedOHMUrl": true
}
}
}
],
"buildModeSet": [
{
"name": "debug",
},
{
"name": "release"
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
}
]
}
\ No newline at end of file
{
"files": [
"**/*.ets"
],
"ignore": [
"**/src/ohosTest/**/*",
"**/src/test/**/*",
"**/src/mock/**/*",
"**/node_modules/**/*",
"**/oh_modules/**/*",
"**/build/**/*",
"**/.preview/**/*"
],
"ruleSet": [
"plugin:@performance/recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@security/no-unsafe-aes": "error",
"@security/no-unsafe-hash": "error",
"@security/no-unsafe-mac": "warn",
"@security/no-unsafe-dh": "error",
"@security/no-unsafe-dsa": "error",
"@security/no-unsafe-ecdsa": "error",
"@security/no-unsafe-rsa-encrypt": "error",
"@security/no-unsafe-rsa-sign": "error",
"@security/no-unsafe-rsa-key": "error",
"@security/no-unsafe-dsa-key": "error",
"@security/no-unsafe-dh-key": "error",
"@security/no-unsafe-3des": "error"
}
}
\ No newline at end of file
/node_modules
/oh_modules
/.preview
/build
/.cxx
/.test
\ No newline at end of file
{
"apiType": "stageMode",
"buildOption": {
"externalNativeOptions": {
"path": "./src/main/cpp/CMakeLists.txt",
"arguments": "",
"cppFlags": "",
"abiFilters": [
"arm64-v8a"
]
},
"resOptions": {
"copyCodeResource": {
"enable": false
}
}
},
"buildOptionSet": [
{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": false,
"files": [
"./obfuscation-rules.txt"
]
}
}
}
},
],
"targets": [
{
"name": "default"
},
{
"name": "ohosTest",
}
]
}
import { hapTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
plugins: [] /* Custom plugin to extend the functionality of Hvigor. */
}
\ No newline at end of file
# Define project specific obfuscation rules here.
# You can include the obfuscation configuration files in the current module's build-profile.json5.
#
# For more details, see
# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5
# Obfuscation options:
# -disable-obfuscation: disable all obfuscations
# -enable-property-obfuscation: obfuscate the property names
# -enable-toplevel-obfuscation: obfuscate the names in the global scope
# -compact: remove unnecessary blank spaces and all line feeds
# -remove-log: remove all console.* statements
# -print-namecache: print the name cache that contains the mapping from the old names to new names
# -apply-namecache: reuse the given cache file
# Keep options:
# -keep-property-name: specifies property names that you want to keep
# -keep-global-name: specifies names that you want to keep in the global scope
-enable-property-obfuscation
-enable-toplevel-obfuscation
-enable-filename-obfuscation
-enable-export-obfuscation
\ No newline at end of file
{
"name": "entry",
"version": "1.0.0",
"description": "Please describe the basic information.",
"main": "",
"author": "",
"license": "",
"dependencies": {}
}
cmake_minimum_required(VERSION 3.10)
project(entry_module)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
file(GLOB KITS_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/kits/*.cpp")
add_library(entry SHARED
entry_module.cpp
${KITS_SOURCES}
)
target_include_directories(entry
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
# Link prebuilt libkn.so
add_library(kn SHARED IMPORTED)
set_target_properties(kn PROPERTIES
IMPORTED_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/arm64-v8a/libkn.so"
)
target_link_libraries(entry PUBLIC libace_napi.z.so)
target_link_libraries(entry PRIVATE kn)
#include "napi/native_api.h"
#include <vector>
typedef void (*KitRegistrar)(napi_env, napi_value);
static std::vector<KitRegistrar>& GetKitRegistrars()
{
static std::vector<KitRegistrar> registrars;
return registrars;
}
extern "C" void AddKitRegistrar(KitRegistrar fn)
{
GetKitRegistrars().push_back(fn);
}
static napi_value Init(napi_env env, napi_value exports) {
for (auto fn : GetKitRegistrars()) {
fn(env, exports);
}
return exports;
}
// Module name must match 'libentry'
NAPI_MODULE(libentry, Init)
This diff is collapsed.
#include "napi/native_api.h"
#include "libkn_api.h"
#include <string.h>
extern "C" const char* avcGetName(const char* mime, libkn_KUInt isEncoder);
extern "C" libkn_KInt avcGetMaxSupportedInstances(const char* mime, libkn_KUInt isEncoder);
extern "C" libkn_KInt avcIsHardware(const char* mime, libkn_KUInt isEncoder);
extern "C" const char* avcGetEncoderBitrateRange(const char* mime);
extern "C" const char* avcGetVideoWidthRange(const char* mime, libkn_KUInt isEncoder);
extern "C" const char* avcGetVideoHeightRange(const char* mime, libkn_KUInt isEncoder);
extern "C" libkn_KInt avcIsVideoSizeSupported(const char* mime, libkn_KUInt isEncoder, libkn_KInt width, libkn_KInt height);
extern "C" const char* avcGetVideoFrameRateRange(const char* mime, libkn_KUInt isEncoder);
extern "C" const char* getAVCapabilityEnumValuesJson();
extern "C" const char* avcGetNameByCategory(const char* mime, libkn_KUInt isEncoder, libkn_KUInt category);
extern "C" libkn_KInt avcGetVideoWidthAlignment(const char* mime, libkn_KUInt isEncoder);
extern "C" libkn_KInt avcGetVideoHeightAlignment(const char* mime, libkn_KUInt isEncoder);
extern "C" const char* avcGetVideoWidthRangeForHeight(const char* mime, libkn_KUInt isEncoder, libkn_KInt height);
extern "C" const char* avcGetVideoHeightRangeForWidth(const char* mime, libkn_KUInt isEncoder, libkn_KInt width);
extern "C" const char* avcGetVideoFrameRateRangeForSize(const char* mime, libkn_KUInt isEncoder, libkn_KInt width, libkn_KInt height);
extern "C" libkn_KInt avcAreVideoSizeAndFrameRateSupported(const char* mime, libkn_KUInt isEncoder, libkn_KInt width, libkn_KInt height, libkn_KInt frameRate);
extern "C" libkn_KInt avcIsFeatureSupported(const char* mime, libkn_KUInt isEncoder, libkn_KUInt feature);
extern "C" const char* avcGetFeaturePropertiesDump(const char* mime, libkn_KUInt isEncoder, libkn_KUInt feature);
extern "C" const char* avcGetAudioSupportedSampleRates(const char* mime, libkn_KUInt isEncoder);
extern "C" const char* avcGetAudioSupportedSampleRateRanges(const char* mime, libkn_KUInt isEncoder);
extern "C" const char* avcGetAudioChannelCountRange(const char* mime, libkn_KUInt isEncoder);
extern "C" const char* avcGetVideoSupportedPixelFormats(const char* mime, libkn_KUInt isEncoder);
extern "C" const char* avcGetSupportedProfiles(const char* mime, libkn_KUInt isEncoder);
extern "C" const char* avcGetSupportedLevelsForProfile(const char* mime, libkn_KUInt isEncoder, libkn_KInt profile);
extern "C" libkn_KInt avcAreProfileAndLevelSupported(const char* mime, libkn_KUInt isEncoder, libkn_KInt profile, libkn_KInt level);
extern "C" libkn_KInt avcIsEncoderBitrateModeSupported(const char* mime, libkn_KUInt bitrateMode);
extern "C" const char* avcGetEncoderQualityRange(const char* mime);
extern "C" const char* avcGetEncoderComplexityRange(const char* mime);
static napi_value AvcGetName(napi_env env, napi_callback_info info) {
size_t argc = 2; napi_value argv[2];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
uint32_t isEnc = 0; napi_get_value_uint32(env, argv[1], &isEnc);
const char* s = avcGetName(mime_buffer, (libkn_KUInt)isEnc);
if (s == nullptr) { s = "error"; }
napi_value result; napi_create_string_utf8(env, s, NAPI_AUTO_LENGTH, &result);
if (s && strcmp(s, "error") != 0) { libkn_symbols()->DisposeString(s); }
return result;
}
static napi_value AvcGetMaxSupportedInstances(napi_env env, napi_callback_info info) {
size_t argc = 2; napi_value argv[2];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
uint32_t isEnc = 0; napi_get_value_uint32(env, argv[1], &isEnc);
int code = avcGetMaxSupportedInstances(mime_buffer, (libkn_KUInt)isEnc);
napi_value result; napi_create_int32(env, code, &result);
return result;
}
extern "C" void RegisterAVCodecKit(napi_env env, napi_value exports) {
napi_value f1; napi_create_function(env, "avcGetName", NAPI_AUTO_LENGTH, AvcGetName, nullptr, &f1);
napi_set_named_property(env, exports, "avcGetName", f1);
napi_value f2; napi_create_function(env, "avcGetMaxSupportedInstances", NAPI_AUTO_LENGTH, AvcGetMaxSupportedInstances, nullptr, &f2);
napi_set_named_property(env, exports, "avcGetMaxSupportedInstances", f2);
napi_value f3; napi_create_function(env, "avcIsHardware", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 2; napi_value argv[2];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
uint32_t isEnc = 0; napi_get_value_uint32(env, argv[1], &isEnc);
int code = avcIsHardware(mime_buffer, (libkn_KUInt)isEnc);
napi_value result; napi_create_int32(env, code, &result);
return result;
}, nullptr, &f3);
napi_set_named_property(env, exports, "avcIsHardware", f3);
napi_value f4; napi_create_function(env, "avcGetEncoderBitrateRange", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 1; napi_value argv[1];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
const char* s = avcGetEncoderBitrateRange(mime_buffer);
if (s == nullptr) { s = "error"; }
napi_value result; napi_create_string_utf8(env, s, NAPI_AUTO_LENGTH, &result);
if (s && strcmp(s, "error") != 0) { libkn_symbols()->DisposeString(s); }
return result;
}, nullptr, &f4);
napi_set_named_property(env, exports, "avcGetEncoderBitrateRange", f4);
napi_value f5; napi_create_function(env, "avcGetVideoWidthRange", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 2; napi_value argv[2];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
uint32_t isEnc = 0; napi_get_value_uint32(env, argv[1], &isEnc);
const char* s = avcGetVideoWidthRange(mime_buffer, (libkn_KUInt)isEnc);
if (s == nullptr) { s = "error"; }
napi_value result; napi_create_string_utf8(env, s, NAPI_AUTO_LENGTH, &result);
if (s && strcmp(s, "error") != 0) { libkn_symbols()->DisposeString(s); }
return result;
}, nullptr, &f5);
napi_set_named_property(env, exports, "avcGetVideoWidthRange", f5);
napi_value f6; napi_create_function(env, "avcGetVideoHeightRange", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 2; napi_value argv[2];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
uint32_t isEnc = 0; napi_get_value_uint32(env, argv[1], &isEnc);
const char* s = avcGetVideoHeightRange(mime_buffer, (libkn_KUInt)isEnc);
if (s == nullptr) { s = "error"; }
napi_value result; napi_create_string_utf8(env, s, NAPI_AUTO_LENGTH, &result);
if (s && strcmp(s, "error") != 0) { libkn_symbols()->DisposeString(s); }
return result;
}, nullptr, &f6);
napi_set_named_property(env, exports, "avcGetVideoHeightRange", f6);
napi_value f7; napi_create_function(env, "avcIsVideoSizeSupported", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 4; napi_value argv[4];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
uint32_t isEnc = 0; napi_get_value_uint32(env, argv[1], &isEnc);
int32_t w = 0; napi_get_value_int32(env, argv[2], &w);
int32_t h = 0; napi_get_value_int32(env, argv[3], &h);
int code = avcIsVideoSizeSupported(mime_buffer, (libkn_KUInt)isEnc, w, h);
napi_value result; napi_create_int32(env, code, &result);
return result;
}, nullptr, &f7);
napi_set_named_property(env, exports, "avcIsVideoSizeSupported", f7);
napi_value f7b; napi_create_function(env, "avcGetVideoFrameRateRange", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 2; napi_value argv[2];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
uint32_t isEnc = 0; napi_get_value_uint32(env, argv[1], &isEnc);
const char* s = avcGetVideoFrameRateRange(mime_buffer, (libkn_KUInt)isEnc);
if (s == nullptr) { s = "error"; }
napi_value result; napi_create_string_utf8(env, s, NAPI_AUTO_LENGTH, &result);
if (s && strcmp(s, "error") != 0) { libkn_symbols()->DisposeString(s); }
return result;
}, nullptr, &f7b);
napi_set_named_property(env, exports, "avcGetVideoFrameRateRange", f7b);
napi_value f9; napi_create_function(env, "avcGetNameByCategory", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 3; napi_value argv[3];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
uint32_t isEnc = 0; napi_get_value_uint32(env, argv[1], &isEnc);
int32_t cat = 0; napi_get_value_int32(env, argv[2], &cat);
const char* s = avcGetNameByCategory(mime_buffer, (libkn_KUInt)isEnc, (libkn_KUInt)cat);
if (s == nullptr) { s = "error"; }
napi_value result; napi_create_string_utf8(env, s, NAPI_AUTO_LENGTH, &result);
if (s && strcmp(s, "error") != 0) { libkn_symbols()->DisposeString(s); }
return result;
}, nullptr, &f9);
napi_set_named_property(env, exports, "avcGetNameByCategory", f9);
napi_value f10; napi_create_function(env, "avcGetVideoWidthAlignment", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 2; napi_value argv[2];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
uint32_t isEnc = 0; napi_get_value_uint32(env, argv[1], &isEnc);
int code = avcGetVideoWidthAlignment(mime_buffer, (libkn_KUInt)isEnc);
napi_value result; napi_create_int32(env, code, &result);
return result;
}, nullptr, &f10);
napi_set_named_property(env, exports, "avcGetVideoWidthAlignment", f10);
napi_value f11; napi_create_function(env, "avcGetVideoHeightAlignment", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 2; napi_value argv[2];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
uint32_t isEnc = 0; napi_get_value_uint32(env, argv[1], &isEnc);
int code = avcGetVideoHeightAlignment(mime_buffer, (libkn_KUInt)isEnc);
napi_value result; napi_create_int32(env, code, &result);
return result;
}, nullptr, &f11);
napi_set_named_property(env, exports, "avcGetVideoHeightAlignment", f11);
napi_value f12; napi_create_function(env, "avcGetVideoWidthRangeForHeight", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 3; napi_value argv[3];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
uint32_t isEnc = 0; napi_get_value_uint32(env, argv[1], &isEnc);
int32_t h = 0; napi_get_value_int32(env, argv[2], &h);
const char* s = avcGetVideoWidthRangeForHeight(mime_buffer, (libkn_KUInt)isEnc, h);
if (s == nullptr) { s = "error"; }
napi_value result; napi_create_string_utf8(env, s, NAPI_AUTO_LENGTH, &result);
if (s && strcmp(s, "error") != 0) { libkn_symbols()->DisposeString(s); }
return result;
}, nullptr, &f12);
napi_set_named_property(env, exports, "avcGetVideoWidthRangeForHeight", f12);
napi_value f13; napi_create_function(env, "avcGetVideoHeightRangeForWidth", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 3; napi_value argv[3];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
uint32_t isEnc = 0; napi_get_value_uint32(env, argv[1], &isEnc);
int32_t w = 0; napi_get_value_int32(env, argv[2], &w);
const char* s = avcGetVideoHeightRangeForWidth(mime_buffer, (libkn_KUInt)isEnc, w);
if (s == nullptr) { s = "error"; }
napi_value result; napi_create_string_utf8(env, s, NAPI_AUTO_LENGTH, &result);
if (s && strcmp(s, "error") != 0) { libkn_symbols()->DisposeString(s); }
return result;
}, nullptr, &f13);
napi_set_named_property(env, exports, "avcGetVideoHeightRangeForWidth", f13);
napi_value f14; napi_create_function(env, "avcGetVideoFrameRateRangeForSize", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 4; napi_value argv[4];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
uint32_t isEnc = 0; napi_get_value_uint32(env, argv[1], &isEnc);
int32_t w = 0; napi_get_value_int32(env, argv[2], &w);
int32_t h = 0; napi_get_value_int32(env, argv[3], &h);
const char* s = avcGetVideoFrameRateRangeForSize(mime_buffer, (libkn_KUInt)isEnc, w, h);
if (s == nullptr) { s = "error"; }
napi_value result; napi_create_string_utf8(env, s, NAPI_AUTO_LENGTH, &result);
if (s && strcmp(s, "error") != 0) { libkn_symbols()->DisposeString(s); }
return result;
}, nullptr, &f14);
napi_set_named_property(env, exports, "avcGetVideoFrameRateRangeForSize", f14);
napi_value f15; napi_create_function(env, "avcAreVideoSizeAndFrameRateSupported", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 5; napi_value argv[5];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
uint32_t isEnc = 0; napi_get_value_uint32(env, argv[1], &isEnc);
int32_t w = 0; napi_get_value_int32(env, argv[2], &w);
int32_t h = 0; napi_get_value_int32(env, argv[3], &h);
int32_t fr = 0; napi_get_value_int32(env, argv[4], &fr);
int code = avcAreVideoSizeAndFrameRateSupported(mime_buffer, (libkn_KUInt)isEnc, w, h, fr);
napi_value result; napi_create_int32(env, code, &result);
return result;
}, nullptr, &f15);
napi_set_named_property(env, exports, "avcAreVideoSizeAndFrameRateSupported", f15);
napi_value f16; napi_create_function(env, "avcIsFeatureSupported", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 3; napi_value argv[3];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
uint32_t isEnc = 0; napi_get_value_uint32(env, argv[1], &isEnc);
int32_t feat = 0; napi_get_value_int32(env, argv[2], &feat);
int code = avcIsFeatureSupported(mime_buffer, (libkn_KUInt)isEnc, (libkn_KUInt)feat);
napi_value result; napi_create_int32(env, code, &result);
return result;
}, nullptr, &f16);
napi_set_named_property(env, exports, "avcIsFeatureSupported", f16);
napi_value f17; napi_create_function(env, "avcGetFeaturePropertiesDump", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 3; napi_value argv[3];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
uint32_t isEnc = 0; napi_get_value_uint32(env, argv[1], &isEnc);
int32_t feat = 0; napi_get_value_int32(env, argv[2], &feat);
const char* s = avcGetFeaturePropertiesDump(mime_buffer, (libkn_KUInt)isEnc, (libkn_KUInt)feat);
if (s == nullptr) { s = "error"; }
napi_value result; napi_create_string_utf8(env, s, NAPI_AUTO_LENGTH, &result);
if (s && strcmp(s, "error") != 0) { libkn_symbols()->DisposeString(s); }
return result;
}, nullptr, &f17);
napi_set_named_property(env, exports, "avcGetFeaturePropertiesDump", f17);
napi_value f8; napi_create_function(env, "getAVCapabilityEnumValuesJson", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 0; napi_value argv[1];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
const char* s = getAVCapabilityEnumValuesJson();
if (s == nullptr) { s = "error"; }
napi_value result; napi_create_string_utf8(env, s, NAPI_AUTO_LENGTH, &result);
if (s && strcmp(s, "error") != 0) { libkn_symbols()->DisposeString(s); }
return result;
}, nullptr, &f8);
napi_set_named_property(env, exports, "getAVCapabilityEnumValuesJson", f8);
napi_value f18; napi_create_function(env, "avcGetAudioSupportedSampleRates", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 2; napi_value argv[2];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
uint32_t isEnc = 0; napi_get_value_uint32(env, argv[1], &isEnc);
const char* s = avcGetAudioSupportedSampleRates(mime_buffer, (libkn_KUInt)isEnc);
if (s == nullptr) { s = "error"; }
napi_value result; napi_create_string_utf8(env, s, NAPI_AUTO_LENGTH, &result);
if (s && strcmp(s, "error") != 0) { libkn_symbols()->DisposeString(s); }
return result;
}, nullptr, &f18);
napi_set_named_property(env, exports, "avcGetAudioSupportedSampleRates", f18);
napi_value f19; napi_create_function(env, "avcGetAudioSupportedSampleRateRanges", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 2; napi_value argv[2];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
uint32_t isEnc = 0; napi_get_value_uint32(env, argv[1], &isEnc);
const char* s = avcGetAudioSupportedSampleRateRanges(mime_buffer, (libkn_KUInt)isEnc);
if (s == nullptr) { s = "error"; }
napi_value result; napi_create_string_utf8(env, s, NAPI_AUTO_LENGTH, &result);
if (s && strcmp(s, "error") != 0) { libkn_symbols()->DisposeString(s); }
return result;
}, nullptr, &f19);
napi_set_named_property(env, exports, "avcGetAudioSupportedSampleRateRanges", f19);
napi_value f20; napi_create_function(env, "avcGetAudioChannelCountRange", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 2; napi_value argv[2];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
uint32_t isEnc = 0; napi_get_value_uint32(env, argv[1], &isEnc);
const char* s = avcGetAudioChannelCountRange(mime_buffer, (libkn_KUInt)isEnc);
if (s == nullptr) { s = "error"; }
napi_value result; napi_create_string_utf8(env, s, NAPI_AUTO_LENGTH, &result);
if (s && strcmp(s, "error") != 0) { libkn_symbols()->DisposeString(s); }
return result;
}, nullptr, &f20);
napi_set_named_property(env, exports, "avcGetAudioChannelCountRange", f20);
napi_value f21; napi_create_function(env, "avcGetVideoSupportedPixelFormats", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 2; napi_value argv[2];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
uint32_t isEnc = 0; napi_get_value_uint32(env, argv[1], &isEnc);
const char* s = avcGetVideoSupportedPixelFormats(mime_buffer, (libkn_KUInt)isEnc);
if (s == nullptr) { s = "error"; }
napi_value result; napi_create_string_utf8(env, s, NAPI_AUTO_LENGTH, &result);
if (s && strcmp(s, "error") != 0) { libkn_symbols()->DisposeString(s); }
return result;
}, nullptr, &f21);
napi_set_named_property(env, exports, "avcGetVideoSupportedPixelFormats", f21);
napi_value f22; napi_create_function(env, "avcGetSupportedProfiles", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 2; napi_value argv[2];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
uint32_t isEnc = 0; napi_get_value_uint32(env, argv[1], &isEnc);
const char* s = avcGetSupportedProfiles(mime_buffer, (libkn_KUInt)isEnc);
if (s == nullptr) { s = "error"; }
napi_value result; napi_create_string_utf8(env, s, NAPI_AUTO_LENGTH, &result);
if (s && strcmp(s, "error") != 0) { libkn_symbols()->DisposeString(s); }
return result;
}, nullptr, &f22);
napi_set_named_property(env, exports, "avcGetSupportedProfiles", f22);
napi_value f23; napi_create_function(env, "avcGetSupportedLevelsForProfile", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 3; napi_value argv[3];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
uint32_t isEnc = 0; napi_get_value_uint32(env, argv[1], &isEnc);
int32_t profile = 0; napi_get_value_int32(env, argv[2], &profile);
const char* s = avcGetSupportedLevelsForProfile(mime_buffer, (libkn_KUInt)isEnc, profile);
if (s == nullptr) { s = "error"; }
napi_value result; napi_create_string_utf8(env, s, NAPI_AUTO_LENGTH, &result);
if (s && strcmp(s, "error") != 0) { libkn_symbols()->DisposeString(s); }
return result;
}, nullptr, &f23);
napi_set_named_property(env, exports, "avcGetSupportedLevelsForProfile", f23);
napi_value f24; napi_create_function(env, "avcAreProfileAndLevelSupported", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 4; napi_value argv[4];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
uint32_t isEnc = 0; napi_get_value_uint32(env, argv[1], &isEnc);
int32_t profile = 0; napi_get_value_int32(env, argv[2], &profile);
int32_t level = 0; napi_get_value_int32(env, argv[3], &level);
int code = avcAreProfileAndLevelSupported(mime_buffer, (libkn_KUInt)isEnc, profile, level);
napi_value result; napi_create_int32(env, code, &result);
return result;
}, nullptr, &f24);
napi_set_named_property(env, exports, "avcAreProfileAndLevelSupported", f24);
napi_value f25; napi_create_function(env, "avcIsEncoderBitrateModeSupported", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 2; napi_value argv[2];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
int32_t mode = 0; napi_get_value_int32(env, argv[1], &mode);
int code = avcIsEncoderBitrateModeSupported(mime_buffer, (libkn_KUInt)mode);
napi_value result; napi_create_int32(env, code, &result);
return result;
}, nullptr, &f25);
napi_set_named_property(env, exports, "avcIsEncoderBitrateModeSupported", f25);
napi_value f26; napi_create_function(env, "avcGetEncoderQualityRange", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 1; napi_value argv[1];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
const char* s = avcGetEncoderQualityRange(mime_buffer);
if (s == nullptr) { s = "error"; }
napi_value result; napi_create_string_utf8(env, s, NAPI_AUTO_LENGTH, &result);
if (s && strcmp(s, "error") != 0) { libkn_symbols()->DisposeString(s); }
return result;
}, nullptr, &f26);
napi_set_named_property(env, exports, "avcGetEncoderQualityRange", f26);
napi_value f27; napi_create_function(env, "avcGetEncoderComplexityRange", NAPI_AUTO_LENGTH, [](napi_env env, napi_callback_info info){
size_t argc = 1; napi_value argv[1];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
char mime_buffer[256]; size_t mime_len = 0; napi_get_value_string_utf8(env, argv[0], mime_buffer, 256, &mime_len);
const char* s = avcGetEncoderComplexityRange(mime_buffer);
if (s == nullptr) { s = "error"; }
napi_value result; napi_create_string_utf8(env, s, NAPI_AUTO_LENGTH, &result);
if (s && strcmp(s, "error") != 0) { libkn_symbols()->DisposeString(s); }
return result;
}, nullptr, &f27);
napi_set_named_property(env, exports, "avcGetEncoderComplexityRange", f27);
}
extern "C" void AddKitRegistrar(void (*fn)(napi_env, napi_value));
static void __avcodeckit_auto_register() __attribute__((constructor));
static void __avcodeckit_auto_register()
{
AddKitRegistrar(RegisterAVCodecKit);
}
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
const DOMAIN = 0x0000;
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// Check for testMode parameter
if (want.parameters && (want.parameters['testMode'] === true || want.parameters['testMode'] === 'true')) {
AppStorage.setOrCreate('testMode', true);
hilog.info(DOMAIN, 'testTag', 'Test Mode Enabled via Launch Param');
}
try {
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
} catch (err) {
hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
}
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
}
onDestroy(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}
onWindowStageDestroy(): void {
// Main window is destroyed, release UI related resources
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
}
onForeground(): void {
// Ability has brought to foreground
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
}
onBackground(): void {
// Ability has back to background
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
}
}
\ No newline at end of file
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
const DOMAIN = 0x0000;
export default class ResourceAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(DOMAIN, 'ResourceAbility', '%{public}s', 'onCreate');
}
onDestroy(): void {
hilog.info(DOMAIN, 'ResourceAbility', '%{public}s', 'onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
hilog.info(DOMAIN, 'ResourceAbility', '%{public}s', 'onWindowStageCreate');
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'ResourceAbility', 'Failed to load content: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'ResourceAbility', 'Succeeded in loading the content.');
});
}
onWindowStageDestroy(): void {
hilog.info(DOMAIN, 'ResourceAbility', '%{public}s', 'onWindowStageDestroy');
}
onForeground(): void {
hilog.info(DOMAIN, 'ResourceAbility', '%{public}s', 'onForeground');
}
onBackground(): void {
hilog.info(DOMAIN, 'ResourceAbility', '%{public}s', 'onBackground');
}
}
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit';
const DOMAIN = 0x0000;
export default class EntryBackupAbility extends BackupExtensionAbility {
async onBackup() {
hilog.info(DOMAIN, 'testTag', 'onBackup ok');
await Promise.resolve();
}
async onRestore(bundleVersion: BundleVersion) {
hilog.info(DOMAIN, 'testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion));
await Promise.resolve();
}
}
\ No newline at end of file
import { getComponent } from './component/registry';
import './component/index';
import ComponentHost from './component/ComponentHost';
import router from '@ohos.router';
import promptAction from '@ohos.promptAction';
interface ComponentParams { component?: string }
@Entry
@Component
struct ComponentDetail {
@State componentName: string = '';
@State nameResult: string = '';
@State maxResult: string = '';
@State printMsgResult: string = '';
@State printMsgLenResult: string = '';
aboutToAppear() {
try {
const params: ComponentParams = router.getParams() as ComponentParams;
this.componentName = params && params.component ? params.component : '';
} catch (e) {
this.componentName = '';
}
}
build() {
Column() {
Row() {
Text('C Api调用')
.fontSize(24)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.justifyContent(FlexAlign.Center)
.alignItems(VerticalAlign.Center)
Row() {
Text('<')
.fontSize(20)
.fontColor(Color.Blue)
.width(20)
.height(40)
.onClick(() => {
router.back();
})
Text(this.componentName.length > 0 ? this.componentName : '未知 component')
.fontSize(18)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.padding({ left: 8, right: 8, bottom: 16, top: 16 })
.alignItems(VerticalAlign.Center)
Column() {
if (getComponent(this.componentName)) {
ComponentHost({ child: getComponent(this.componentName)! })
} else {
Text('未知组件或未实现').fontSize(16).fontColor(Color.Red).padding(12)
}
}
.layoutWeight(1)
.width('100%')
.backgroundColor('#FFFFFF')
}
.height('100%')
.width('100%')
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Start)
}
}
import common from '@ohos.app.ability.common';
import router from '@ohos.router';
import util from '@ohos.util';
interface CapiItem {
kit: string;
component: string;
header: string;
}
interface ApiChild {
api: string;
name?: string;
}
interface HeaderNode {
header: string;
name?: string;
file?: string;
children?: ApiChild[];
api?: string[];
}
interface ComponentNode {
component: string;
name?: string;
children?: HeaderNode[];
}
interface KitNode {
kit: string;
name?: string;
children?: ComponentNode[];
}
type CapiRoot = KitNode | Array<KitNode | CapiItem>;
@Entry
@Component
struct Index {
@State capiData: CapiItem[] = [];
@State capiKitList: string[] = [];
@State capiComponentMap: Map<string, string[]> = new Map();
@State capiViewLevel: string = 'kit';
@State capiSelectedKit: string = '';
@State capiSearchQuery: string = '';
aboutToAppear() {
try {
this.loadCapiData();
} catch (_) {}
}
async loadCapiData() {
try {
const context = getContext(this) as common.UIAbilityContext;
const resourceMgr = context.resourceManager;
let bytes: Uint8Array;
try {
const res1: object = await resourceMgr.getRawFileContent('rawfile/capi_data.json');
bytes = res1 instanceof Uint8Array ? res1 as Uint8Array : new Uint8Array(res1 as ArrayBuffer);
} catch (e1) {
const res2: object = await resourceMgr.getRawFileContent('capi_data.json');
bytes = res2 instanceof Uint8Array ? res2 as Uint8Array : new Uint8Array(res2 as ArrayBuffer);
}
const textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true });
let s = textDecoder.decodeToString(bytes, { stream: false }).replace(/\uFEFF/g, '').trim();
const startBracket = s.indexOf('[');
const startBrace = s.indexOf('{');
let start = -1;
if (startBracket >= 0 && startBrace >= 0) {
start = Math.min(startBracket, startBrace);
} else {
start = startBracket >= 0 ? startBracket : startBrace;
}
const endBracket = s.lastIndexOf(']');
const endBrace = s.lastIndexOf('}');
let end = -1;
if (endBracket >= 0 && endBrace >= 0) {
end = Math.max(endBracket, endBrace);
} else {
end = endBracket >= 0 ? endBracket : endBrace;
}
if (start > 0 || (end >= 0 && end < s.length - 1)) {
s = s.slice(start >= 0 ? start : 0, end >= 0 ? end + 1 : s.length);
}
s = s.replace(/[^\x09\x0A\x0D\x20-\x7E\u00A0-\uFFFF]/g, '');
const parsed: CapiRoot = JSON.parse(s) as CapiRoot;
this.capiData = this.flattenCapi(parsed);
this.rebuildCapiIndex();
} catch (_) {}
}
private flattenCapi(parsed: CapiRoot): CapiItem[] {
const out: CapiItem[] = [];
const processKit = (kitItem: KitNode) => {
const kitName: string = kitItem.kit ?? kitItem.name ?? '';
const comps: ComponentNode[] = Array.isArray(kitItem.children) ? kitItem.children as ComponentNode[] : [];
comps.forEach((compItem: ComponentNode) => {
const compName: string = compItem.component ?? compItem.name ?? '';
const headers: HeaderNode[] = Array.isArray(compItem.children) ? compItem.children as HeaderNode[] : [];
headers.forEach((h: HeaderNode) => {
const headerName: string = h.header ?? h.name ?? h.file ?? '';
if (kitName && compName && headerName) {
out.push({ kit: kitName, component: compName, header: headerName });
}
});
});
};
if (Array.isArray(parsed)) {
parsed.forEach((item) => {
const comp = (item as CapiItem).component;
const hdr = (item as CapiItem).header;
if ((comp !== undefined) && (hdr !== undefined)) {
const flat = item as CapiItem;
out.push({ kit: flat.kit, component: flat.component, header: flat.header });
} else {
processKit(item as KitNode);
}
});
return out;
} else {
processKit(parsed as KitNode);
return out;
}
}
rebuildCapiIndex() {
const kits = new Set<string>();
const compSets: Map<string, Set<string>> = new Map();
this.capiData.forEach(item => {
kits.add(item.kit);
const set = compSets.get(item.kit) ?? new Set<string>();
set.add(item.component);
compSets.set(item.kit, set);
});
this.capiKitList = Array.from(kits).sort();
const m = new Map<string, string[]>();
Array.from(compSets.keys()).forEach(k => {
m.set(k, Array.from(compSets.get(k) as Set<string>).sort());
});
this.capiComponentMap = m;
}
private getFilteredKits(): string[] {
const q = (this.capiSearchQuery || '').toLowerCase();
if (!q) return this.capiKitList;
return this.capiKitList.filter(k => k.toLowerCase().includes(q));
}
private getFilteredComponents(): string[] {
const comps = this.capiComponentMap.get(this.capiSelectedKit) ?? [];
const q = (this.capiSearchQuery || '').toLowerCase();
if (!q) return comps;
return comps.filter(c => c.toLowerCase().includes(q));
}
build() {
Column() {
Row() {
Text('C Api调用')
.fontSize(24)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.justifyContent(FlexAlign.Center)
.alignItems(VerticalAlign.Center)
Column() {
Row() {
if (this.capiViewLevel === 'component') {
Text('<')
.fontSize(20)
.fontColor(Color.Blue)
.width(20)
.height(40)
.onClick(() => { this.capiViewLevel = 'kit'; this.capiSelectedKit = ''; this.capiSearchQuery = ''; })
}
Text(this.capiViewLevel === 'kit' ? 'Kit 列表' : 'Component 列表')
.fontSize(18)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
Row() {
TextInput({ placeholder: '搜索', text: this.capiSearchQuery })
.width('100%')
.height(40)
.onChange((v: string) => { this.capiSearchQuery = v; })
}
.padding({ left: 16, right: 16 })
.margin({ bottom: 8 })
List() {
if (this.capiViewLevel === 'kit') {
ForEach(this.getFilteredKits(), (kit: string) => {
ListItem() {
Row() {
Text(kit).layoutWeight(1).fontSize(16)
Text('>').fontSize(14).fontColor(Color.Gray)
}
.width('100%')
.padding({ top: 12, bottom: 12, left: 16, right: 16 })
.onClick(() => { this.capiSelectedKit = kit; this.capiViewLevel = 'component'; this.capiSearchQuery = ''; })
}
})
} else {
ForEach(this.getFilteredComponents(), (comp: string) => {
ListItem() {
Row() {
Text(comp).layoutWeight(1).fontSize(16)
}
.width('100%')
.padding({ top: 12, bottom: 12, left: 16, right: 16 })
.onClick(() => {
router.pushUrl({ url: 'pages/ComponentDetail', params: { component: comp } });
})
}
})
}
}
.layoutWeight(1)
.backgroundColor('#FFFFFF')
.borderRadius(8)
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Start)
.alignItems(HorizontalAlign.Start)
}
.height('100%')
.width('100%')
.alignItems(HorizontalAlign.Start)
.justifyContent(FlexAlign.Start)
}
}
import * as lib from 'libentry.so';
import promptAction from '@ohos.promptAction';
import { registerComponent } from '../registry';
import { avcGetName, avcGetMaxSupportedInstances, avcIsHardware, avcGetEncoderBitrateRange, avcGetVideoWidthRange, avcGetVideoHeightRange, avcIsVideoSizeSupported, getAVCapabilityEnumValuesJson, avcGetVideoFrameRateRange, avcGetNameByCategory, avcGetVideoWidthAlignment, avcGetVideoHeightAlignment, avcGetVideoWidthRangeForHeight, avcGetVideoHeightRangeForWidth, avcGetVideoFrameRateRangeForSize, avcAreVideoSizeAndFrameRateSupported, avcIsFeatureSupported, avcGetFeaturePropertiesDump, avcGetAudioSupportedSampleRates, avcGetAudioSupportedSampleRateRanges, avcGetAudioChannelCountRange, avcGetVideoSupportedPixelFormats, avcGetSupportedProfiles, avcGetSupportedLevelsForProfile, avcAreProfileAndLevelSupported, avcIsEncoderBitrateModeSupported, avcGetEncoderQualityRange, avcGetEncoderComplexityRange } from 'libentry.so';
import { describeAVCapabilityCategoryByName, describeAVCapabilityFeatureByName } from '../common/EnumDesc';
import { safeNumberCall, safeStringCall } from '../common/SafeLib';
@Component
export default struct AVCapabilityView {
@State nameResult: string = '';
@State maxResult: string = '';
@State hardwareResult: string = '';
@State bitrateRangeResult: string = '';
@State widthRangeResult: string = '';
@State heightRangeResult: string = '';
@State sizeSupportedResult: string = '';
@State enumCodecHardware: string = '';
@State enumCodecSoftware: string = '';
@State enumFeatureTemporalScalability: string = '';
@State enumFeatureLongTermRef: string = '';
@State enumFeatureLowLatency: string = '';
@State enumFeatureBFrame: string = '';
@State nameByCategoryResult: string = '';
@State widthAlignResult: string = '';
@State heightAlignResult: string = '';
@State widthRangeForHeightResult: string = '';
@State heightRangeForWidthResult: string = '';
@State frameRateRangeForSizeResult: string = '';
@State frameRateRangeResult: string = '';
@State sizeAndFrameRateSupportedResult: string = '';
@State featureSupportedResult: string = '';
@State featureDumpResult: string = '';
@State audioSampleRatesResult: string = '';
@State audioSampleRateRangesResult: string = '';
@State audioChannelCountRangeResult: string = '';
@State videoPixelFormatsResult: string = '';
@State supportedProfilesResult: string = '';
@State supportedLevelsForProfileResult: string = '';
@State profileLevelSupportedResult: string = '';
@State encoderBitrateModeSupportedResult: string = '';
@State encoderQualityRangeResult: string = '';
@State encoderComplexityRangeResult: string = '';
aboutToAppear() {
try {
const s: string = getAVCapabilityEnumValuesJson();
const getNum = (key: string): string => {
const m = new RegExp(`"${key}"\\s*:\\s*(\\d+)`).exec(s);
return m ? m[1] : '';
};
this.enumCodecHardware = getNum('HARDWARE');
this.enumCodecSoftware = getNum('SOFTWARE');
this.enumFeatureTemporalScalability = getNum('VIDEO_ENCODER_TEMPORAL_SCALABILITY');
this.enumFeatureLongTermRef = getNum('VIDEO_ENCODER_LONG_TERM_REFERENCE');
this.enumFeatureLowLatency = getNum('VIDEO_LOW_LATENCY');
this.enumFeatureBFrame = getNum('VIDEO_ENCODER_B_FRAME');
} catch (e) {
try { promptAction.showToast({ message: '枚举值获取失败' }); } catch (_) {}
}
}
build() {
Column() {
List() {
ListItem() {
Column() {
Row() {
Text('获取能力实例的名称').layoutWeight(1).fontSize(18)
Button('调用')
.onClick(() => {
try {
const s: string = safeStringCall((): string => avcGetName('video/avc', 1));
this.nameResult = s;
} catch (e) {
this.nameResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `GetName failed: ${e}`));
}
})
}.width('100%').padding(12)
Row() {
Text(`结果: ${this.nameResult}`).fontSize(16)
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
}
}
ListItem() {
Column() {
Row() {
Text('获取支持的最大并发实例数').layoutWeight(1).fontSize(18)
Button('调用')
.onClick(() => {
try {
const ret: number = safeNumberCall((): number => avcGetMaxSupportedInstances('video/avc', 1));
this.maxResult = ret >= 0 ? `${ret}` : '调用失败';
} catch (e) {
this.maxResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `GetMaxSupportedInstances failed: ${e}`));
}
})
}.width('100%').padding(12)
Row() {
Text(`结果: ${this.maxResult}`).fontSize(16)
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
}
}
ListItem() {
Column() {
Row() {
Text('检查能力实例是否描述了硬件编解码器').layoutWeight(1).fontSize(18)
Button('调用')
.onClick(() => {
try {
const ret: number = safeNumberCall((): number => avcIsHardware('video/avc', 1));
this.hardwareResult = ret === 1 ? '硬件编解码器' : (ret === 0 ? '软件编解码器' : '调用失败');
} catch (e) {
this.hardwareResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `IsHardware failed: ${e}`));
}
})
}.width('100%').padding(12)
Row() {
Text(`结果: ${this.hardwareResult}`).fontSize(16)
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
}
}
ListItem() {
Column() {
Row() {
Text('获取编码器支持的比特率范围').layoutWeight(1).fontSize(18)
Button('调用')
.onClick(() => {
try {
const s: string = safeStringCall((): string => avcGetEncoderBitrateRange('video/avc'));
this.bitrateRangeResult = s;
} catch (e) {
this.bitrateRangeResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `GetEncoderBitrateRange failed: ${e}`));
}
})
}.width('100%').padding(12)
Row() {
Text(`结果: ${this.bitrateRangeResult}`).fontSize(16)
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
}
}
ListItem() {
Column() {
Row() {
Text('获取视频宽度范围').layoutWeight(1).fontSize(18)
Button('调用')
.onClick(() => {
try {
const s: string = safeStringCall((): string => avcGetVideoWidthRange('video/avc', 1));
this.widthRangeResult = s;
} catch (e) {
this.widthRangeResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `GetVideoWidthRange failed: ${e}`));
}
})
}.width('100%').padding(12)
Row() {
Text(`结果: ${this.widthRangeResult}`).fontSize(16)
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
}
}
ListItem() {
Column() {
Row() {
Text('获取视频高度范围').layoutWeight(1).fontSize(18)
Button('调用')
.onClick(() => {
try {
const s: string = safeStringCall((): string => avcGetVideoHeightRange('video/avc', 1));
this.heightRangeResult = s;
} catch (e) {
this.heightRangeResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `GetVideoHeightRange failed: ${e}`));
}
})
}.width('100%').padding(12)
Row() {
Text(`结果: ${this.heightRangeResult}`).fontSize(16)
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
}
}
ListItem() {
Column() {
Row() {
Text('检查是否支持指定分辨率(示例:1920×1080)').layoutWeight(1).fontSize(18)
Button('调用')
.onClick(() => {
try {
const ret: number = safeNumberCall((): number => avcIsVideoSizeSupported('video/avc', 1, 1920, 1080));
this.sizeSupportedResult = ret === 1 ? '支持 1920x1080' : (ret === 0 ? '不支持 1920x1080' : '调用失败');
} catch (e) {
this.sizeSupportedResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `IsVideoSizeSupported failed: ${e}`));
}
})
}.width('100%').padding(12)
Row() {
Text(`结果: ${this.sizeSupportedResult}`).fontSize(16)
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
}
}
ListItem() {
Column() {
Row() {
Text('获取视频帧率范围').layoutWeight(1).fontSize(18)
Button('调用')
.onClick(() => {
try {
const s: string = safeStringCall((): string => avcGetVideoFrameRateRange('video/avc', 1));
this.frameRateRangeResult = s;
} catch (e) {
this.frameRateRangeResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `GetVideoFrameRateRange failed: ${e}`));
}
})
}.width('100%').padding(12)
Row() {
Text(`结果: ${this.frameRateRangeResult}`).fontSize(14)
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
}
}
ListItem() {
Column() {
Row() {
Text('根据类别获取能力实例的名称(硬件)').layoutWeight(1).fontSize(18)
Button('调用')
.onClick(() => {
try {
const s: string = safeStringCall((): string => avcGetNameByCategory('video/avc', 1, Number(this.enumCodecHardware)));
this.nameByCategoryResult = s;
} catch (e) {
this.nameByCategoryResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `GetNameByCategory failed: ${e}`));
}
})
}.width('100%').padding(12)
Row() {
Text(`结果: ${this.nameByCategoryResult}`).fontSize(14)
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
}
}
ListItem() {
Column() {
Row() {
Text('获取视频宽度/高度对齐').layoutWeight(1).fontSize(18)
}.width('100%').padding(12)
Row() {
Button('调用宽度')
.onClick(() => {
try {
const ret: number = safeNumberCall((): number => avcGetVideoWidthAlignment('video/avc', 1));
this.widthAlignResult = ret >= 0 ? `${ret}` : '调用失败';
} catch (e) {
this.widthAlignResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `GetVideoWidthAlignment failed: ${e}`));
}
})
}.width('100%').padding({ left: 12, right: 12, bottom: 8 })
Row() {
Button('调用高度')
.onClick(() => {
try {
const ret: number = safeNumberCall((): number => avcGetVideoHeightAlignment('video/avc', 1));
this.heightAlignResult = ret >= 0 ? `${ret}` : '调用失败';
} catch (e) {
this.heightAlignResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `GetVideoHeightAlignment failed: ${e}`));
}
})
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
Row() {
Text(`宽度对齐: ${this.widthAlignResult},高度对齐: ${this.heightAlignResult}`).fontSize(14)
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
}
}
ListItem() {
Column() {
Row() {
Text('指定高获取宽度范围/指定宽获取高度范围').layoutWeight(1).fontSize(18)
}.width('100%').padding(12)
Row() {
Button('高=1080').onClick(() => {
try {
const s: string = safeStringCall((): string => avcGetVideoWidthRangeForHeight('video/avc', 1, 1080));
this.widthRangeForHeightResult = s;
} catch (e) {
this.widthRangeForHeightResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `GetVideoWidthRangeForHeight failed: ${e}`));
}
})
}.width('100%').padding({ left: 12, right: 12, bottom: 8 })
Row() {
Button('宽=1920').onClick(() => {
try {
const s: string = safeStringCall((): string => avcGetVideoHeightRangeForWidth('video/avc', 1, 1920));
this.heightRangeForWidthResult = s;
} catch (e) {
this.heightRangeForWidthResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `GetVideoHeightRangeForWidth failed: ${e}`));
}
})
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
Row() {
Text(`宽范围: ${this.widthRangeForHeightResult},高范围: ${this.heightRangeForWidthResult}`).fontSize(14)
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
}
}
ListItem() {
Column() {
Row() {
Text('指定分辨率获取帧率范围/检查是否支持').layoutWeight(1).fontSize(18)
}.width('100%').padding(12)
Row() {
Button('获取 1920x1080 帧率范围').onClick(() => {
try {
const s: string = safeStringCall((): string => avcGetVideoFrameRateRangeForSize('video/avc', 1, 1920, 1080));
this.frameRateRangeForSizeResult = s;
} catch (e) {
this.frameRateRangeForSizeResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `GetVideoFrameRateRangeForSize failed: ${e}`));
}
})
}.width('100%').padding({ left: 12, right: 12, bottom: 8 })
Row() {
Button('支持 1920x1080@30').onClick(() => {
try {
const ret: number = safeNumberCall((): number => avcAreVideoSizeAndFrameRateSupported('video/avc', 1, 1920, 1080, 30));
this.sizeAndFrameRateSupportedResult = ret === 1 ? '支持' : (ret === 0 ? '不支持' : '调用失败');
} catch (e) {
this.sizeAndFrameRateSupportedResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `AreVideoSizeAndFrameRateSupported failed: ${e}`));
}
})
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
Row() {
Text(`帧率范围: ${this.frameRateRangeForSizeResult},支持性: ${this.sizeAndFrameRateSupportedResult}`).fontSize(14)
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
}
}
ListItem() {
Column() {
Row() {
Text('特性支持与属性(低时延示例)').layoutWeight(1).fontSize(18)
}.width('100%').padding(12)
Row() {
Button('检查支持').onClick(() => {
try {
const ret: number = safeNumberCall((): number => avcIsFeatureSupported('video/avc', 0, Number(this.enumFeatureLowLatency)));
this.featureSupportedResult = ret === 1 ? '支持' : (ret === 0 ? '不支持' : '调用失败');
} catch (e) {
this.featureSupportedResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `IsFeatureSupported failed: ${e}`));
}
})
}.width('100%').padding({ left: 12, right: 12, bottom: 8 })
Row() {
Button('属性Dump').onClick(() => {
try {
const s: string = safeStringCall((): string => avcGetFeaturePropertiesDump('video/avc', 0, Number(this.enumFeatureLowLatency)));
this.featureDumpResult = s;
} catch (e) {
this.featureDumpResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `GetFeaturePropertiesDump failed: ${e}`));
}
})
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
Row() {
Text(`支持性: ${this.featureSupportedResult}`).fontSize(14)
}.width('100%').padding({ left: 12, right: 12 })
Row() {
Text(`属性: ${this.featureDumpResult === 'error' ? '不支持或无属性' : this.featureDumpResult}`).fontSize(14)
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
}
}
ListItem() {
Column() {
Row() {
Text('音频支持的采样率/采样率范围/声道范围(AAC)').layoutWeight(1).fontSize(18)
}.width('100%').padding(12)
Row() {
Button('采样率').onClick(() => {
try {
const s: string = safeStringCall((): string => avcGetAudioSupportedSampleRates('audio/mp4a-latm', 1));
this.audioSampleRatesResult = s;
} catch (e) {
this.audioSampleRatesResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `GetAudioSupportedSampleRates failed: ${e}`));
}
})
}.width('100%').padding({ left: 12, right: 12, bottom: 8 })
Row() {
Button('采样率范围').onClick(() => {
try {
const s: string = safeStringCall((): string => avcGetAudioSupportedSampleRateRanges('audio/mp4a-latm', 1));
this.audioSampleRateRangesResult = s;
} catch (e) {
this.audioSampleRateRangesResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `GetAudioSupportedSampleRateRanges failed: ${e}`));
}
})
}.width('100%').padding({ left: 12, right: 12, bottom: 8 })
Row() {
Button('声道范围').onClick(() => {
try {
const s: string = safeStringCall((): string => avcGetAudioChannelCountRange('audio/mp4a-latm', 1));
this.audioChannelCountRangeResult = s;
} catch (e) {
this.audioChannelCountRangeResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `GetAudioChannelCountRange failed: ${e}`));
}
})
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
Row() {
Text(`采样率: ${this.audioSampleRatesResult}`).fontSize(14)
}.width('100%').padding({ left: 12, right: 12 })
Row() {
Text(`采样率范围: ${this.audioSampleRateRangesResult}`).fontSize(14)
}.width('100%').padding({ left: 12, right: 12 })
Row() {
Text(`声道范围: ${this.audioChannelCountRangeResult}`).fontSize(14)
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
}
}
ListItem() {
Column() {
Row() {
Text('视频像素格式/支持的Profiles/Levels').layoutWeight(1).fontSize(18)
}.width('100%').padding(12)
Row() {
Button('像素格式').onClick(() => {
try {
const s: string = safeStringCall((): string => avcGetVideoSupportedPixelFormats('video/avc', 1));
this.videoPixelFormatsResult = s;
} catch (e) {
this.videoPixelFormatsResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `GetVideoSupportedPixelFormats failed: ${e}`));
}
})
}.width('100%').padding({ left: 12, right: 12, bottom: 8 })
Row() {
Button('Profiles').onClick(() => {
try {
const s: string = safeStringCall((): string => avcGetSupportedProfiles('video/avc', 1));
this.supportedProfilesResult = s;
} catch (e) {
this.supportedProfilesResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `GetSupportedProfiles failed: ${e}`));
}
})
}.width('100%').padding({ left: 12, right: 12, bottom: 8 })
Row() {
Button('Levels(假设profile=1)').onClick(() => {
try {
const s: string = safeStringCall((): string => avcGetSupportedLevelsForProfile('video/avc', 1, 1));
this.supportedLevelsForProfileResult = s;
} catch (e) {
this.supportedLevelsForProfileResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `GetSupportedLevelsForProfile failed: ${e}`));
}
})
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
Row() {
Text(`像素格式: ${this.videoPixelFormatsResult}`).fontSize(14)
}.width('100%').padding({ left: 12, right: 12 })
Row() {
Text(`Profiles: ${this.supportedProfilesResult}`).fontSize(14)
}.width('100%').padding({ left: 12, right: 12 })
Row() {
Text(`Levels: ${this.supportedLevelsForProfileResult === 'error' ? '不支持或参数无效' : this.supportedLevelsForProfileResult}`).fontSize(14)
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
}
}
ListItem() {
Column() {
Row() {
Text('检查Profile+Level组合/编码器比特率模式支持/质量/复杂度范围').layoutWeight(1).fontSize(18)
}.width('100%').padding(12)
Row() {
Button('Profile=1, Level=1 支持?').onClick(() => {
try {
const ret: number = safeNumberCall((): number => avcAreProfileAndLevelSupported('video/avc', 1, 1, 1));
this.profileLevelSupportedResult = ret === 1 ? '支持' : (ret === 0 ? '不支持' : '调用失败');
} catch (e) {
this.profileLevelSupportedResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `AreProfileAndLevelSupported failed: ${e}`));
}
})
}.width('100%').padding({ left: 12, right: 12, bottom: 8 })
Row() {
Button('比特率模式 CBR?').onClick(() => {
try {
const ret: number = safeNumberCall((): number => avcIsEncoderBitrateModeSupported('video/avc', 0));
this.encoderBitrateModeSupportedResult = ret === 1 ? '支持' : (ret === 0 ? '不支持' : '调用失败');
} catch (e) {
this.encoderBitrateModeSupportedResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `IsEncoderBitrateModeSupported failed: ${e}`));
}
})
}.width('100%').padding({ left: 12, right: 12, bottom: 8 })
Row() {
Button('质量范围').onClick(() => {
try {
const s: string = safeStringCall((): string => avcGetEncoderQualityRange('video/avc'));
this.encoderQualityRangeResult = s;
} catch (e) {
this.encoderQualityRangeResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `GetEncoderQualityRange failed: ${e}`));
}
})
}.width('100%').padding({ left: 12, right: 12, bottom: 8 })
Row() {
Button('复杂度范围').onClick(() => {
try {
const s: string = safeStringCall((): string => avcGetEncoderComplexityRange('video/avc'));
this.encoderComplexityRangeResult = s;
} catch (e) {
this.encoderComplexityRangeResult = '错误';
try { promptAction.showToast({ message: '调用失败' }); } catch (_) {}
safeNumberCall((): number => lib.hiLogPrintMsg(1, 5, 0, 'AVCapability', `GetEncoderComplexityRange failed: ${e}`));
}
})
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
Row() {
Text(`Profile+Level: ${this.profileLevelSupportedResult},比特率模式: ${this.encoderBitrateModeSupportedResult}`).fontSize(14)
}.width('100%').padding({ left: 12, right: 12 })
Row() {
Text(`质量范围: ${this.encoderQualityRangeResult},复杂度范围: ${this.encoderComplexityRangeResult}`).fontSize(14)
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
}
}
ListItem() {
Column() {
Row() {
Text('枚举值').layoutWeight(1).fontSize(18)
}.width('100%').padding(12)
Row() {
Text(`AVCodecCategory.HARDWARE = ${this.enumCodecHardware}(${describeAVCapabilityCategoryByName('HARDWARE')})`).layoutWeight(1)
}.width('100%').padding({ left: 12, right: 12, bottom: 4 })
Row() {
Text(`AVCodecCategory.SOFTWARE = ${this.enumCodecSoftware}(${describeAVCapabilityCategoryByName('SOFTWARE')})`).layoutWeight(1)
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
Row() {
Text(`AVCapabilityFeature.VIDEO_ENCODER_TEMPORAL_SCALABILITY = ${this.enumFeatureTemporalScalability}(${describeAVCapabilityFeatureByName('VIDEO_ENCODER_TEMPORAL_SCALABILITY')})`).layoutWeight(1)
}.width('100%').padding({ left: 12, right: 12, bottom: 4 })
Row() {
Text(`AVCapabilityFeature.VIDEO_ENCODER_LONG_TERM_REFERENCE = ${this.enumFeatureLongTermRef}(${describeAVCapabilityFeatureByName('VIDEO_ENCODER_LONG_TERM_REFERENCE')})`).layoutWeight(1)
}.width('100%').padding({ left: 12, right: 12, bottom: 4 })
Row() {
Text(`AVCapabilityFeature.VIDEO_LOW_LATENCY = ${this.enumFeatureLowLatency}(${describeAVCapabilityFeatureByName('VIDEO_LOW_LATENCY')})`).layoutWeight(1)
}.width('100%').padding({ left: 12, right: 12, bottom: 4 })
Row() {
Text(`AVCapabilityFeature.VIDEO_ENCODER_B_FRAME = ${this.enumFeatureBFrame}(${describeAVCapabilityFeatureByName('VIDEO_ENCODER_B_FRAME')})`).layoutWeight(1)
}.width('100%').padding({ left: 12, right: 12, bottom: 12 })
}
}
}
.width('100%')
}
.width('100%')
}
}
@Builder
export function BuildAVCapability() { AVCapabilityView() }
registerComponent('AVCapability', BuildAVCapability)
import type { ComponentBuilder } from './registry';
@Component
export default struct ComponentHost {
@BuilderParam child: ComponentBuilder
build() { this.child() }
}
import { registerComponent } from '../registry';
@Component
export default struct LoadingOverlay {
@Prop visible: boolean = false
@Prop message: string = '加载中'
@State dots: string = ''
private timerId: number = -1
private start() {
if (this.timerId !== -1) { return }
this.dots = ''
this.timerId = Number(setInterval(() => {
this.dots = this.dots.length >= 3 ? '' : this.dots + '.'
}, 400))
}
private stop() {
if (this.timerId !== -1) {
clearInterval(this.timerId)
this.timerId = -1
}
this.dots = ''
}
build() {
if (this.visible) {
Stack() {
Column() {
Text(`${this.message}${this.dots}`).fontSize(18).fontColor('#FFFFFF')
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.justifyContent(FlexAlign.Center)
.zIndex(999)
.onAppear(() => { this.start() })
.onDisAppear(() => { this.stop() })
}
.width('100%')
.height('100%')
}
}
}
@Builder
export function BuildLoadingOverlay(visible: boolean, message: string) {
LoadingOverlay({ visible: visible, message: message })
}
@Builder
export function BuildLoadingOverlayDefault() {
LoadingOverlay()
}
registerComponent('LoadingOverlay', BuildLoadingOverlayDefault)
import './AVCodecKit/AVCapabilityView'
export type ComponentBuilder = () => void
const R: Map<string, ComponentBuilder> = new Map()
export function registerComponent(name: string, builder: ComponentBuilder) { R.set(name, builder) }
export function getComponent(name: string): ComponentBuilder | undefined { return R.get(name) }
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment