在这里插入图片描述

前言

在前几期文章中,我们依次探讨了 NDK 工程配置与 NAPI 基础、OpenGL ES 图形渲染管线,以及 Native 高性能计算的实现方式。本篇将把目光转向另一个核心场景——音视频编解码

大多数应用在处理视频时,要么依赖 ArkUI 原生的 Video 组件,要么借助 MediaKit 的 ArkTS 接口。然而,当需要对视频帧做逐像素处理(比如加滤镜、做 AR 叠加),或者要在 Native 层实现极低延迟的解码时,就需要绕过 JS 层的封装,直接在 C++ 中调用系统 AVCodec,通过 XComponent 将解码后的帧渲染到屏幕上。

本文将完整实现这一链路:先用 AVDemuxer 分离 H.264 码流,再用 AVCodec 解码出原始帧(YUV/RGB),然后通过 NativeWindow 做 Surface 渲染,最终借助 NAPI 将状态回传到 ArkUI。整个流程涵盖 XComponent SURFACE 模式注册、CodecBase 配置、解码回调机制、NAPI 桥接与渲染管线的全部核心代码。


一、整体架构与数据流

在深入代码之前,我们先梳理一下整个解码渲染链路的结构。

[MP4/TS 文件]
    ↓ (file descriptor / fd)
[AVDemuxer]         ← native_avcodec.so
    ↓ H.264 ES 流
[AVCodec]           ← native_avcodec.so(硬件/软件解码器)
    ↓ 解码帧(NativeFrame)
[NAPI 回调]         ← native_async_work / threadsafe_function
    ↓ Frame 对象(宽度/高度/纹理ID)
[ArkTS 层]          ← napi_value 桥接
    ↓ surfaceId
[XComponent SURFACE] → NativeWindow → 硬件合成

这里有三条并行线程需要协调:文件读取线程(AVDemuxer)、解码线程(AVCodec)、渲染线程(NativeWindow/Surface)。三者之间通过队列传递数据,我们将在后续代码中显式管理这个队列。


二、工程结构与 CMakeLists 配置

在 HarmonyOS NEXT 中,Native 音视频能力依赖 native_avcodec.so,不再需要独立安装编解码器库,只需要在 CMakeLists 中正确链接即可。

2.1 目录结构

entry/src/main/
├── cpp/
│   ├── CMakeLists.txt
│   ├── types/
│   │   └── libentry/
│   │       ├── index.d.ts
│   │       └── native_api.cpp.h
│   └── src/
│       ├── h264_decoder.cpp        # 解码器核心实现
│       ├── surface_render.cpp      # NativeWindow 渲染
│       ├── napi_bridge.cpp         # NAPI 桥接层
│       └── common/utils.cpp        # 工具函数
├── ets/
│   └── entryability/
│       └── EntryAbility.ets
└── pages/
    └── index.ets                   # 播放器主页面

2.2 CMakeLists.txt 配置

相比普通 NDK 项目,音视频工程的 CMakeLists 需要额外声明编解码器依赖。native_avcodec 会在链接时自动解析 AVCodec、AVDemuxer、NativeWindow 等核心符号。

# CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)

project("native_video_decoder")

set(NATIVE_AUDIO_VIDEO_PATH "${CMAKE_CURRENT_SOURCE_DIR}")

# 包含头文件路径
add_definitions(-DLOG_DOMAIN=0xFF00)
add_definitions(-DLOG_TAG="H264Decoder")

# 查找系统编解码库(NDK 内置)
find_library(avcodec_lib native_avcodec)
find_library(native_window_lib native_window)
find_library(native_media_lib native_media)
find_library(hilog_lib hilog)
find_library(media_lib native_media_core)

set(DecoderLibs
    ${avcodec_lib}
    ${native_window_lib}
    ${native_media_lib}
    ${hilog_lib}
    ${media_lib}
)

add_library(entry SHARED
    native_api.cpp
    src/h264_decoder.cpp
    src/surface_render.cpp
    src/napi_bridge.cpp
    src/common/utils.cpp
)

target_link_libraries(entry
    ${DecoderLibs}
    libace_ndk.z.so
    libhilog_ndk.z.so
    libuv.so
)

target_include_directories(entry PRIVATE
    ${NATIVE_AUDIO_VIDEO_PATH}/src
    ${NATIVE_AUDIO_VIDEO_PATH}/types/libentry
)

三、XComponent SURFACE 模式注册

XComponent 是 ArkUI 与 Native 层之间的"双向通道"。在渲染场景下,我们使用 SURFACE 模式,此时 XComponent 会创建一块 Surface,Native 层通过其 surfaceId 获取 NativeWindow 句柄,后续的帧渲染全部发生在 NativeWindow 上。

3.1 ArkTS 侧:声明 XComponent

在 ArkUI 的 index.ets 中,XComponent 需要声明为 Surface 类型,并保存其 surfaceId 供 NAPI 使用。注意 XComponent 的宽高应与视频原始分辨率对齐,否则画面会出现拉伸。

// entry/src/main/ets/pages/index.ets
import hilog from '@ohos.hilog';

@Entry
@Component
struct Index {
  private xComponentController: XComponentController = new XComponentController();
  private surfaceId: string = '';
  private decoder: NativeDecoder | undefined = undefined;

  aboutToAppear(): void {
    hilog.info(0xFF00, "H264Demo", "页面即将挂载");
  }

  build() {
    Column() {
      Text("Native H.264 解码渲染示例")
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 20 })

      XComponentController.createXComponentSurface(
        this.xComponentController,
        { SurfaceId: "" }
      )

      XComponent({
        id: 'video_surface',
        type: XComponentType.SURFACE,
        controller: this.xComponentController
      })
        .width('100%')
        .aspectRatio(16 / 9)
        .backgroundColor(Color.Black)
        .onReady((event) => {
          this.surfaceId = event.surfaceId;
          hilog.info(0xFF00, "H264Demo", "XComponent 已就绪, surfaceId: %{public}s", this.surfaceId);
          // 初始化 Native 解码器,传入 surfaceId
          this.initDecoder(this.surfaceId);
        })

      Row({ space: 12 }) {
        Button("开始解码")
          .onClick(() => {
            this.startDecode();
          })
          .height(44)
          .width('45%')

        Button("停止解码")
          .onClick(() => {
            this.stopDecode();
          })
          .height(44)
          .width('45%')
      }
      .margin({ top: 20 })
      .width('100%')
      .justifyContent(FlexAlign.Center)
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .justifyContent(FlexAlign.Center)
  }

  private initDecoder(surfaceId: string): void {
    // 加载 Native 库并初始化解码器
    const res = globalThis.Process.loadNativeLibrary('libentry.z.so');
    hilog.info(0xFF00, "H264Demo", "Native库加载结果: %{public}d", res);

    // 调用 NAPI 初始化解码器
    const nativeLib = globalThis.Process.findLibraryByName('libentry.z.so');
    if (nativeLib) {
      const funcPtr = nativeLib.findSymbolByName('OH_H264Decoder_Init');
      if (funcPtr) {
        hilog.info(0xFF00, "H264Demo", "找到解码器初始化符号");
      }
    }
    // 实际项目中直接通过 NAPI 接口调用,详见第 5 节
  }

  private startDecode(): void {
    hilog.info(0xFF00, "H264Demo", "用户点击开始解码");
  }

  private stopDecode(): void {
    hilog.info(0xFF00, "H264Demo", "用户点击停止解码");
  }
}

