鸿蒙原生开发实战:用 Native CodecBase 解码 H.264 并通过 XComponent 渲染

前言
在前几期文章中,我们依次探讨了 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)实现边解码边推流。
更多推荐


所有评论(0)