diff --git a/.gitignore b/.gitignore index 8110f7d18f..bf51adf871 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ !/subprojects/packagefiles !/subprojects/*.wrap /build/ +/dist/ .hvigor/ .DS_Store diff --git a/DOCS/CORE_API_VERSIONING.md b/DOCS/CORE_API_VERSIONING.md new file mode 100644 index 0000000000..f7bbae3616 --- /dev/null +++ b/DOCS/CORE_API_VERSIONING.md @@ -0,0 +1,11 @@ +# Core API versioning + +当前冻结版本:`VELIX_CORE_API_VERSION = 0x000100`(v0)。 + +- `MAJOR` 变化表示 ABI/API 不兼容,必须升级主 App 适配层。 +- `MINOR` 变化只增加尾部字段或能力,旧调用方保持可用。 +- `PATCH` 变化只修复实现,不改变公开结构布局。 + +版本宏、`velix_core_get_api_version()`、`velix_core_get_version()` 和 `velix_core_get_build_info()` 位于 `include/velix/core.h`,C ABI 使用 opaque handle 与固定宽度整数。变更 API 时同步更新 `metadata/build-info.json`、此文档与集成文档,并在发布前检查动态符号。 + +v0 不把 `mpv_handle`、`mpv_node`、FFmpeg、OHCodec、EGL、OHAudio 或 OHNativeWindow 类型带出 SDK。 diff --git a/DOCS/CORE_SDK.md b/DOCS/CORE_SDK.md new file mode 100644 index 0000000000..cd62108e10 --- /dev/null +++ b/DOCS/CORE_SDK.md @@ -0,0 +1,27 @@ +# Velix Core SDK + +Phase 7 SDK v0 以 `include/velix/core.h` 与 `libvelix_core_binding.so` 为产品边界,主 App 不编译 MPV/FFmpeg 源码。 + +## 目录 + +```text +velix-core/ +├── include/velix/core.h +├── include/mpv/harmony_playback.h +├── lib/arm64-v8a/libvelix_core_binding.so +├── lib/arm64-v8a/libmpv.so.2 +├── lib/arm64-v8a/libc++_shared.so +├── metadata/build-info.json +└── licenses/ +``` + +`libvelix_core_binding.so` 仅提供产品生命周期、控制、状态、媒体、音轨、字幕、指标与 build-info NAPI;XComponent 的 OHNativeWindow/EGL/mpv render context 仍由 Core 内部实现。主 App 只能看到 `PlaybackEngine`/`PlaybackController` 的业务类型。 + +默认 profile 为 software decode,OHCodec GL mapper 保持关闭。SDK 产物位于 `dist/velix-core`,已在 `.gitignore` 中忽略,禁止把 34MB `libmpv` 二进制提交到 Git。 + +构建与 staging: + +```sh +./scripts/package-velix-core-sdk.sh --profile default +../Velix/scripts/build-velix-app.sh --release-core --build-mode debug +``` diff --git a/DOCS/PLAYBACK_ENGINE.md b/DOCS/PLAYBACK_ENGINE.md new file mode 100644 index 0000000000..69851d8f77 --- /dev/null +++ b/DOCS/PLAYBACK_ENGINE.md @@ -0,0 +1,13 @@ +# PlaybackEngine + +`PlaybackEngine` 是主 App 的唯一播放抽象,`MpvPlaybackEngine` 是 Core binding 的适配器,`PlaybackController` 负责页面状态刷新与生命周期编排。 + +业务层只使用: + +- `PlaybackSource { kind, url, headers }` +- `PlaybackState { state, position, duration, media, tracks, metrics }` +- play/pause/stop/release、seek、音量/倍速、音轨/字幕操作 + +provider 接口暂不绑定 Emby、Jellyfin、百度网盘等服务。HTTP(S) 源支持有限 headers;复杂 header 编辑器、登录与播放列表不在 v0 范围。 + +默认解码策略为软件解码;`requestedDecoder=software` 是主 App 的固定默认值,OHCodec GL mapper 不在本阶段启用。 diff --git a/DOCS/PLAYER_LIFECYCLE.md b/DOCS/PLAYER_LIFECYCLE.md new file mode 100644 index 0000000000..27df9674b9 --- /dev/null +++ b/DOCS/PLAYER_LIFECYCLE.md @@ -0,0 +1,14 @@ +# Player lifecycle + +产品调用顺序: + +```text +create → attachSurface → prepare(source) → play + ↘ pause / seekTo / seekBy / tracks / subtitles +stop → prepare 或 release +detachSurface → release +``` + +`onForeground`、`onBackground`、`onSurfaceAvailable`、`onSurfaceDestroyed` 是显式适配接口。当前后台策略只释放/暂停 Surface 相关资源,不承诺后台播放、PiP 或通知栏控制。Surface 与 player 生命周期解耦:Surface 销毁不会自动销毁 Core player,新的 Surface 可重新 attach。 + +页面状态以 500ms 轮询读取,错误通过 `errorCode` 与 `lastError` 映射到 UI;首帧、缓冲、seek、音频与 renderer 指标仅在“显示调试统计”打开时显示。 diff --git a/DOCS/SDK_PACKAGING.md b/DOCS/SDK_PACKAGING.md new file mode 100644 index 0000000000..a041951f7f --- /dev/null +++ b/DOCS/SDK_PACKAGING.md @@ -0,0 +1,20 @@ +# SDK packaging + +使用 `scripts/package-velix-core-sdk.sh` 生成 staging 目录。脚本按 profile 选择已构建的 `libmpv`,编译 binding,复制公开 headers、运行库、许可证,并生成包含 API 版本、commit、profile、尺寸和 SHA-256 的 `metadata/build-info.json`。 + +支持 profile: + +```sh +./scripts/package-velix-core-sdk.sh --profile default +./scripts/package-velix-core-sdk.sh --profile software +./scripts/package-velix-core-sdk.sh --profile ffmpeg-harmony +``` + +ABI 检查重点: + +- `libvelix_core_binding.so` 只导出 3 个 `velix_core_get_*` ABI 符号;NAPI 模块描述由系统注册段加载,不把内部 C++ 符号作为 ABI; +- `libmpv.so.2` 保持自身运行时导出,不将其头文件类型传给主 App; +- 所有动态依赖必须存在于 `lib/arm64-v8a` 或 OpenHarmony 系统库中; +- staging 与构建目录不应进入 Git,发布包必须保留 LGPL/GPL/第三方声明。 + +验证:`./scripts/verify-velix-core-sdk.sh`。 diff --git a/DOCS/VELIX_APP_INTEGRATION.md b/DOCS/VELIX_APP_INTEGRATION.md new file mode 100644 index 0000000000..a34d23b1a5 --- /dev/null +++ b/DOCS/VELIX_APP_INTEGRATION.md @@ -0,0 +1,10 @@ +# Velix 主 App 集成 + +主 App 工程为 `../Velix`,当前是独立的 Stage Model/API 24 工程,目录本身没有 Git 元数据。集成脚本提供两种依赖模式: + +- `scripts/sync-velix-core-sdk.sh --local`:从相邻 Core 重新构建并 staging,适合本地开发。 +- `scripts/sync-velix-core-sdk.sh --release --sdk PATH`:消费已经发布的 SDK 目录,适合 release 验证。 + +`scripts/build-velix-app.sh` 会先同步 SDK,再执行 `devecocli build`。主 App 只将 `libvelix_core_binding.so`、`libmpv.so.2` 与 `libc++_shared.so` 放进 `entry/libs/arm64-v8a`,不引入 Core/FFmpeg 源码。 + +ArkTS 入口为 `domain/playback/PlaybackEngine.ets` → `MpvPlaybackEngine.ets` → native binding;页面为 `pages/PlayerPage.ets`。XComponent 的 `libraryname` 为 `velix_core_binding`,Surface 回调由 Core binding 注册,页面不重复实现 EGL renderer。 diff --git a/include/velix/core.h b/include/velix/core.h new file mode 100644 index 0000000000..4866c2fa88 --- /dev/null +++ b/include/velix/core.h @@ -0,0 +1,37 @@ +#pragma once + +#include + +#if defined(__GNUC__) || defined(__clang__) +#define VELIX_CORE_API __attribute__((visibility("default"))) +#else +#define VELIX_CORE_API +#endif + +#define VELIX_CORE_API_VERSION 0x000100u + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct velix_core_build_info { + uint32_t api_version; + const char* sdk_version; + const char* mpv_version; + const char* mpv_commit; + const char* ffmpeg_commit; + const char* ffmpeg_harmony_commit; + const char* build_date; + const char* target; + const char* architecture; + const char* profile; + const char* feature_flags; +} velix_core_build_info; + +VELIX_CORE_API uint32_t velix_core_get_api_version(void); +VELIX_CORE_API const char* velix_core_get_version(void); +VELIX_CORE_API const velix_core_build_info* velix_core_get_build_info(void); + +#ifdef __cplusplus +} +#endif diff --git a/playback_lab/scripts/stage-mpv.sh b/playback_lab/scripts/stage-mpv.sh index 7465030e3f..be7c31096a 100755 --- a/playback_lab/scripts/stage-mpv.sh +++ b/playback_lab/scripts/stage-mpv.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) +repo_root=$(CDPATH= cd -- "${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" @@ -11,6 +11,6 @@ if [ ! -f "$source_lib" ]; then exit 1 fi -mkdir -p "$target_dir" -cp -f "$source_lib" "$target_lib" -echo "staged $target_lib ($(stat -f %z "$target_lib") bytes)" +/bin/mkdir -p "$target_dir" +/bin/cp -f "$source_lib" "$target_lib" +echo "staged $target_lib ($(/usr/bin/stat -f %z "$target_lib") bytes)" diff --git a/scripts/package-velix-core-sdk.sh b/scripts/package-velix-core-sdk.sh new file mode 100755 index 0000000000..3f26d1e2fb --- /dev/null +++ b/scripts/package-velix-core-sdk.sh @@ -0,0 +1,106 @@ +#!/bin/sh + +set -eu + +PROJECT_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +PROFILE=default +OUTPUT_DIR=${VELIX_CORE_SDK_OUT:-$PROJECT_ROOT/dist/velix-core} + +while test "$#" -gt 0; do + case "$1" in + --profile) + test "$#" -ge 2 || { echo "missing --profile value" >&2; exit 2; } + PROFILE=$2 + shift 2 + ;; + --out) + test "$#" -ge 2 || { echo "missing --out value" >&2; exit 2; } + OUTPUT_DIR=$2 + shift 2 + ;; + *) + echo "usage: $0 [--profile default|software|ffmpeg-harmony] [--out DIR]" >&2 + exit 2 + ;; + esac +done + +case "$PROFILE" in + default) + PROFILE_NAME=default-software + MPV_LIBRARY="$PROJECT_ROOT/build/ohos-arm64/prefix/lib/libmpv.so.2.5.0" + BUILD_SCRIPT="$PROJECT_ROOT/scripts/build-mpv-ohos.sh" + ;; + software) + PROFILE_NAME=software + MPV_LIBRARY="$PROJECT_ROOT/build/ohos-arm64/build/mpv-software/libmpv.so.2.5.0" + BUILD_SCRIPT="$PROJECT_ROOT/scripts/build-mpv-ohos-software.sh" + ;; + ffmpeg-harmony) + PROFILE_NAME=ffmpeg-harmony + MPV_LIBRARY="$PROJECT_ROOT/build/ohos-arm64/build/mpv-ffmpeg-harmony/libmpv.so.2.5.0" + BUILD_SCRIPT="$PROJECT_ROOT/scripts/build-mpv-ohos-ffmpeg-harmony.sh" + ;; + *) + echo "unsupported profile: $PROFILE" >&2 + exit 2 + ;; +esac + +if test ! -f "$MPV_LIBRARY"; then + "$BUILD_SCRIPT" +fi +test -f "$MPV_LIBRARY" || { echo "missing libmpv: $MPV_LIBRARY" >&2; exit 1; } + +. "$PROJECT_ROOT/scripts/setup-ohos-env.sh" +SDK_BUILD="$OHOS_BUILD_ROOT/build/velix-core-sdk-${PROFILE}" +cmake -S "$PROJECT_ROOT/sdk/native" -B "$SDK_BUILD" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_COMPILER="$CC" -DCMAKE_CXX_COMPILER="$CXX" \ + -DCMAKE_AR="$AR" -DCMAKE_RANLIB="$RANLIB" -DCMAKE_STRIP="$STRIP" \ + -DCMAKE_SYSTEM_NAME=OHOS -DCMAKE_SYSTEM_PROCESSOR=aarch64 \ + -DCMAKE_C_COMPILER_TARGET="$TARGET" -DCMAKE_CXX_COMPILER_TARGET="$TARGET" \ + -DCMAKE_SYSROOT="$OHOS_SYSROOT" -DCMAKE_C_FLAGS="$OHOS_COMMON_FLAGS" \ + -DCMAKE_CXX_FLAGS="$OHOS_COMMON_FLAGS" \ + -DMPV_LIBRARY="$MPV_LIBRARY" -DVELIX_CORE_PROFILE_NAME="$PROFILE_NAME" +cmake --build "$SDK_BUILD" -j2 + +rm -rf "$OUTPUT_DIR" +mkdir -p "$OUTPUT_DIR/include/velix" "$OUTPUT_DIR/include/mpv" "$OUTPUT_DIR/lib/arm64-v8a" "$OUTPUT_DIR/metadata" "$OUTPUT_DIR/licenses" +cp "$PROJECT_ROOT/include/velix/core.h" "$OUTPUT_DIR/include/velix/core.h" +cp "$PROJECT_ROOT/include/mpv/harmony_playback.h" "$OUTPUT_DIR/include/mpv/harmony_playback.h" +cp "$SDK_BUILD/libvelix_core_binding.so" "$OUTPUT_DIR/lib/arm64-v8a/libvelix_core_binding.so" +cp "$MPV_LIBRARY" "$OUTPUT_DIR/lib/arm64-v8a/libmpv.so.2" + +RUNTIME_LIB=$(find "$PROJECT_ROOT/playback_lab/entry/build" -type f -name 'libc++_shared.so' -print -quit 2>/dev/null || true) +if test -n "$RUNTIME_LIB"; then + cp "$RUNTIME_LIB" "$OUTPUT_DIR/lib/arm64-v8a/libc++_shared.so" +fi + +cp "$PROJECT_ROOT/LICENSE.LGPL" "$OUTPUT_DIR/licenses/LICENSE.LGPL" +cp "$PROJECT_ROOT/LICENSE.GPL" "$OUTPUT_DIR/licenses/LICENSE.GPL" +cp "$PROJECT_ROOT/Copyright" "$OUTPUT_DIR/licenses/Copyright" +cp "$PROJECT_ROOT/THIRD_PARTY_NOTICES.md" "$OUTPUT_DIR/licenses/THIRD_PARTY_NOTICES.md" + +binding_size=$(stat -f %z "$OUTPUT_DIR/lib/arm64-v8a/libvelix_core_binding.so") +mpv_size=$(stat -f %z "$OUTPUT_DIR/lib/arm64-v8a/libmpv.so.2") +binding_sha=$(shasum -a 256 "$OUTPUT_DIR/lib/arm64-v8a/libvelix_core_binding.so" | awk '{print $1}') +mpv_sha=$(shasum -a 256 "$OUTPUT_DIR/lib/arm64-v8a/libmpv.so.2" | awk '{print $1}') +cat > "$OUTPUT_DIR/metadata/build-info.json" <&2; exit 1; } +done + +. "$PROJECT_ROOT/scripts/setup-ohos-env.sh" +"$PROJECT_ROOT/scripts/verify-ohos-binary.sh" "$BIN_DIR/libvelix_core_binding.so" "$BIN_DIR/libmpv.so.2" >/dev/null +symbols=$($NM -D --defined-only "$BIN_DIR/libvelix_core_binding.so") +for symbol in velix_core_get_api_version velix_core_get_version velix_core_get_build_info; do + printf '%s\n' "$symbols" | /usr/bin/grep -q " $symbol$" || { echo "missing exported symbol: $symbol" >&2; exit 1; } +done +public_count=$(printf '%s\n' "$symbols" | /usr/bin/awk '$3 !~ /^(_fini|_init)$/ {count++} END {print count + 0}') +test "$public_count" -eq 3 || { echo "unexpected Velix public symbol count: $public_count" >&2; exit 1; } +printf 'SDK verified: %s (Velix ABI exports=%s)\n' "$SDK_ROOT" "$public_count" diff --git a/sdk/native/CMakeLists.txt b/sdk/native/CMakeLists.txt new file mode 100644 index 0000000000..3e3a639027 --- /dev/null +++ b/sdk/native/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.5.0) +project(velix_core_sdk) + +set(CORE_ROOT "${CMAKE_CURRENT_LIST_DIR}/../..") +set(MPV_LIBRARY "${CORE_ROOT}/build/ohos-arm64/prefix/lib/libmpv.so.2.5.0" CACHE FILEPATH "Core libmpv path") +set(VELIX_CORE_PROFILE_NAME "default-software" CACHE STRING "SDK build profile") +if(NOT EXISTS "${MPV_LIBRARY}") + message(FATAL_ERROR "Missing ${MPV_LIBRARY}; build Core before packaging the SDK") +endif() + +set(PLAYER_ROOT "${CORE_ROOT}/playback_lab/entry/src/main/cpp") +include_directories( + "${CORE_ROOT}/include" + "${CORE_ROOT}" + "${PLAYER_ROOT}" + "${CORE_ROOT}/build/ohos-arm64/prefix/include" +) + +add_library(mpv SHARED IMPORTED GLOBAL) +set_target_properties(mpv PROPERTIES IMPORTED_LOCATION "${MPV_LIBRARY}") + +add_library(velix_core_binding SHARED + product_binding.cpp + core_build_info.cpp + "${PLAYER_ROOT}/player.cpp" + "${PLAYER_ROOT}/render.cpp" +) +target_compile_features(velix_core_binding PRIVATE cxx_std_17) +target_compile_options(velix_core_binding PRIVATE -fvisibility=hidden -Wall -Wextra -Werror=return-type) +target_compile_definitions(velix_core_binding PRIVATE VELIX_CORE_PROFILE_NAME="${VELIX_CORE_PROFILE_NAME}") +target_link_libraries(velix_core_binding PUBLIC + mpv + libace_napi.z.so + libace_ndk.z.so + libEGL.so + libGLESv3.so + libnative_window.so + libhilog_ndk.z.so +) diff --git a/sdk/native/core_build_info.cpp b/sdk/native/core_build_info.cpp new file mode 100644 index 0000000000..efdef23efd --- /dev/null +++ b/sdk/native/core_build_info.cpp @@ -0,0 +1,46 @@ +#include "velix/core.h" + +namespace { +constexpr char kSdkVersion[] = "7.0.0"; +constexpr char kMpvVersion[] = "0.41.0-UNKNOWN"; +constexpr char kMpvCommit[] = "e594f66c35d5eb51ecb44895ceacd1bffa469db2"; +constexpr char kFfmpegCommit[] = "f3a036c695bcde37407b48a84f3d75e827a792a7"; +constexpr char kFfmpegHarmonyCommit[] = "f3a036c695bcde37407b48a84f3d75e827a792a7"; +constexpr char kBuildDate[] = "2026-08-29"; +constexpr char kTarget[] = "OpenHarmony API 24"; +constexpr char kArchitecture[] = "arm64-v8a"; +#ifndef VELIX_CORE_PROFILE_NAME +#define VELIX_CORE_PROFILE_NAME "default-software" +#endif +constexpr char kProfile[] = VELIX_CORE_PROFILE_NAME; +constexpr char kFeatures[] = "software-decode,opengl,ohos-audio,http,subtitle"; + +constexpr velix_core_build_info kBuildInfo = { + VELIX_CORE_API_VERSION, + kSdkVersion, + kMpvVersion, + kMpvCommit, + kFfmpegCommit, + kFfmpegHarmonyCommit, + kBuildDate, + kTarget, + kArchitecture, + kProfile, + kFeatures, +}; +} + +extern "C" VELIX_CORE_API uint32_t velix_core_get_api_version(void) +{ + return VELIX_CORE_API_VERSION; +} + +extern "C" VELIX_CORE_API const char* velix_core_get_version(void) +{ + return kSdkVersion; +} + +extern "C" VELIX_CORE_API const velix_core_build_info* velix_core_get_build_info(void) +{ + return &kBuildInfo; +} diff --git a/sdk/native/product_binding.cpp b/sdk/native/product_binding.cpp new file mode 100644 index 0000000000..86ed1a6027 --- /dev/null +++ b/sdk/native/product_binding.cpp @@ -0,0 +1,579 @@ +#include "player.h" +#include "render.h" +#include "velix/core.h" + +#include +#include + +#include +#include +#include + +namespace { + +PlaybackEngine gPlayer; +EglRenderer gRenderer(gPlayer); + +napi_value boolean(napi_env env, bool value) +{ + napi_value result = nullptr; + napi_get_boolean(env, value, &result); + return result; +} + +void setString(napi_env env, napi_value object, const char* name, const std::string& value) +{ + napi_value item = nullptr; + napi_create_string_utf8(env, value.c_str(), NAPI_AUTO_LENGTH, &item); + napi_set_named_property(env, object, name, item); +} + +void setString(napi_env env, napi_value object, const char* name, const char* value) +{ + napi_value item = nullptr; + napi_create_string_utf8(env, value == nullptr ? "" : value, NAPI_AUTO_LENGTH, &item); + napi_set_named_property(env, object, name, item); +} + +void setDouble(napi_env env, napi_value object, const char* name, double value) +{ + napi_value item = nullptr; + napi_create_double(env, value, &item); + napi_set_named_property(env, object, name, item); +} + +void setInt64(napi_env env, napi_value object, const char* name, int64_t value) +{ + napi_value item = nullptr; + napi_create_int64(env, value, &item); + napi_set_named_property(env, object, name, item); +} + +void setBool(napi_env env, napi_value object, const char* name, bool value) +{ + napi_value item = nullptr; + napi_get_boolean(env, value, &item); + napi_set_named_property(env, object, name, item); +} + +bool firstArg(napi_env env, napi_callback_info info, napi_value& value) +{ + size_t argc = 1; + napi_value argv[1] = {nullptr}; + if (napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr) != napi_ok || argc == 0) return false; + value = argv[0]; + return true; +} + +bool valueString(napi_env env, napi_value value, std::string& result) +{ + size_t size = 0; + if (napi_get_value_string_utf8(env, value, nullptr, 0, &size) != napi_ok) return false; + result.resize(size + 1); + if (napi_get_value_string_utf8(env, value, result.data(), result.size(), &size) != napi_ok) return false; + result.resize(size); + return true; +} + +bool namedString(napi_env env, napi_value object, const char* name, std::string& result) +{ + bool has = false; + napi_value value = nullptr; + return napi_has_named_property(env, object, name, &has) == napi_ok && has && + napi_get_named_property(env, object, name, &value) == napi_ok && valueString(env, value, result); +} + +bool namedDouble(napi_env env, napi_value object, const char* name, double& result) +{ + bool has = false; + napi_value value = nullptr; + return napi_has_named_property(env, object, name, &has) == napi_ok && has && + napi_get_named_property(env, object, name, &value) == napi_ok && + napi_get_value_double(env, value, &result) == napi_ok; +} + +bool namedInt(napi_env env, napi_value object, const char* name, int64_t& result) +{ + double number = 0.0; + if (!namedDouble(env, object, name, number)) return false; + result = static_cast(number); + return true; +} + +bool namedBool(napi_env env, napi_value object, const char* name, bool& result) +{ + bool has = false; + napi_value value = nullptr; + return napi_has_named_property(env, object, name, &has) == napi_ok && has && + napi_get_named_property(env, object, name, &value) == napi_ok && + napi_get_value_bool(env, value, &result) == napi_ok; +} + +bool requestFromArg(napi_env env, napi_value argument, PlaybackRequestModel& request) +{ + napi_valuetype type = napi_undefined; + if (napi_typeof(env, argument, &type) != napi_ok) return false; + if (type == napi_string) return valueString(env, argument, request.url) && !request.url.empty(); + if (type != napi_object || !namedString(env, argument, "url", request.url) || request.url.empty()) return false; + namedString(env, argument, "title", request.title); + namedString(env, argument, "userAgent", request.userAgent); + namedString(env, argument, "referer", request.referer); + namedString(env, argument, "cookies", request.cookies); + int64_t number = -1; + if (namedInt(env, argument, "startPositionMs", number)) request.startPositionMs = number; + if (namedInt(env, argument, "audioTrackId", number)) request.audioTrackId = static_cast(number); + if (namedInt(env, argument, "subtitleTrackId", number)) request.subtitleTrackId = static_cast(number); + double decimal = 0.0; + if (namedDouble(env, argument, "playbackSpeed", decimal)) request.playbackSpeed = decimal; + if (namedDouble(env, argument, "networkTimeout", decimal)) request.networkTimeout = decimal; + bool enabled = true; + if (namedBool(env, argument, "subtitleEnabled", enabled)) request.subtitleEnabled = enabled ? 1 : 0; + + napi_value headers = nullptr; + bool has = false; + if (napi_has_named_property(env, argument, "httpHeaders", &has) == napi_ok && has && + napi_get_named_property(env, argument, "httpHeaders", &headers) == napi_ok) { + uint32_t length = 0; + if (napi_get_array_length(env, headers, &length) == napi_ok) { + for (uint32_t index = 0; index < length && index < 32; ++index) { + napi_value item = nullptr; + PlaybackHeader header; + if (napi_get_element(env, headers, index, &item) == napi_ok && + namedString(env, item, "name", header.name) && namedString(env, item, "value", header.value)) { + request.headers.push_back(std::move(header)); + } + } + } + } + return true; +} + +void serializeTrack(napi_env env, napi_value object, const PlaybackTrack& track) +{ + setInt64(env, object, "id", track.id); + setString(env, object, "type", track.type); + setString(env, object, "title", track.title); + setString(env, object, "language", track.language); + setString(env, object, "codec", track.codec); + setBool(env, object, "default", track.isDefault); + setBool(env, object, "forced", track.forced); + setBool(env, object, "selected", track.selected); + setBool(env, object, "external", track.external); +} + +napi_value tracksValue(napi_env env, const std::vector& tracks) +{ + napi_value result = nullptr; + napi_create_array_with_length(env, tracks.size(), &result); + for (size_t index = 0; index < tracks.size(); ++index) { + napi_value item = nullptr; + napi_create_object(env, &item); + serializeTrack(env, item, tracks[index]); + napi_set_element(env, result, index, item); + } + return result; +} + +napi_value mediaValue(napi_env env, const PlaybackMediaInfo& media) +{ + napi_value result = nullptr; + napi_create_object(env, &result); + setString(env, result, "container", media.container); + setString(env, result, "demuxer", media.demuxer); + setDouble(env, result, "duration", media.duration); + setString(env, result, "videoCodec", media.videoCodec); + setString(env, result, "audioCodec", media.audioCodec); + setString(env, result, "pixelFormat", media.pixelFormat); + setString(env, result, "colorPrimaries", media.colorPrimaries); + setString(env, result, "colorTransfer", media.colorTransfer); + setString(env, result, "colorSpace", media.colorSpace); + setString(env, result, "colorMatrix", media.colorMatrix); + setInt64(env, result, "width", media.width); + setInt64(env, result, "height", media.height); + setInt64(env, result, "videoWidth", media.videoWidth); + setInt64(env, result, "videoHeight", media.videoHeight); + setInt64(env, result, "bitDepth", media.bitDepth); + setInt64(env, result, "audioSampleRate", media.audioSampleRate); + setInt64(env, result, "audioChannels", media.audioChannels); + setString(env, result, "audioChannelLayout", media.audioChannelLayout); + setDouble(env, result, "fps", media.fps); + setInt64(env, result, "bitrate", media.bitrate); + setInt64(env, result, "videoBitrate", media.videoBitrate); + setInt64(env, result, "audioBitrate", media.audioBitrate); + setInt64(env, result, "trackCount", media.trackCount); + setInt64(env, result, "subtitleCount", media.subtitleCount); + return result; +} + +napi_value metricsValue(napi_env env, const PlaybackSnapshot& state) +{ + napi_value result = nullptr; + napi_create_object(env, &result); + setInt64(env, result, "droppedFrames", state.dropped); + setInt64(env, result, "renderCount", static_cast(state.renderCount)); + setBool(env, result, "buffering", state.network.buffering); + setInt64(env, result, "bufferingEnterCount", state.metrics.buffering_enter_count); + setInt64(env, result, "bufferingExitCount", state.metrics.buffering_exit_count); + setInt64(env, result, "bufferingTotalMs", state.metrics.buffering_total_ms); + setInt64(env, result, "bufferingMaxSingleMs", state.metrics.buffering_max_single_ms); + setInt64(env, result, "networkWaitMs", state.metrics.network_wait_ms); + setInt64(env, result, "lateFrames", state.metrics.late_frames); + setInt64(env, result, "mistimedFrames", state.metrics.mistimed_frames); + setInt64(env, result, "decoderFrames", state.metrics.decoder_frames); + setInt64(env, result, "cacheBytes", state.network.cacheBytes); + setDouble(env, result, "cacheDuration", state.network.cacheDuration); + setInt64(env, result, "currentBitrate", state.network.currentBitrate); + return result; +} + +napi_value stateValue(napi_env env) +{ + const PlaybackSnapshot state = gPlayer.snapshot(); + napi_value result = nullptr; + napi_create_object(env, &result); + setString(env, result, "state", state.state); + setInt64(env, result, "sessionId", static_cast(state.sessionId)); + setDouble(env, result, "position", state.position); + setDouble(env, result, "duration", state.duration); + setDouble(env, result, "fps", state.fps); + setInt64(env, result, "dropped", state.dropped); + setInt64(env, result, "firstFrameMs", state.firstFrameMs); + setInt64(env, result, "renderCount", static_cast(state.renderCount)); + setString(env, result, "lastError", state.lastError); + setString(env, result, "errorCode", state.errorCode); + setDouble(env, result, "speed", state.speed); + setDouble(env, result, "volume", state.volume); + setBool(env, result, "muted", state.muted); + setInt64(env, result, "subtitleDelayMs", state.subtitleDelayMs); + setDouble(env, result, "subtitleScale", state.subtitleScale); + setDouble(env, result, "subtitlePosition", state.subtitlePosition); + setString(env, result, "requestedDecoder", state.requestedDecoder); + setString(env, result, "actualDecoder", state.actualDecoder); + setString(env, result, "hwCapability", state.hwCapability); + setBool(env, result, "hwdecRequested", state.hwdecRequested); + setBool(env, result, "hwdecActive", state.hwdecActive); + setInt64(env, result, "hardwareFrameCount", state.hardwareFrameCount); + setInt64(env, result, "softwareFrameCount", state.softwareFrameCount); + setInt64(env, result, "hwFallbackCount", state.hwFallbackCount); + setInt64(env, result, "nativeBufferImportCount", state.nativeBufferImportCount); + setInt64(env, result, "nativeBufferImportFailure", state.nativeBufferImportFailure); + setInt64(env, result, "surfacePresentCount", state.surfacePresentCount); + setInt64(env, result, "gpuInteropFrameCount", state.gpuInteropFrameCount); + setString(env, result, "surface", gRenderer.surfaceStatus()); + setString(env, result, "egl", gRenderer.eglStatus()); + setString(env, result, "mpv", gRenderer.mpvStatus()); + napi_set_named_property(env, result, "media", mediaValue(env, state.media)); + napi_set_named_property(env, result, "tracks", tracksValue(env, state.tracks)); + napi_set_named_property(env, result, "metrics", metricsValue(env, state)); + return result; +} + +napi_value createPlayer(napi_env env, napi_callback_info) +{ + return boolean(env, gPlayer.initialize()); +} + +napi_value prepare(napi_env env, napi_callback_info info) +{ + napi_value argument = nullptr; + PlaybackRequestModel request; + return firstArg(env, info, argument) && requestFromArg(env, argument, request) + ? boolean(env, gPlayer.load(request)) + : boolean(env, false); +} + +napi_value play(napi_env env, napi_callback_info) +{ + gPlayer.play(); + return boolean(env, true); +} + +napi_value pause(napi_env env, napi_callback_info) +{ + gPlayer.pause(); + return boolean(env, true); +} + +napi_value stop(napi_env env, napi_callback_info) +{ + gPlayer.stop(); + return boolean(env, true); +} + +napi_value release(napi_env env, napi_callback_info) +{ + gRenderer.onSurfaceDestroyed(nullptr); + gPlayer.release(); + return boolean(env, true); +} + +napi_value attachSurface(napi_env env, napi_callback_info) +{ + gRenderer.requestRender(); + return boolean(env, gRenderer.hasSurface()); +} + +napi_value detachSurface(napi_env env, napi_callback_info) +{ + gRenderer.onSurfaceDestroyed(nullptr); + return boolean(env, true); +} + +napi_value onForeground(napi_env env, napi_callback_info) +{ + gRenderer.requestRender(); + return boolean(env, true); +} + +napi_value onBackground(napi_env env, napi_callback_info) +{ + return boolean(env, true); +} + +napi_value onSurfaceAvailable(napi_env env, napi_callback_info) +{ + gRenderer.requestRender(); + return boolean(env, true); +} + +napi_value onSurfaceDestroyed(napi_env env, napi_callback_info) +{ + gRenderer.onSurfaceDestroyed(nullptr); + return boolean(env, true); +} + +napi_value seekTo(napi_env env, napi_callback_info info) +{ + napi_value argument = nullptr; + double milliseconds = 0.0; + if (!firstArg(env, info, argument) || napi_get_value_double(env, argument, &milliseconds) != napi_ok) { + return boolean(env, false); + } + return boolean(env, gPlayer.seekAbsolute(milliseconds / 1000.0, false, false)); +} + +napi_value seekBy(napi_env env, napi_callback_info info) +{ + napi_value argument = nullptr; + double milliseconds = 0.0; + if (!firstArg(env, info, argument) || napi_get_value_double(env, argument, &milliseconds) != napi_ok) { + return boolean(env, false); + } + gPlayer.seek(milliseconds / 1000.0); + return boolean(env, true); +} + +napi_value setSpeed(napi_env env, napi_callback_info info) +{ + napi_value argument = nullptr; + double value = 1.0; + return firstArg(env, info, argument) && napi_get_value_double(env, argument, &value) == napi_ok + ? boolean(env, gPlayer.setSpeed(value)) + : boolean(env, false); +} + +napi_value setVolume(napi_env env, napi_callback_info info) +{ + napi_value argument = nullptr; + double value = 100.0; + return firstArg(env, info, argument) && napi_get_value_double(env, argument, &value) == napi_ok + ? boolean(env, gPlayer.setVolume(value)) + : boolean(env, false); +} + +napi_value setMute(napi_env env, napi_callback_info info) +{ + napi_value argument = nullptr; + bool value = false; + return firstArg(env, info, argument) && napi_get_value_bool(env, argument, &value) == napi_ok + ? boolean(env, gPlayer.setMute(value)) + : boolean(env, false); +} + +napi_value selectAudioTrack(napi_env env, napi_callback_info info) +{ + napi_value argument = nullptr; + double value = -1; + return firstArg(env, info, argument) && napi_get_value_double(env, argument, &value) == napi_ok + ? boolean(env, gPlayer.selectAudioTrack(static_cast(value))) + : boolean(env, false); +} + +napi_value selectSubtitleTrack(napi_env env, napi_callback_info info) +{ + napi_value argument = nullptr; + double value = -1; + return firstArg(env, info, argument) && napi_get_value_double(env, argument, &value) == napi_ok + ? boolean(env, gPlayer.selectSubtitleTrack(static_cast(value))) + : boolean(env, false); +} + +napi_value disableSubtitle(napi_env env, napi_callback_info) +{ + return boolean(env, gPlayer.disableSubtitle()); +} + +napi_value addSubtitle(napi_env env, napi_callback_info info) +{ + napi_value argument = nullptr; + if (!firstArg(env, info, argument)) return boolean(env, false); + PlaybackSubtitleSource source; + napi_valuetype type = napi_undefined; + if (napi_typeof(env, argument, &type) != napi_ok) return boolean(env, false); + if (type == napi_string) { + if (!valueString(env, argument, source.url)) return boolean(env, false); + } else if (type == napi_object) { + namedString(env, argument, "url", source.url); + namedString(env, argument, "title", source.title); + namedString(env, argument, "language", source.language); + namedString(env, argument, "encoding", source.encoding); + namedBool(env, argument, "forced", source.forced); + namedBool(env, argument, "select", source.select); + } + return boolean(env, !source.url.empty() && gPlayer.addSubtitle(source)); +} + +napi_value removeSubtitle(napi_env env, napi_callback_info info) +{ + napi_value argument = nullptr; + double value = -1; + return firstArg(env, info, argument) && napi_get_value_double(env, argument, &value) == napi_ok + ? boolean(env, gPlayer.removeSubtitle(static_cast(value))) + : boolean(env, false); +} + +napi_value setSubtitleDelay(napi_env env, napi_callback_info info) +{ + napi_value argument = nullptr; + double value = 0.0; + return firstArg(env, info, argument) && napi_get_value_double(env, argument, &value) == napi_ok + ? boolean(env, gPlayer.setSubtitleDelay(value)) + : boolean(env, false); +} + +napi_value setSubtitleScale(napi_env env, napi_callback_info info) +{ + napi_value argument = nullptr; + double value = 1.0; + return firstArg(env, info, argument) && napi_get_value_double(env, argument, &value) == napi_ok + ? boolean(env, gPlayer.setSubtitleScale(value)) + : boolean(env, false); +} + +napi_value setSubtitlePosition(napi_env env, napi_callback_info info) +{ + napi_value argument = nullptr; + double value = 100.0; + return firstArg(env, info, argument) && napi_get_value_double(env, argument, &value) == napi_ok + ? boolean(env, gPlayer.setSubtitlePosition(value)) + : boolean(env, false); +} + +napi_value getState(napi_env env, napi_callback_info) +{ + return stateValue(env); +} + +napi_value getMediaInfo(napi_env env, napi_callback_info) +{ + return mediaValue(env, gPlayer.snapshot().media); +} + +napi_value getTracks(napi_env env, napi_callback_info) +{ + return tracksValue(env, gPlayer.snapshot().tracks); +} + +napi_value getMetrics(napi_env env, napi_callback_info) +{ + return metricsValue(env, gPlayer.snapshot()); +} + +napi_value getBuildInfo(napi_env env, napi_callback_info) +{ + const velix_core_build_info* info = velix_core_get_build_info(); + napi_value result = nullptr; + napi_create_object(env, &result); + setInt64(env, result, "apiVersion", info->api_version); + setString(env, result, "sdkVersion", info->sdk_version); + setString(env, result, "mpvVersion", info->mpv_version); + setString(env, result, "mpvCommit", info->mpv_commit); + setString(env, result, "ffmpegCommit", info->ffmpeg_commit); + setString(env, result, "ffmpegHarmonyCommit", info->ffmpeg_harmony_commit); + setString(env, result, "buildDate", info->build_date); + setString(env, result, "target", info->target); + setString(env, result, "architecture", info->architecture); + setString(env, result, "profile", info->profile); + setString(env, result, "featureFlags", info->feature_flags); + return result; +} + +void onSurfaceCreated(OH_NativeXComponent* component, void* window) +{ + gRenderer.onSurfaceCreated(component, window); +} + +void onSurfaceChanged(OH_NativeXComponent* component, void* window) +{ + gRenderer.onSurfaceChanged(component, window); +} + +void onSurfaceDestroyedCallback(OH_NativeXComponent*, void* window) +{ + gRenderer.onSurfaceDestroyed(window); +} + +void onTouch(OH_NativeXComponent*, void*) {} + +napi_value Init(napi_env env, napi_value exports) +{ + gPlayer.initialize(); + napi_value xcomponent = nullptr; + if (napi_get_named_property(env, exports, OH_NATIVE_XCOMPONENT_OBJ, &xcomponent) == napi_ok) { + OH_NativeXComponent* component = nullptr; + if (napi_unwrap(env, xcomponent, reinterpret_cast(&component)) == napi_ok && component != nullptr) { + static OH_NativeXComponent_Callback callbacks = { + onSurfaceCreated, onSurfaceChanged, onSurfaceDestroyedCallback, onTouch + }; + OH_NativeXComponent_RegisterCallback(component, &callbacks); + } + } + napi_property_descriptor properties[] = { + {"createPlayer", nullptr, createPlayer, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"prepare", nullptr, prepare, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"play", nullptr, play, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"pause", nullptr, pause, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"stop", nullptr, stop, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"release", nullptr, release, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"attachSurface", nullptr, attachSurface, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"detachSurface", nullptr, detachSurface, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"onForeground", nullptr, onForeground, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"onBackground", nullptr, onBackground, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"onSurfaceAvailable", nullptr, onSurfaceAvailable, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"onSurfaceDestroyed", nullptr, onSurfaceDestroyed, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"seekTo", nullptr, seekTo, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"seekBy", nullptr, seekBy, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"setSpeed", nullptr, setSpeed, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"setVolume", nullptr, setVolume, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"setMute", nullptr, setMute, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"selectAudioTrack", nullptr, selectAudioTrack, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"selectSubtitleTrack", nullptr, selectSubtitleTrack, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"disableSubtitle", nullptr, disableSubtitle, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"addSubtitle", nullptr, addSubtitle, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"removeSubtitle", nullptr, removeSubtitle, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"setSubtitleDelay", nullptr, setSubtitleDelay, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"setSubtitleScale", nullptr, setSubtitleScale, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"setSubtitlePosition", nullptr, setSubtitlePosition, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"getState", nullptr, getState, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"getMediaInfo", nullptr, getMediaInfo, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"getTracks", nullptr, getTracks, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"getMetrics", nullptr, getMetrics, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"getBuildInfo", nullptr, getBuildInfo, nullptr, nullptr, nullptr, napi_default, nullptr}, + }; + napi_define_properties(env, exports, sizeof(properties) / sizeof(properties[0]), properties); + return exports; +} + +} // namespace + +NAPI_MODULE(velix_core_binding, Init)