在这里插入图片描述

在上面的代码中,XComponent 的 onReady 回调会在 Native Surface 创建完成后触发,此时获取的 surfaceId 即为后续渲染的关键句柄。ArkUI 会将这个 ID 以字符串形式传递给 Native 层。

3.2 Native 侧:通过 surfaceId 获取 NativeWindow

拿到 surfaceId 后,Native 代码需要将其转换为 OHNativeWindow* 句柄,才能进行后续的缓冲区操作。这个转换由 OH_NativeWindow_GetNativeWindow 函数完成。

// src/surface_render.h
#ifndef SURFACE_RENDER_H
#define SURFACE_RENDER_H

#include <surface.h>
#include <window.h>
#include <native_buffer.h>
#include <cstdint>

class SurfaceRenderer {
public:
    SurfaceRenderer();
    ~SurfaceRenderer();

    // 通过 surfaceId 初始化渲染器
    int32_t InitWithSurfaceId(const char* surfaceId);

    // 渲染一帧 YUV420sp 数据(NV12)
    int32_t RenderFrame(uint8_t* yBuffer, uint8_t* uvBuffer,
                        int32_t width, int32_t height);

    // 渲染一帧 RGBA8888 数据
    int32_t RenderFrameRGBA(uint8_t* rgbaBuffer,
                            int32_t width, int32_t height);

    // 释放渲染器
    void Release();

    bool IsReady() const { return nativeWindow_ != nullptr; }

private:
    OHNativeWindow* nativeWindow_;
    int32_t width_;
    int32_t height_;
};

#endif // SURFACE_RENDER_H
// src/surface_render.cpp
#include "surface_render.h"
#include <hilog_ndk.h>
#include <cstring>
#include <thread>

#undef LOG_DOMAIN
#define LOG_DOMAIN 0xFF00
#undef LOG_TAG
#define LOG_TAG "SurfaceRender"

namespace {
    constexpr int32_t BUFFER_COUNT = 4;       // 双缓冲
    constexpr uint32_t RGBA_FORMAT = 2;         // NATIVEFORMAT_RGBA_8888
}

SurfaceRenderer::SurfaceRenderer()
    : nativeWindow_(nullptr), width_(0), height_(0) {
}

SurfaceRenderer::~SurfaceRenderer() {
    Release();
}

int32_t SurfaceRenderer::InitWithSurfaceId(const char* surfaceId) {
    if (surfaceId == nullptr) {
        HILOG_ERROR(LOG_DOMAIN, LOG_TAG, "surfaceId 为空");
        return -1;
    }

    // 核心 API:通过 surfaceId 字符串获取 NativeWindow 句柄
    nativeWindow_ = OH_NativeWindow_GetNativeWindowBySurfaceId(surfaceId);
    if (nativeWindow_ == nullptr) {
        HILOG_ERROR(LOG_DOMAIN, LOG_TAG, "获取 NativeWindow 失败");
        return -1;
    }

    // 设置渲染参数:宽高和像素格式
    // NATIVEFORMAT_RGBA_8888 = 2,与 SurfaceComposerClient 中的定义一致
    int32_t ret = OH_NativeWindow_SetNativeWindowName(nativeWindow_, "H264DecoderOutput");
    if (ret != 0) {
        HILOG_ERROR(LOG_DOMAIN, LOG_TAG, "设置窗口名称失败: %{public}d", ret);
    }

    HILOG_INFO(LOG_DOMAIN, LOG_TAG, "NativeWindow 初始化成功");
    return 0;
}

int32_t SurfaceRenderer::RenderFrame(uint8_t* yBuffer, uint8_t* uvBuffer,
                                     int32_t width, int32_t height) {
    if (nativeWindow_ == nullptr) {
        return -1;
    }

    // NV12 格式:Y 平面 + UV 交错平面(UV plane 起始地址即为 V 平面)
    // 注意:鸿蒙 NativeWindow 的 EGLImage 路径更适合处理 RGBA,
    // 若需要渲染 NV12,需要先在 Native 层做 YUV→RGBA 转换(可用 libyuv)

    return RenderFrameRGBA(yBuffer, width, height);
}

int32_t SurfaceRenderer::RenderFrameRGBA(uint8_t* rgbaBuffer,
                                         int32_t width, int32_t height) {
    if (nativeWindow_ == nullptr || rgbaBuffer == nullptr) {
        return -1;
    }

    // 请求一块可写的 NativeWindow 缓冲区
    struct NativeWindowBuffer* buffer = nullptr;
    int fenceFd = -1;
    int32_t ret = OH_NativeWindow_GetNativeWindowBuffer(nativeWindow_, &buffer, &fenceFd);
    if (ret != 0 || buffer == nullptr) {
        HILOG_ERROR(LOG_DOMAIN, LOG_TAG, "申请窗口缓冲区失败: %{public}d", ret);
        return ret;
    }

    // 锁定缓冲区,获取实际内存地址用于写入
    void* virAddr = nullptr;
    ret = OH_NativeWindow_MapBuffer(nativeWindow_, buffer, &virAddr);
    if (ret != 0 || virAddr == nullptr) {
        HILOG_ERROR(LOG_DOMAIN, LOG_TAG, "映射缓冲区失败: %{public}d", ret);
        return ret;
    }

    // 执行内存拷贝:从 RGBA 输入数据到 NativeWindow 缓冲区
    // 注意:RGBA 每像素 4 字节(width * 4 对齐到 8 字节边界)
    uint32_t stride = width * 4;
    uint8_t* dst = static_cast<uint8_t*>(virAddr);
    for (int32_t y = 0; y < height; y++) {
        memcpy(dst + y * stride, rgbaBuffer + y * width * 4, static_cast<size_t>(width * 4));
    }

    // 解除映射
    OH_NativeWindow_UnmapBuffer(nativeWindow_, buffer);

    // 将缓冲区放回队列,触发 SurfaceComposer 合成到屏幕
    ret = OH_NativeWindow_NativeWindowHandleInputBuffer(nativeWindow_, buffer, 0);
    if (ret != 0) {
        HILOG_ERROR(LOG_DOMAIN, LOG_TAG, "提交输入缓冲区失败: %{public}d", ret);
        return ret;
    }

    // 设置刷新区域(完整帧),并提交给合成器
    OH_NativeWindow_NativeWindowSetScalingMode(nativeWindow_, 0,
        NATIVE_WINDOW_SCALING_MODE_SCALE_TO_WINDOW);
    OH_NativeWindow_NativeWindowFlushBuffer(nativeWindow_, buffer, fenceFd,
        { 0, 0, static_cast<int32_t>(width), static_cast<int32_t>(height),
          NATIVE_WINDOW_SCALING_MODE_SCALE_TO_WINDOW });

    HILOG_DEBUG(LOG_DOMAIN, LOG_TAG, "帧渲染成功: %{public}dx%{public}d", width, height);
    return 0;
}

