feat(playback-lab): add isolated HarmonyOS app shell

This commit is contained in:
BoatingCloud committed 2026-08-29 09:05:42 +08:00
1 parent 75ed9dd2f4
commit 2356e2f67b
22 files changed
+308

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
entry/libs/arm64-v8a/*.so*
entry/build/
build/
oh_modules/
.hvigor/
local.properties
+10
View File
@@ -0,0 +1,10 @@
{
"app": {
"bundleName": "com.velix.playbacklab",
"vendor": "Velix",
"versionCode": 1,
"versionName": "0.1.0",
"icon": "$media:icon",
"label": "$string:app_name"
}
}
+12
View File
@@ -0,0 +1,12 @@
# Velix Playback Lab
独立的 HarmonyOS Native XComponent + OHNativeWindow + EGL + libmpv 播放验证工程。
## 构建
```sh
./scripts/stage-mpv.sh
./hvigorw assembleHap --mode module -p product=default
```
`stage-mpv.sh` 只把本地 Phase 1 产物复制到未跟踪的 HAP native 库目录,不把 34 MB 的二进制提交到仓库。
+30
View File
@@ -0,0 +1,30 @@
{
"app": {
"signingConfigs": [],
"products": [
{
"name": "default",
"signingConfig": "default",
"targetSdkVersion": "6.1.1(24)",
"compatibleSdkVersion": "6.1.1(24)",
"runtimeOS": "HarmonyOS"
}
],
"buildModeSet": [
{ "name": "debug" },
{ "name": "release" }
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": ["default"]
}
]
}
]
}
+5
View File
@@ -0,0 +1,5 @@
{
"files": {
"ignore": ["**/entry/libs/**"]
}
}
+3
View File
@@ -0,0 +1,3 @@
build/
.cxx/
libs/arm64-v8a/*.so*
+14
View File
@@ -0,0 +1,14 @@
{
"apiType": "stageMode",
"buildOption": {
"externalNativeOptions": {
"path": "./src/main/cpp/CMakeLists.txt",
"arguments": "",
"cppFlags": "-std=c++17 -fvisibility=hidden",
"abiFilters": ["arm64-v8a"]
}
},
"targets": [
{ "name": "default" }
]
}
+6
View File
@@ -0,0 +1,6 @@
import { hapTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: hapTasks,
plugins: []
};
+1
View File
@@ -0,0 +1 @@
# Playback Lab keeps native bridge names stable for NAPI lookup.
+6
View File
@@ -0,0 +1,6 @@
{
"modelVersion": "6.1.1",
"name": "entry",
"version": "1.0.0",
"description": "Playback Lab entry module"
}
@@ -0,0 +1,8 @@
import { UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index');
}
}
@@ -0,0 +1,85 @@
import playbackLab from 'libentry.so';
interface PlaybackState {
state: string;
position: number;
duration: number;
fps: number;
dropped: number;
firstFrameMs: number;
renderCount: number;
lastError: string;
surface: string;
egl: string;
mpv: string;
}
const LOCAL_TEST_URL: string = 'file:///data/storage/el2/base/haps/entry/files/sample.mp4';
@Entry
@Component
struct Index {
@State url: string = LOCAL_TEST_URL;
@State status: string = 'IDLE';
@State detail: string = 'Surface waiting';
@State state: PlaybackState = {
state: 'IDLE', position: 0, duration: 0, fps: 0, dropped: 0,
firstFrameMs: -1, renderCount: 0, lastError: '', surface: 'WAITING', egl: 'WAITING', mpv: 'WAITING'
};
private poller: number = 0;
aboutToAppear(): void {
this.poller = setInterval(() => {
this.state = playbackLab.getState() as PlaybackState;
this.status = this.state.state;
this.detail = this.state.lastError || `position ${this.state.position.toFixed(2)} / ${this.state.duration.toFixed(2)}s`;
}, 250);
}
aboutToDisappear(): void {
if (this.poller !== 0) {
clearInterval(this.poller);
this.poller = 0;
}
}
build() {
Column({ space: 10 }) {
Text('Velix Playback Lab').fontSize(24).fontWeight(FontWeight.Bold).width('100%')
Text(`state: ${this.status}`).fontSize(16).fontColor($r('app.color.primary')).width('100%')
Text(`surface=${this.state.surface} egl=${this.state.egl} mpv=${this.state.mpv}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%')
XComponent({ id: 'playback_surface', type: XComponentType.SURFACE, libraryname: 'entry' })
.width('100%').height(250).backgroundColor(Color.Black)
TextInput({ text: this.url, placeholder: 'file:///... or https://...' })
.width('100%').onChange((value: string) => this.url = value)
Row({ space: 8 }) {
Button('Load Local Test').onClick(() => {
this.url = LOCAL_TEST_URL;
playbackLab.load(LOCAL_TEST_URL);
})
Button('Load URL').onClick(() => playbackLab.load(this.url))
}.width('100%')
Row({ space: 8 }) {
Button('Play').onClick(() => playbackLab.play())
Button('Pause').onClick(() => playbackLab.pause())
Button('Stop').onClick(() => playbackLab.stop())
Button('Release').onClick(() => playbackLab.release())
}.width('100%')
Row({ space: 8 }) {
Button('-10s').onClick(() => playbackLab.seek(-10))
Button('+10s').onClick(() => playbackLab.seek(10))
}.width('100%')
Text(`position=${this.state.position.toFixed(2)} duration=${this.state.duration.toFixed(2)} fps=${this.state.fps.toFixed(2)}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%')
Text(`dropped=${this.state.dropped} firstFrameMs=${this.state.firstFrameMs} renderCount=${this.state.renderCount}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%')
Text(detailLabel(this.detail)).fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
}
.padding(16).width('100%').height('100%').backgroundColor($r('app.color.background'))
}
}
function detailLabel(value: string): string {
return `detail: ${value}`;
}
+35
View File
@@ -0,0 +1,35 @@
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": ["phone", "tablet"],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:ability_desc",
"icon": "$media:icon",
"label": "$string:app_name",
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["ohos.want.action.home"]
}
]
}
],
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
}
}
@@ -0,0 +1,9 @@
{
"color": [
{ "name": "background", "value": "#101318" },
{ "name": "start_window_background", "value": "#101318" },
{ "name": "panel", "value": "#1A202A" },
{ "name": "primary", "value": "#6EA8FE" },
{ "name": "muted", "value": "#A9B4C2" }
]
}
@@ -0,0 +1,8 @@
{
"string": [
{ "name": "app_name", "value": "Velix Playback Lab" },
{ "name": "module_desc", "value": "Native libmpv playback verification" },
{ "name": "ability_desc", "value": "Playback Lab main ability" },
{ "name": "status_idle", "value": "IDLE" }
]
}
@@ -0,0 +1,4 @@
<svg width="96" height="96" xmlns="http://www.w3.org/2000/svg">
<rect width="96" height="96" rx="20" fill="#101318"/>
<path d="M38 27v42l32-21z" fill="#6EA8FE"/>
</svg>

After

Width:  |  Height:  |  Size: 173 B

@@ -0,0 +1,3 @@
{
"src": ["pages/Index"]
}
+4
View File
@@ -0,0 +1,4 @@
{
"modelVersion": "6.1.1",
"dependencies": {}
}
+6
View File
@@ -0,0 +1,6 @@
import { appTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: appTasks,
plugins: []
};
+28
View File
@@ -0,0 +1,28 @@
{
"meta": {
"stableOrder": true,
"enableUnifiedLockfile": false
},
"lockfileVersion": 3,
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
"specifiers": {
"@ohos/hamock@1.0.0": "@ohos/hamock@1.0.0",
"@ohos/hypium@1.0.25": "@ohos/hypium@1.0.25"
},
"packages": {
"@ohos/hamock@1.0.0": {
"name": "@ohos/hamock",
"version": "1.0.0",
"integrity": "sha512-K6lDPYc6VkKe6ZBNQa9aoG+ZZMiwqfcR/7yAVFSUGIuOAhPvCJAo9+t1fZnpe0dBRBPxj2bxPPbKh69VuyAtDg==",
"resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hamock/-/hamock-1.0.0.har",
"registryType": "ohpm"
},
"@ohos/hypium@1.0.25": {
"name": "@ohos/hypium",
"version": "1.0.25",
"integrity": "sha512-l6uO2pjl8HyEKdekLqQt7tUpWbDqX/42zoAzkagtUVZAW9jT6lMvbe54MVjoLxq/RwQGygRvi6j4GpypSMFSHw==",
"resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hypium/-/hypium-1.0.25.har",
"registryType": "ohpm"
}
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"modelVersion": "6.1.1",
"description": "Velix Playback Lab",
"dependencies": {},
"devDependencies": {
"@ohos/hypium": "1.0.25",
"@ohos/hamock": "1.0.0"
}
}
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
set -eu
repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
source_lib="$repo_root/build/ohos-arm64/prefix/lib/libmpv.so.2.5.0"
target_dir="$repo_root/playback_lab/entry/libs/arm64-v8a"
target_lib="$target_dir/libmpv.so.2"
if [ ! -f "$source_lib" ]; then
echo "missing Phase 1 libmpv: $source_lib" >&2
exit 1
fi
mkdir -p "$target_dir"
cp -f "$source_lib" "$target_lib"
echo "staged $target_lib ($(stat -f %z "$target_lib") bytes)"