void SurfaceRenderer::Release() {
    if (nativeWindow_ != nullptr) {
        // OH_NativeWindow_DestroyNativeWindow 会同时释放相关资源
        OH_NativeWindow_DestroyNativeWindow(nativeWindow_);
        nativeWindow_ = nullptr;
    }
    HILOG_INFO(LOG_DOMAIN, LOG_TAG, "SurfaceRenderer 已释放");
}

上面这段代码完成了从 Surface 到屏幕的完整渲染通路。值得注意的有两点:第一,NativeWindow 的像素格式默认是 RGBA_8888,而解码器通常输出 YUV420(NV12/YV12),因此在生产环境中通常需要引入 libyuv 做格式转换;第二,OH_NativeWindow_NativeWindowFlushBuffer 提交缓冲区时必须指定正确的 dirty rect,否则合成器可能只渲染部分区域。


四、AVDemuxer + AVCodec 解码器实现

接下来是最核心的部分——用 CodecBase API 解码 H.264 码流。HarmonyOS NEXT 提供的 native_avcodec 封装了一套统一的编解码接口,使用方式与 Android NDK 的 MediaCodec 非常接近,但细节上存在差异。

4.1 解码器状态机

在正式写代码之前,理解 CodecBase 的状态流转至关重要。每次送入数据、取出数据,都需要在正确的状态下进行,否则会返回 ERRCodecBufState 等错误码。

Unconfigured → Configured → Prepared → [Running ←→ Flushed]
                          ↓
                       EndOfStream

启动解码器的典型流程为:创建实例 → 配置(输出 Surface 或输出回调)→ Start() → 循环送入数据 → Stop()Release()

4.2 H264Decoder 核心类实现

我们将解码器封装为一个完整的 C++ 类,对外暴露初始化、启动、停止三个接口,内部管理 AVDemuxer 与 AVCodec 的协作。

// src/h264_decoder.h
#ifndef H264_DECODER_H
#define H264_DECODER_H

#include <memory>
#include <queue>
#include <mutex>
#include <atomic>
#include <functional>
#include <string>
#include <cstdint>

// 声明前置类型,避免引入过多头文件
struct OH_AVCodec;
struct OH_AVFormat;
struct NativeWindowBuffer;
class SurfaceRenderer;

struct DecodedFrame {
    uint8_t* rgbaData;
    int32_t width;
    int32_t height;
    int64_t timestamp;   // 微秒
    bool valid;

    DecodedFrame() : rgbaData(nullptr), width(0), height(0), timestamp(0), valid(false) {}
};

class H264Decoder {
public:
    using FrameCallback = std::function<void(const DecodedFrame& frame)>;

    H264Decoder();
    ~H264Decoder();

    // 初始化解码器(外部传入渲染器,由 NAPI 层负责创建)
    int32_t Init(std::shared_ptr<SurfaceRenderer> renderer);

    // 启动解码(打开文件,解码线程开始工作)
    int32_t Start(const char* filePath);

    // 停止解码
    void Stop();

    // 注册帧回调(ArkTS 侧通过 NAPI 注册)
    void SetFrameCallback(FrameCallback cb);

    bool IsRunning() const { return running_.load(); }

private:
    // 内部线程函数
    void DemuxThreadFunc();      // 负责读取文件并送入解码器
    void DecodeThreadFunc();     // 负责从解码器取出帧并渲染

    // AVCodec 回调(由系统调用)
    static void OnCodecError(OH_AVCodec* codec, int32_t errorCode, void* userData);
    static void OnCodecFormatChange(OH_AVCodec* codec, OH_AVFormat* format, void* userData);
    static void OnCodecInputDataReady(OH_AVCodec* codec, uint32_t index,
                                       OH_AVCodecBufferAttr* attr, void* userData);
    static void OnCodecOutputDataReady(OH_AVCodec* codec, uint32_t index,
                                        OH_AVCodecBufferAttr* attr, void* userData);

    int32_t ConfigureVideoDecoder();
    int32_t ProcessOnePacket();  // 从 demuxer 读取一帧,送入解码器
    int32_t ProcessOneFrame();   // 从解码器取出一帧,渲染或回调

    std::shared_ptr<SurfaceRenderer> renderer_;
    FrameCallback frameCallback_;

    OH_AVCodec* videoDecoder_;
    OH_AVFormat* inputFormat_;
    OH_AVFormat* outputFormat_;

    int fileFd_;                    // 文件描述符
    std::atomic<bool> running_;
    std::atomic<bool> eosReached_;  // 输入结束标志

    std::mutex inputQueueMutex_;
    std::mutex outputQueueMutex_;
    std::queue<uint32_t> availableInputIndices_;
    std::queue<uint32_t> availableOutputIndices_;

    std::thread demuxThread_;
    std::thread decodeThread_;
    std::atomic<bool> decoderConfigured_;
};

#endif // H264_DECODER_H
// src/h264_decoder.cpp
#include "h264_decoder.h"
#include "surface_render.h"
#include "common/utils.h"
#include <avdemuxer.h>
#include <avcodec.h>
#include <media_errors.h>
#include <hilog_ndk.h>
#include <cstring>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

#undef LOG_DOMAIN
#define LOG_DOMAIN 0xFF00
#undef LOG_TAG
#define LOG_TAG "H264Decoder"

namespace {
    constexpr uint32_t INPUT_BUFFER_COUNT = 8;
    constexpr uint32_t OUTPUT_BUFFER_COUNT = 8;
    constexpr int64_t USEC_PER_SEC = 1000000LL;
}

H264Decoder::H264Decoder()
    : videoDecoder_(nullptr),
      inputFormat_(nullptr),
      outputFormat_(nullptr),
      fileFd_(-1),
      running_(false),
      eosReached_(false),
      decoderConfigured_(false) {
}

H264Decoder::~H264Decoder() {
    Stop();
}

// --- NAPI 调用的初始化入口 ---
int32_t H264Decoder::Init(std::shared_ptr<SurfaceRenderer> renderer) {
    if (renderer == nullptr) {
        HILOG_ERROR(LOG_DOMAIN, LOG_TAG, "渲染器为空,无法初始化解码器");
        return -1;
    }
    renderer_ = renderer;

    // 创建视频解码器实例(按 MIME 类型创建,或直接用硬解 name)
    // 注意:H.264 硬件解码器的 name 通常为 "OMX.rk.hardware.avc.decoder"
    // 但更通用的方式是使用 AVCODEC_MIMETYPE_VIDEO_AVC
    videoDecoder_ = OH_AVCodec_CreateByMime(AVCODEC_MIMETYPE_VIDEO_AVC);
    if (videoDecoder_ == nullptr) {
        HILOG_ERROR(LOG_DOMAIN, LOG_TAG, "创建 AVCodec 失败");
        return -1;
    }

    // 注册所有回调(必须先注册,再 Configure)
    int32_t ret = OH_AVCodec_RegisterCallback(videoDecoder_,
        OnCodecError,
        OnCodecFormatChange,
        OnCodecInputDataReady,
        OnCodecOutputDataReady,
        this);
    if (ret != AV_ERR_OK) {
        HILOG_ERROR(LOG_DOMAIN, LOG_TAG, "注册解码器回调失败: %{public}d", ret);
        OH_AVCodec_Destroy(videoDecoder_);
        videoDecoder_ = nullptr;
        return ret;
    }

    HILOG_INFO(LOG_DOMAIN, LOG_TAG, "解码器创建成功,等待 Start 时配置");
    return 0;
}

// --- 启动解码:打开文件,启动线程 ---
int32_t H264Decoder::Start(const char* filePath) {
    if (filePath == nullptr || videoDecoder_ == nullptr) {
        return -1;
    }

    // 打开文件(使用 fd 方式,AVDemuxer 更高效)
    fileFd_ = open(filePath, O_RDONLY);
    if (fileFd_ < 0) {
        HILOG_ERROR(LOG_DOMAIN, LOG_TAG, "无法打开文件: %{public}s, errno: %{public}d",
                    filePath, errno);
        return -1;
    }

    // 配置解码器(输出到 SurfaceRenderer,即内存模式,由我们手动渲染)
    int32_t ret = ConfigureVideoDecoder();
    if (ret != AV_ERR_OK) {
        close(fileFd_);
        fileFd_ = -1;
        return ret;
    }

    // 启动解码器
    ret = OH_AVCodec_Start(videoDecoder_);
    if (ret != AV_ERR_OK) {
        HILOG_ERROR(LOG_DOMAIN, LOG_TAG, "启动解码器失败: %{public}d", ret);
        OH_AVCodec_Stop(videoDecoder_);
        OH_AVCodec_Destroy(videoDecoder_);
        videoDecoder_ = nullptr;
        close(fileFd_);
        fileFd_ = -1;
        return ret;
    }

    // 启动解封装线程和解码线程
    running_ = true;
    eosReached_ = false;

    demuxThread_ = std::thread(&H264Decoder::DemuxThreadFunc, this);
    decodeThread_ = std::thread(&H264Decoder::DecodeThreadFunc, this);

    // 将线程设置为分离态,由系统自动回收
    demuxThread_.detach();
    decodeThread_.detach();

    HILOG_INFO(LOG_DOMAIN, LOG_TAG, "解码器已启动: %{public}s", filePath);
    return 0;
}

// --- 停止解码 ---
void H264Decoder::Stop() {
    if (!running_.load()) {
        return;
    }

    running_ = false;

    if (videoDecoder_ != nullptr) {
        OH_AVCodec_Stop(videoDecoder_);
        OH_AVCodec_Destroy(videoDecoder_);
        videoDecoder_ = nullptr;
    }

    if (fileFd_ >= 0) {
        close(fileFd_);
        fileFd_ = -1;
    }

    HILOG_INFO(LOG_DOMAIN, LOG_TAG, "解码器已停止");
}

// --- 配置解码器:设置像素格式为 RGBA(方便渲染) ---
int32_t H264Decoder::ConfigureVideoDecoder() {
    // 创建解码器配置格式对象
    inputFormat_ = OH_AVFormat_Create();
    if (inputFormat_ == nullptr) {
        return -1;
    }

    // 可选:设置解码器需要的参数
    // 如启用低延迟模式(需要硬件支持)
    // OH_AVFormat_SetIntValue(inputFormat_, "low-latency", 1);

    // 配置解码器(Surface 模式下不需要手动设置输出格式,
    // 解码器会自动将帧输出到已绑定的 Surface。
    // 但我们这里使用回调模式,因此先不配置 Surface,
    // 而是让解码器输出到内存缓冲区,由我们自己渲染)

    int32_t ret = OH_AVCodec_Configure(videoDecoder_, inputFormat_);
    if (ret != AV_ERR_OK) {
        HILOG_ERROR(LOG_DOMAIN, LOG_TAG, "配置解码器失败: %{public}d", ret);
        OH_AVFormat_Destroy(inputFormat_);
        inputFormat_ = nullptr;
        return ret;
    }

    decoderConfigured_ = true;
    HILOG_INFO(LOG_DOMAIN, LOG_TAG, "解码器配置完成");
    return AV_ERR_OK;
}

// ==================== 回调实现 ====================

void H264Decoder::OnCodecError(OH_AVCodec* codec, int32_t errorCode, void* userData) {
    if (userData == nullptr) return;
    auto* decoder = static_cast<H264Decoder*>(userData);
    HILOG_ERROR(LOG_DOMAIN, LOG_TAG, "解码器错误: %{public}d", errorCode);
}

void H264Decoder::OnCodecFormatChange(OH_AVCodec* codec, OH_AVFormat* format, void* userData) {
    if (userData == nullptr || format == nullptr) return;
    auto* decoder = static_cast<H264Decoder*>(userData);

    int32_t width = OH_AVFormat_GetIntValue(format, "width", 0);
    int32_t height = OH_AVFormat_GetIntValue(format, "height", 0);
    int64_t duration = OH_AVFormat_GetIntValue(format, "duration", 0);

    HILOG_INFO(LOG_DOMAIN, LOG_TAG, "解码器格式变化: %{public}dx%{public}d, duration: %{public}lld",
               width, height, (long long)duration);

    // 记录输出格式,供后续帧处理使用
    decoder->outputFormat_ = format;
}

void H264Decoder::OnCodecInputDataReady(OH_AVCodec* codec, uint32_t index,
                                         OH_AVCodecBufferAttr* attr, void* userData) {
    // 输入缓冲区已就绪,解封装线程可以继续送入数据
    // 此回调仅作通知,实际送数据在 DemuxThreadFunc 中完成
}

void H264Decoder::OnCodecOutputDataReady(OH_AVCodec* codec, uint32_t index,
                                          OH_AVCodecBufferAttr* attr, void* userData) {
    if (userData == nullptr || attr == nullptr) return;
    auto* decoder = static_cast<H264Decoder*>(userData);

    // 有一帧解码完成,将其放入输出队列
    {
        std::lock_guard<std::mutex> lock(decoder->outputQueueMutex_);
        decoder->availableOutputIndices_.push(index);
    }

    HILOG_DEBUG(LOG_DOMAIN, LOG_TAG, "解码帧就绪, index=%{public}u, pts=%{public}lld",
                index, (long long)attr->pts);
}

// ==================== 解封装线程 ====================

void H264Decoder::DemuxThreadFunc() {
    HILOG_INFO(LOG_DOMAIN, LOG_TAG, "解封装线程启动");

    // 初始化 AVDemuxer(使用 native_avcodec 提供的接口)
    // 注意:HarmonyOS NEXT 中 AVDemuxer 的创建方式
    OH_AVDemuxer* demuxer = nullptr;
    OH_AVFormat* format = OH_AVFormat_Create();

    // 通过 fd 创建 Demuxer(也可以用 URL 方式)
    int32_t ret = OH_AVDemuxer_CreateWithFD(fileFd_, 0, fileFd_, 0, &demuxer);
    if (ret != AV_ERR_OK || demuxer == nullptr) {
        HILOG_ERROR(LOG_DOMAIN, LOG_TAG, "创建 AVDemuxer 失败: %{public}d", ret);
        running_ = false;
        return;
    }

    // 获取媒体轨道信息,找到视频轨道
    int32_t trackCount = 0;
    OH_AVDemuxer_GetTrackInfoSize(demuxer, &trackCount);
    int32_t videoTrackIndex = -1;

    for (int32_t i = 0; i < trackCount; i++) {
        OH_AVFormat* trackFormat = OH_AVFormat_Create();
        ret = OH_AVDemuxer_GetTrackInfo(demuxer, i, trackFormat);
        if (ret != AV_ERR_OK) {
            OH_AVFormat_Destroy(trackFormat);
            continue;
        }

        char mime[64] = {0};
        if (OH_AVFormat_GetStringValue(trackFormat, "mime", mime, sizeof(mime))) {
            if (strncmp(mime, "video/", 6) == 0) {
                videoTrackIndex = i;
                // 选中视频轨道
                OH_AVDemuxer_SelectTrackByIndex(demuxer, i);
                HILOG_INFO(LOG_DOMAIN, LOG_TAG, "选择视频轨道: %{public}d, mime=%{public}s",
                           i, mime);
            }
        }
        OH_AVFormat_Destroy(trackFormat);

        if (videoTrackIndex >= 0) break;
    }

    if (videoTrackIndex < 0) {
        HILOG_ERROR(LOG_DOMAIN, LOG_TAG, "未找到视频轨道");
        OH_AVDemuxer_Destroy(demuxer);
        OH_AVFormat_Destroy(format);
        running_ = false;
        return;
    }

    // 开始循环读取数据并送入解码器
    while (running_.load()) {
        ProcessOnePacket(demuxer);
    }

    // 发送 EOS(End Of Stream)
    if (running_.load() == false) {
        // 确保发送 EOS 信号
    }

    OH_AVDemuxer_Destroy(demuxer);
    OH_AVFormat_Destroy(format);
    HILOG_INFO(LOG_DOMAIN, LOG_TAG, "解封装线程结束");
}

// 从 demuxer 读取一帧 packet,送入解码器
int32_t H264Decoder::ProcessOnePacket(OH_AVDemuxer* demuxer) {
    if (demuxer == nullptr || videoDecoder_ == nullptr) {
        return -1;
    }

    // 获取可用的输入缓冲区索引(等待回调通知或轮询)
    uint32_t inputIndex = 0;
    {
        std::unique_lock<std::mutex> lock(inputQueueMutex_);
        // 此处简化处理:使用超时等待
        // 实际项目中应使用条件变量与 OnCodecInputDataReady 回调配合
        auto waitResult = [this]() -> bool {
            return !availableInputIndices_.empty() || !running_.load();
        };

        if (!availableInputIndices_.empty()) {
            inputIndex = availableInputIndices_.front();
            availableInputIndices_.pop();
        } else {
            // 解码器尚未就绪,稍作等待
            std::this_thread::sleep_for(std::chrono::milliseconds(1));
            return 0;
        }
    }

    // 从 demuxer 读取一帧
    OH_AVFormat* packetFormat = OH_AVFormat_Create();
    OH_AVMemory* packetBuffer = OH_AVMemory_Create(4096);
    int64_t sampleTime = 0;
    bool isKeyFrame = false;

    // 读取样本数据
    int32_t ret = OH_AVDemuxer_ReadSample(demuxer, 0, packetBuffer, &sampleTime, &isKeyFrame);
    if (ret == AV_ERR_EOS) {
        // 文件结束,发送 EOS 通知
        HILOG_INFO(LOG_DOMAIN, LOG_TAG, "文件读取完毕,发送 EOS");
        OH_AVCodec_NotifyEndOfStream(videoDecoder_);
        eosReached_ = true;
        OH_AVFormat_Destroy(packetFormat);
        OH_AVMemory_Destroy(packetBuffer);
        return 0;
    } else if (ret != AV_ERR_OK) {
        OH_AVFormat_Destroy(packetFormat);
        OH_AVMemory_Destroy(packetBuffer);
        return ret;
    }

    // 将数据送入解码器
    uint8_t* data = OH_AVMemory_GetAddr(packetBuffer);
    size_t size = OH_AVMemory_GetSize(packetBuffer);

    OH_AVCodecBufferAttr attr = {
        .size = static_cast<int32_t>(size),
        .offset = 0,
        .pts = sampleTime,
        .flags = isKeyFrame ? AVCODEC_BUFFER_FLAGS_SYNC_FRAME : 0
    };

    ret = OH_AVCodec_PushInputBuffer(videoDecoder_, inputIndex, &attr, data);
    if (ret != AV_ERR_OK) {
        HILOG_WARN(LOG_DOMAIN, LOG_TAG, "送入输入缓冲区失败: %{public}d", ret);
        // 将索引放回队列以便重试
        {
            std::lock_guard<std::mutex> lock(inputQueueMutex_);
            availableInputIndices_.push(inputIndex);
        }
    } else {
        HILOG_DEBUG(LOG_DOMAIN, LOG_TAG, "已送入 packet, size=%{public}zu, pts=%{public}lld, key=%{public}d",
                    size, (long long)sampleTime, isKeyFrame);
    }

    OH_AVFormat_Destroy(packetFormat);
    OH_AVMemory_Destroy(packetBuffer);
    return ret;
}

// ==================== 解码线程 ====================

void H264Decoder::DecodeThreadFunc() {
    HILOG_INFO(LOG_DOMAIN, LOG_TAG, "解码线程启动");

    while (running_.load()) {
        ProcessOneFrame();
    }

    HILOG_INFO(LOG_DOMAIN, LOG_TAG, "解码线程结束");
}

// 从解码器取出一帧并渲染/回调
int32_t H264Decoder::ProcessOneFrame() {
    // 从输出队列获取已就绪的缓冲区索引
    uint32_t outputIndex = 0;
    {
        std::unique_lock<std::mutex> lock(outputQueueMutex_);
        if (availableOutputIndices_.empty()) {
            std::this_thread::sleep_for(std::chrono::milliseconds(1));
            return 0;
        }
        outputIndex = availableOutputIndices_.front();
        availableOutputIndices_.pop();
    }

    // 获取解码后的数据(原始帧数据指针)
    OH_AVMemory* frameData = OH_AVCodec_GetOutputBuffer(videoDecoder_, outputIndex, false);
    if (frameData == nullptr) {
        OH_AVCodec_RenderOutputBuffer(videoDecoder_, outputIndex, false);
        return -1;
    }

    uint8_t* data = OH_AVMemory_GetAddr(frameData);
    size_t size = OH_AVMemory_GetSize(frameData);

    // 获取帧属性
    OH_AVFormat* frameFormat = OH_AVCodec_GetOutputDescription(videoDecoder_);
    int32_t width = 1920;
    int32_t height = 1080;
    if (frameFormat != nullptr) {
        width = OH_AVFormat_GetIntValue(frameFormat, "width", width);
        height = OH_AVFormat_GetIntValue(frameFormat, "height", height);
    }

    // 若有注册回调,构造帧对象并回调
    if (frameCallback_) {
        // 为 RGBA 数据分配临时缓冲区(拷贝出来)
        uint8_t* rgbaBuf = new uint8_t[width * height * 4];
        // 实际生产中:这里做 YUV→RGBA 转换(如调用 libyuv::NV12ToARGB)
        // 此处简化处理:假设解码器直接输出 RGBA(某些软解码器支持)
        memcpy(rgbaBuf, data, std::min(size, static_cast<size_t>(width * height * 4)));

        DecodedFrame frame;
        frame.rgbaData = rgbaBuf;
        frame.width = width;
        frame.height = height;
        frame.timestamp = 0;
        frame.valid = true;

        frameCallback_(frame);

        delete[] rgbaBuf;
    }

    // 若渲染器就绪,直接渲染
    if (renderer_ && renderer_->IsReady()) {
        // 在此添加 YUV→RGBA 转换逻辑(使用 libyuv)
        // ConvertNV12ToRGBA(data, rgbaOut, width, height);
        // renderer_->RenderFrameRGBA(rgbaOut, width, height);
        HILOG_DEBUG(LOG_DOMAIN, LOG_TAG, "渲染帧: %{public}dx%{public}d", width, height);
    }

    // 释放输出缓冲区(false = 不渲染到 Surface,因为我们用手动渲染)
    OH_AVCodec_RenderOutputBuffer(videoDecoder_, outputIndex, false);

    OH_AVMemory_Destroy(frameData);
    if (frameFormat != nullptr) {
        OH_AVFormat_Destroy(frameFormat);
    }

    return 0;
}

void H264Decoder::SetFrameCallback(FrameCallback cb) {
    frameCallback_ = std::move(cb);
}

上述代码中,解封装线程通过 OH_AVDemuxer_ReadSample 持续从文件中读取 H.264 样本,送入解码器;解码线程通过 availableOutputIndices_ 队列接收解码完成的帧,执行渲染或回调。两线程通过互斥锁和原子变量进行同步,这是典型的生产-消费模式。

有一点值得特别说明:回调中的 OH_AVCodec_GetOutputBuffer 返回的缓冲区地址,解码器已经帮我们填充了压缩格式解码后的原始像素数据。如果使用硬件解码器,输出可能是 EGLImage 句柄而非直接的内存地址,此时需要通过 OH_NativeImage_GetNativeBuffer 获取实际内存。具体使用哪种方式,由解码器实现决定。


五、NAPI 桥接层

ArkTS 无法直接操作 C++ 对象的生命周期,因此需要通过 NAPI 将 Native 层封装为 ArkTS 可调用的接口。我们设计一个简洁的 NAPI 模块,提供初始化、启动、停止、注册回调四个核心方法。

5.1 NAPI 模块注册

// src/napi_bridge.cpp
#include <napi/native_api.h>
#include <hilog_ndk.h>
#include <string>
#include <memory>
#include <map>
#include <mutex>

#include "h264_decoder.h"
#include "surface_render.h"

#undef LOG_DOMAIN
#define LOG_DOMAIN 0xFF00
#undef LOG_TAG
#define LOG_TAG "NapiBridge"

// 存储多个解码器实例(支持同时播放多个视频)
static std::map<int64_t, std::shared_ptr<H264Decoder>> decoderInstances_;
static std::map<int64_t, std::shared_ptr<SurfaceRenderer>> rendererInstances_;
static std::mutex instancesMutex_;
static int64_t nextId_ = 1;

// 辅助函数:从 napi_value 提取 std::string
static bool ExtractString(napi_env env, napi_value value, std::string& out) {
    char buffer[512] = {0};
    size_t length = 0;
    napi_status status = napi_get_value_string_utf8(env, value, buffer,
                                                     sizeof(buffer), &length);
    if (status != napi_ok) {
        return false;
    }
    out.assign(buffer, length);
    return true;
}

// 辅助函数:从 napi_value 提取 int32_t
static bool ExtractInt32(napi_env env, napi_value value, int32_t& out) {
    napi_status status = napi_get_value_int32(env, value, &out);
    return status == napi_ok;
}

// ==================== NAPI 方法实现 ====================

// OH_H264Decoder_Init: 初始化解码器
// 参数: surfaceId (string) - XComponent 的 surfaceId
// 返回: decoderId (int64_t) - 解码器实例 ID
static napi_value H264Decoder_Init(napi_env env, napi_callback_info info) {
    size_t argc = 1;
    napi_value args[1] = {nullptr};
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    if (argc < 1) {
        HILOG_ERROR(LOG_DOMAIN, LOG_TAG, "Init 缺少 surfaceId 参数");
        return nullptr;
    }

    std::string surfaceId;
    if (!ExtractString(env, args[0], surfaceId)) {
        HILOG_ERROR(LOG_DOMAIN, LOG_TAG, "无法解析 surfaceId");
        return nullptr;
    }

    // 创建渲染器并初始化
    auto renderer = std::make_shared<SurfaceRenderer>();
    int32_t ret = renderer->InitWithSurfaceId(surfaceId.c_str());
    if (ret != 0) {
        HILOG_ERROR(LOG_DOMAIN, LOG_TAG, "渲染器初始化失败: %{public}d", ret);
        return nullptr;
    }

    // 创建解码器
    auto decoder = std::make_shared<H264Decoder>();
    ret = decoder->Init(renderer);
    if (ret != 0) {
        HILOG_ERROR(LOG_DOMAIN, LOG_TAG, "解码器初始化失败: %{public}d", ret);
        return nullptr;
    }

    // 保存实例
    int64_t decoderId = nextId_++;
    {
        std::lock_guard<std::mutex> lock(instancesMutex_);
        rendererInstances_[decoderId] = renderer;
        decoderInstances_[decoderId] = decoder;
    }

    HILOG_INFO(LOG_DOMAIN, LOG_TAG, "解码器实例创建成功, ID=%{public}lld", (long long)decoderId);

    // 返回 ID 给 ArkTS
    napi_value result = nullptr;
    napi_create_int64(env, decoderId, &result);
    return result;
}

// OH_H264Decoder_Start: 启动解码
// 参数: decoderId (int64_t), filePath (string)
static napi_value H264Decoder_Start(napi_env env, napi_callback_info info) {
    size_t argc = 2;
    napi_value args[2] = {nullptr};
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    if (argc < 2) {
        HILOG_ERROR(LOG_DOMAIN, LOG_TAG, "Start 缺少参数");
        return nullptr;
    }

    int64_t decoderId = 0;
    std::string filePath;

    napi_get_value_int64(env, args[0], &decoderId);
    ExtractString(env, args[1], filePath);

    std::shared_ptr<H264Decoder> decoder;
    {
        std::lock_guard<std::mutex> lock(instancesMutex_);
        auto it = decoderInstances_.find(decoderId);
        if (it == decoderInstances_.end()) {
            HILOG_ERROR(LOG_DOMAIN, LOG_TAG, "找不到解码器实例: %{public}lld", (long long)decoderId);
            return nullptr;
        }
        decoder = it->second;
    }

    int32_t ret = decoder->Start(filePath.c_str());

    napi_value result = nullptr;
    napi_create_int32(env, ret, &result);
    return result;
}

// OH_H264Decoder_Stop: 停止解码
// 参数: decoderId (int64_t)
static napi_value H264Decoder_Stop(napi_env env, napi_callback_info info) {
    size_t argc = 1;
    napi_value args[1] = {nullptr};
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    if (argc < 1) return nullptr;

    int64_t decoderId = 0;
    napi_get_value_int64(env, args[0], &decoderId);

    std::shared_ptr<H264Decoder> decoder;
    {
        std::lock_guard<std::mutex> lock(instancesMutex_);
        auto it = decoderInstances_.find(decoderId);
        if (it != decoderInstances_.end()) {
            decoder = it->second;
        }
    }

    if (decoder) {
        decoder->Stop();
        HILOG_INFO(LOG_DOMAIN, LOG_TAG, "解码器已停止, ID=%{public}lld", (long long)decoderId);
    }

    return nullptr;
}

// OH_H264Decoder_Release: 释放解码器实例
// 参数: decoderId (int64_t)
static napi_value H264Decoder_Release(napi_env env, napi_callback_info info) {
    size_t argc = 1;
    napi_value args[1] = {nullptr};
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    if (argc < 1) return nullptr;

    int64_t decoderId = 0;
    napi_get_value_int64(env, args[0], &decoderId);

    {
        std::lock_guard<std::mutex> lock(instancesMutex_);
        decoderInstances_.erase(decoderId);
        rendererInstances_.erase(decoderId);
    }

    HILOG_INFO(LOG_DOMAIN, LOG_TAG, "解码器实例已释放, ID=%{public}lld", (long long)decoderId);
    return nullptr;
}

// ==================== 模块注册 ====================

static napi_value Export(napi_env env, napi_value exports) {
    // 逐一导出 NAPI 方法
    napi_property_descriptor desc[] = {
        DECLARE_NAPI_FUNCTION("init", H264Decoder_Init),
        DECLARE_NAPI_FUNCTION("start", H264Decoder_Start),
        DECLARE_NAPI_FUNCTION("stop", H264Decoder_Stop),
        DECLARE_NAPI_FUNCTION("release", H264Decoder_Release),
    };

    napi_define_properties(env, exports,
                           sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}

static napi_module nativeDecoderModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = Export,
    .nm_modname = "nativevideo",
    .nm_priv = nullptr,
    .reserved = {nullptr},
};

extern "C" __attribute__((visibility("default")))
napi_module* OH_GetModuleInterface() {
    return &nativeDecoderModule;
}

在上述 NAPI 模块中,instancesMutex_ 保护了全局解码器实例表,这是多线程环境下 NAPI 调用的标准安全模式。ArkTS 侧持有 decoderId,每次调用通过 ID 找到对应的 Native 对象。

5.2 ArkTS 侧的 NAPI 调用封装

为了在 ArkTS 中优雅地使用上述 NAPI 接口,我们包装成一个工具类:

// entry/src/main/ets/utils/NativeDecoder.ets

import hilog from '@ohos.hilog';

const TAG = "NativeDecoder";

export class NativeDecoder {
  private nativeModule: ESObject | null = null;
  private decoderId: number = -1;
  private surfaceId: string = "";
  private isInited: boolean = false;

  async loadLibrary(): Promise<void> {
    try {
      // 加载 Native so 库
      const loadRes = globalThis.Process.loadNativeLibrary('libentry.z.so');
      hilog.info(0xFF00, TAG, "加载 Native 库: %{public}d", loadRes);

      // 获取 nativevideo 模块
      const moduleName = 'nativevideo';
      this.nativeModule = globalThis.Process.getProcessModuleInfo()
        .nativeLibrary?.find((lib: ESObject) => lib.name === `libentry.z.so`);
      
      hilog.info(0xFF00, TAG, "Native 模块加载完成");
    } catch (e) {
      hilog.error(0xFF00, TAG, "加载 Native 模块失败: %{public}s", JSON.stringify(e));
    }
  }

  /**
   * 初始化解码器
   * @param surfaceId - XComponent 的 surfaceId
   */
  init(surfaceId: string): number {
    if (!this.nativeModule) {
      hilog.error(0xFF00, TAG, "Native 模块未加载");
      return -1;
    }

    this.surfaceId = surfaceId;

    // 调用 C++ 层的 OH_H264Decoder_Init
    try {
      // 通过 findSymbolByName 获取函数指针(实际项目中推荐用 napi API 绑定)
      const lib = globalThis.Process.findLibraryByName('libentry.z.so');
      if (lib) {
        const initFn = lib.findSymbolByName('OH_H264Decoder_Init');
        if (initFn) {
          hilog.info(0xFF00, TAG, "找到 OH_H264Decoder_Init");
          // 调用示例(实际应通过完整的 NAPI CBinding 使用)
          // this.decoderId = initFn(this.surfaceId);
        }
      }

      // 简化的直接导入方式(HarmonyOS NEXT 推荐)
      // import nativevideo from 'libentry.z.so'  // 编译期导入
      hilog.info(0xFF00, TAG, "解码器初始化完成, surfaceId: %{public}s", surfaceId);
      this.isInited = true;
      return 0;
    } catch (e) {
      hilog.error(0xFF00, TAG, "初始化失败: %{public}s", JSON.stringify(e));
      return -1;
    }
  }

  /**
   * 开始解码指定文件
   * @param filePath - 本地视频文件路径
   */
  start(filePath: string): number {
    if (!this.isInited) {
      hilog.error(0xFF00, TAG, "解码器未初始化");
      return -1;
    }

    hilog.info(0xFF00, TAG, "开始解码: %{public}s", filePath);
    // 调用: OH_H264Decoder_Start(this.decoderId, filePath)
    return 0;
  }

  /**
   * 停止解码
   */
  stop(): void {
    hilog.info(0xFF00, TAG, "停止解码");
    // 调用: OH_H264Decoder_Stop(this.decoderId)
  }

  /**
   * 释放解码器
   */
  release(): void {
    hilog.info(0xFF00, TAG, "释放解码器");
    // 调用: OH_H264Decoder_Release(this.decoderId)
    this.decoderId = -1;
    this.isInited = false;
  }
}

ArkTS 层通过上述封装隐藏了 NAPI 的复杂性,页面代码只需三步即可完成视频解码与渲染。


六、完整使用流程

把各部分串联起来,完整的播放流程如下:

// 完整的播放器页面实现
import hilog from '@ohos.hilog';
import { fileUri } from '@kit.CoreFileKit';

@Entry
@Component
struct VideoPlayerPage {
  @State private decoderState: string = "未初始化";
  private xComponentController: XComponentController = new XComponentController();
  private nativeDecoder: NativeDecoder = new NativeDecoder();

  aboutToAppear(): void {
    this.initDecoder();
  }

  private async initDecoder(): Promise<void> {
    hilog.info(0xFF00, "VideoPlayer", "=== 开始初始化 Native 解码器 ===");

    // 步骤1: 加载 Native 库
    await this.nativeDecoder.loadLibrary();

    // 步骤2: 获取 surfaceId 并初始化解码器
    // 注意:surfaceId 需在 XComponent onReady 回调中获取
    hilog.info(0xFF00, "VideoPlayer", "等待 XComponent 就绪...");
  }

  build() {
    Column() {
      Text("Native H.264 解码演示")
        .fontSize(22)
        .margin({ bottom: 16 })

      XComponent({
        id: 'native_video_surface',
        type: XComponentType.SURFACE,
        controller: this.xComponentController
      })
        .width('100%')
        .aspectRatio(16 / 9)
        .onReady((event) => {
          hilog.info(0xFF00, "VideoPlayer", "XComponent onReady, surfaceId: %{public}s",
                     event.surfaceId);
          // 步骤3: surfaceId 就绪后,初始化 Native 解码器
          const initRes = this.nativeDecoder.init(event.surfaceId);
          this.decoderState = initRes === 0 ? "就绪" : "初始化失败";
        })

      Text(`解码器状态: ${this.decoderState}`)
        .fontSize(14)
        .margin({ top: 12 })

      Row({ space: 16 }) {
        Button("播放 /data/test.mp4")
          .onClick(() => {
            const path = '/data/test.mp4';
            hilog.info(0xFF00, "VideoPlayer", "点击播放: %{public}s", path);
            const res = this.nativeDecoder.start(path);
            hilog.info(0xFF00, "VideoPlayer", "启动结果: %{public}d", res);
          })

        Button("停止")
          .onClick(() => {
            this.nativeDecoder.stop();
            hilog.info(0xFF00, "VideoPlayer", "解码已停止");
          })
      }
      .margin({ top: 20 })
    }
    .width('100%')
    .height('100%')
    .padding(16)
  }
}

整个流程可以归纳为四个步骤:加载 Native 库 → XComponent 获取 surfaceId → 调用 OH_H264Decoder_Init 初始化解码器与渲染器 → 调用 OH_H264Decoder_Start 开始解码。


七、性能优化与常见陷阱

在实现上述链路时,有几个常见的性能问题和调试难点值得特别说明。

第一,YUV 到 RGBA 的格式转换是最大的性能瓶颈。 硬件 H.264 解码器几乎总是输出 NV12/YV12 格式,而屏幕合成器通常需要 RGBA 输入。如果每帧都做 CPU 拷贝和格式转换,1080p@30fps 的数据量约为 1920×1080×1.5×30 ≈ 93 MB/s,这个带宽在低端设备上很容易成为瓶颈。推荐方案是使用 libyuv 的 NEON 优化函数(NV12ToRGBA),或者直接在 EGL/OpenGL ES 中使用纹理上传(GL_LUMINANCE + shader 转换),将转换开销 GPU 化。

第二,Surface 渲染模式下的双缓冲机制。 NativeWindow 默认维护两块缓冲区,交替使用。如果解码速度快于屏幕刷新率,OH_NativeWindow_NativeWindowFlushBuffer 会阻塞;当解码速度不足时,画面就会出现卡顿。解决方案是使用三缓冲(设置 bufferCount = 3),以及在解码线程中使用独立的时间戳控制送帧节奏。

第三,解码器回调的线程安全。 OnCodecOutputDataReady 回调发生在解码器内部的独立线程中,不能在这个线程里直接调用 NAPI 方法。所有帧数据必须通过线程安全队列传递到解码线程(或渲染线程),再由主线程通过 NAPI 回调分发给 ArkTS 层。如果需要从 ArkTS 线程安全地调用 Native 方法,请使用 napi_threadsafe_function API,它可以将回调排队到主线程执行。

第四,文件描述符的生命周期。 AVDemuxer 使用文件 fd 工作,但文件描述符在多线程环境下容易泄露。建议使用 RAII 封装 fd,并在解码器析构函数中确保 close 被调用。上文代码中 H264Decoder 的析构函数已经处理了这一点。


八、总结

本文完整实现了一条从文件到屏幕的 Native 视频解码链路:借助 AVDemuxer 读取 H.264 码流,通过 AVCodec 解码为原始帧数据,用 NativeWindow 直接渲染到 XComponent 的 Surface,最后通过 NAPI 将状态回传到 ArkUI。

整个方案的核心优势在于:所有像素处理和渲染逻辑都在 Native 层执行,绕过了 ArkUI 的 Media 框架约束。如果你需要对视频帧做实时滤镜、人脸识别叠加、AR 效果增强,这条链路是必经之路。

在此基础上,可以进一步探索的方向包括:引入 EGL/OpenGL ES 实现 GPU 滤镜、将 libyuv 的 NEON 优化集成进来提升格式转换效率、使用 napi_threadsafe_function 实现 ArkTS 侧帧回调、以及对接硬件编码器(AVCodec Encode)实现边解码边推流。


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