鸿蒙5.0实战案例:基于AVCodecKit的音视频解码及二次处理播放

news/2025/2/24 14:57:01

往期推文全新看点(文中附带全新鸿蒙5.0全栈学习笔录)

✏️ 鸿蒙(HarmonyOS)北向开发知识点记录~

✏️ 鸿蒙(OpenHarmony)南向开发保姆级知识点汇总~

✏️ 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?

✏️ 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~

✏️ 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?

✏️ 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?

✏️ 记录一场鸿蒙开发岗位面试经历~

✏️ 持续更新中……


1:场景描述

场景:基于VideoCoder的音视频解码及二次处理播放。

首先导入选择器picker模块,使用PhotoViewPicker方法拉起图库选择视频文件,将视频文件传递到native侧使用Demuxer解封装器进行解封装,再使用OH_VideoDecoder进行解码(surface模式)送显播放

使用的核心API:

  • picker :提供拉起图库选择视频的功能接口。
  • AVDemuxer :音视频解封装,用于获取视频等媒体帧数据。
  • VideoDecoder:视频解码,将视频数据解码后送显播放。

2:方案描述

Step1:导入picker模块(仅代表选择一个视频路径,还有其它获取媒体文件的方式), 拉起图库选择视频文件保存到自定义路径。

Step2:将文件传递到native侧进行交互。

Step3:使用AVDemuxer接口对文件进行解封装获取视频流数据。

Step4:使用VideoDecoder接口将视频数据解码,结合Xcomponent送显播放。

效果图如下:

具体步骤如下:

步骤一:导入picker模块, 拉起图库选择视频文件自定义保存。

import { picker } from '@kit.CoreFileKit';
let photoSelectOptions = new picker.PhotoSelectOptions();
photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.VIDEO_TYPE;
photoSelectOptions.maxSelectNumber = 1;
let photoPicker = new picker.PhotoViewPicker();
photoPicker.select(photoSelectOptions).then((PhotoSelectResult: picker.PhotoSelectResult) => {
  hilog.info(0x0000, TAG, 'PhotoViewPicker.select successfully, PhotoSelectResult uri: ' + JSON.stringify(PhotoSelectResult));
  this.selectFilePath = PhotoSelectResult.photoUris[0];
  hilog.info(0x0000, TAG, 'Get selectFilePath successfully: ' + JSON.stringify(this.selectFilePath));
}).catch((err: BusinessError) => {
  hilog.error(0x0000, TAG, 'PhotoViewPicker.select failed with err: ' + JSON.stringify(err));
})
}

步骤二:将文件传递到native侧进行交互。

import player from 'libplayer.so';
export const playNative: (
  inputFileFd: number,
  inputFileOffset: number,
  inputFileSize: number,
  cbFn: () => void
) => void;
static napi_value Init(napi_env env, napi_value exports) {
  napi_property_descriptor classProp[] = {
    {"playNative", nullptr, Play, nullptr, nullptr, nullptr, napi_default, nullptr},
};
napi_value PlayerNative = nullptr;
const char *classBindName = "playerNative";
napi_define_class(env, classBindName, strlen(classBindName), nullptr, nullptr, 1, classProp, &PlayerNative);
PluginManager::GetInstance()->Export(env, exports);
napi_define_properties(env, exports, sizeof(classProp) / sizeof(classProp[0]), classProp);
return exports;
}

步骤三:使用Demuxer接口对文件进行解封装获取视频流数据。

Step1:创建解封装器,传入媒体文件格式信息。

int32_t Demuxer::CreateDemuxer(SampleInfo &info) {
  source = OH_AVSource_CreateWithFD(info.inputFd, info.inputFileOffset, info.inputFileSize);
  demuxer = OH_AVDemuxer_CreateWithSource(source);
  auto sourceFormat = std::shared_ptr<OH_AVFormat>(OH_AVSource_GetSourceFormat(source), OH_AVFormat_Destroy);
  int32_t ret = GetTrackInfo(sourceFormat, info);
  return AV_ERR_OK;
}

Step2:添加解封装轨道,获取文件轨道信息。

int32_t Demuxer::GetTrackInfo(std::shared_ptr<OH_AVFormat> sourceFormat, SampleInfo &info) {
  int32_t trackCount = 0;
  OH_AVFormat_GetIntValue(sourceFormat.get(), OH_MD_KEY_TRACK_COUNT, &trackCount);
  for (int32_t index = 0; index < trackCount; index++) {
    int trackType = -1;
    auto trackFormat = std::shared_ptr<OH_AVFormat>(OH_AVSource_GetTrackFormat(source, index), OH_AVFormat_Destroy);
    OH_AVFormat_GetIntValue(trackFormat.get(), OH_MD_KEY_TRACK_TYPE, &trackType);
    if (trackType == MEDIA_TYPE_VID) {
      OH_AVDemuxer_SelectTrackByID(demuxer, index);
      OH_AVFormat_GetIntValue(trackFormat.get(), OH_MD_KEY_WIDTH, &info.videoWidth);
      OH_AVFormat_GetIntValue(trackFormat.get(), OH_MD_KEY_HEIGHT, &info.videoHeight);
      OH_AVFormat_GetDoubleValue(trackFormat.get(), OH_MD_KEY_FRAME_RATE, &info.frameRate);
      OH_AVFormat_GetLongValue(trackFormat.get(), OH_MD_KEY_BITRATE, &info.bitrate);
      OH_AVFormat_GetIntValue(trackFormat.get(), "video_is_hdr_vivid", &info.isHDRVivid);
      OH_AVFormat_GetIntValue(trackFormat.get(), OH_MD_KEY_ROTATION, &info.rotation);
      char *codecMime;
      OH_AVFormat_GetStringValue(trackFormat.get(), OH_MD_KEY_CODEC_MIME, const_cast<char const **>(&codecMime));
      info.codecMime = codecMime;
      OH_AVFormat_GetIntValue(trackFormat.get(), OH_MD_KEY_PROFILE, &info.hevcProfile);
      videoTrackId_ = index;
      OH_LOG_ERROR(LOG_APP, "Demuxer config: %{public}d*%{public}d, %{public}.1ffps, %{public}ld" "kbps",
      info.videoWidth, info.videoHeight, info.frameRate, info.bitrate / 1024);
    }
  }
  return AV_ERR_OK;
}

Step3:开始解封装,循环获取视频帧数据。

int32_t Demuxer::ReadSample(OH_AVBuffer *buffer, OH_AVCodecBufferAttr &attr) {
  int32_t ret = OH_AVDemuxer_ReadSampleBuffer(demuxer, videoTrackId_, buffer);
  ret = OH_AVBuffer_GetBufferAttr(buffer, &attr);
  return AV_ERR_OK;
}

解封装支持的文件格式:

步骤四:使用VideoDecoder接口将视频数据解码,结合Xcomponent送显播放。

Step1:将解封装后的数据送去解码器进行解码

void Player::DecInputThread() {
  while (true) {
        std::unique_lock<std::mutex> lock(signal->inputMutex_);
        bool condRet = signal->inputCond_.wait_for(
            lock, 5s, [this]() { return !isStarted_ || !signal->inputBufferInfoQueue_.empty(); });
  if (!isStarted_) {
    OH_LOG_ERROR(LOG_APP, "Work done, thread out");
    break;
  }
  if (signal->inputBufferInfoQueue_.empty()) {
    OH_LOG_ERROR(LOG_APP, "Buffer queue is empty, continue, cond ret: %{public}d", condRet);
  }
  CodecBufferInfo bufferInfo = signal->inputBufferInfoQueue_.front();
  signal->inputBufferInfoQueue_.pop();
  signal->inputFrameCount_++;
  lock.unlock();
  demuxer_->ReadSample(reinterpret_cast<OH_AVBuffer *>(bufferInfo.buffer), bufferInfo.attr);
  int32_t ret = videoDecoder_->PushInputData(bufferInfo);
}
StartRelease();
}

Step2:获取解码后的数据

void Player::DecOutputThread() {
  sampleInfo_.frameInterval = MICROSECOND / sampleInfo_.frameRate;
  while (true) {
        thread_local auto lastPushTime = std::chrono::system_clock::now();
        if (!isStarted_) {
            OH_LOG_ERROR(LOG_APP, "Decoder output thread out");
            break;
        }
        std::unique_lock<std::mutex> lock(signal->outputMutex_);
        bool condRet = signal->outputCond_.wait_for(
            lock, 5s, [this]() { return !isStarted_ || !signal->outputBufferInfoQueue_.empty(); });
  if (!isStarted_) {
    OH_LOG_ERROR(LOG_APP, "Decoder output thread out");
    break;
  }
  if (signal->outputBufferInfoQueue_.empty()) {
    OH_LOG_ERROR(LOG_APP, "Buffer queue is empty, continue, cond ret: %{public}d", condRet);
  }
  CodecBufferInfo bufferInfo = signal->outputBufferInfoQueue_.front();
  signal->outputBufferInfoQueue_.pop();
  if (bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_EOS) {
    OH_LOG_ERROR(LOG_APP, "Catch EOS, thread out");
    break;
  }
  signal->outputFrameCount_++;
  OH_LOG_ERROR(LOG_APP, "Out buffer count: %{public}u, size: %{public}d, flag: %{public}u, pts: %{public}ld",
    signal->outputFrameCount_, bufferInfo.attr.size, bufferInfo.attr.flags, bufferInfo.attr.pts);
  lock.unlock();
  int32_t ret = videoDecoder_->FreeOutputData(bufferInfo.bufferIndex, true);
  if (ret != AV_ERR_OK) {
    OH_LOG_ERROR(LOG_APP, "Decoder output thread out");
    break;
  }
  std::this_thread::sleep_until(lastPushTime + std::chrono::microseconds(sampleInfo_.frameInterval));
  lastPushTime = std::chrono::system_clock::now();
}
OH_LOG_ERROR(LOG_APP, "Exit, frame count: %{public}u", signal->outputFrameCount_);
StartRelease();
}

Step3:使用OH_VideoDecoder_SetSurface设置surface数据和window绑定

int32_t VideoDecoder::Config(const SampleInfo &sampleInfo, VDecSignal *signal) {
  // Configure video decoder
  int32_t ret = ConfigureVideoDecoder(sampleInfo);
  // SetSurface from video decoder
  if (sampleInfo.window != nullptr) {
    int ret = OH_VideoDecoder_SetSurface(decoder, sampleInfo.window);
    if (ret != AV_ERR_OK || sampleInfo.window == nullptr) {
      OH_LOG_ERROR(LOG_APP, "Set surface failed, ret: %{public}d", ret);
      return AV_ERR_UNKNOWN;
    }
  }
  // SetCallback for video decoder
  ret = SetCallback(signal);
  if (ret != AV_ERR_OK) {
    OH_LOG_ERROR(LOG_APP, "Set callback failed, ret: %{public}d", ret);
    return AV_ERR_UNKNOWN;
  }
  // Prepare video decoder
  {
    int ret = OH_VideoDecoder_Prepare(decoder);
    if (ret != AV_ERR_OK) {
      OH_LOG_ERROR(LOG_APP, "Prepare failed, ret: %{public}d", ret);
      return AV_ERR_UNKNOWN;
    }
  }
  return AV_ERR_OK;
}

Step4: native层获取 NativeXComponent

void PluginManager::Export(napi_env env, napi_value exports) {
  napi_value exportInstance = nullptr;
  if (napi_get_named_property(env, exports, OH_NATIVE_XCOMPONENT_OBJ, &exportInstance) != napi_ok) {
    OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, "PluginManager", "Export: napi_get_named_property fail");
    return;
  }
  OH_NativeXComponent *nativeXComponent = nullptr;
  if (napi_unwrap(env, exportInstance, reinterpret_cast<void **>(&nativeXComponent)) != napi_ok) {
    OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, "PluginManager", "Export: napi_unwrap fail");
    return;
  }
  char idStr[OH_XCOMPONENT_ID_LEN_MAX + 1] = {'\0'};
  uint64_t idSize = OH_XCOMPONENT_ID_LEN_MAX + 1;
  if (OH_NativeXComponent_GetXComponentId(nativeXComponent, idStr, &idSize) != OH_NATIVEXCOMPONENT_RESULT_SUCCESS) {
    OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, "PluginManager", "Export: OH_NativeXComponent_GetXComponentId fail");
    return;
  }
  std::string id(idStr);
  auto context = PluginManager::GetInstance();
  if ((context != nullptr) && (nativeXComponent != nullptr)) {
    context->SetNativeXComponent(id, nativeXComponent);
    auto render = context->GetRender(id);
    OH_NativeXComponent_RegisterCallback(nativeXComponent, &PluginRender::m_callback);
  }
}

step5:通过回调将window渲染播放

void OnSurfaceCreatedCB(OH_NativeXComponent *component, void *window) {
  OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, "Callback", "OnSurfaceCreatedCB");
  auto context = PluginManager::GetInstance();
  context->m_window = (OHNativeWindow *)window;
}

http://www.niftyadmin.cn/n/5864492.html

相关文章

springcloud gateway并发量多大

Spring Cloud Gateway的并发量并非固定值&#xff0c;它受到多种因素的影响&#xff0c;包括但不限于网关配置、硬件资源&#xff08;如CPU、内存、网络带宽等&#xff09;、后端服务的处理能力以及系统整体的架构设计。因此&#xff0c;要准确回答Spring Cloud Gateway的并发量…

【Linux基础】Shell脚本

文章目录 一、前言二、Linux脚本编写基础2.1 文件开头2.2 注释2.3 变量2.3.1 系统变量2.3.2 环境变量2.3.3 用户环境变量 2.4 注意事项 三、shell脚本中常用的三类命令3.1 Linux命令3.2 管道、重定向和命令置换3.2.1 管道3.2.2 重定向3.2.3 命令置换 四、流程控制4.1 说明性语句…

软件需求类的论文无法量化评价的问题

软件需求研究的量化难题确实是一个普遍存在的挑战&#xff0c;主要原因在于需求工程本身具有强主观性、领域依赖性和过程复杂性。针对这一问题&#xff0c;可以从以下角度进行突破性思考并提出解决方案&#xff1a; 1. 构建多维度评估体系&#xff08;Multi-dimensional Evalu…

《Linux命令行和shell脚本编程大全》第一章阅读笔记

一.认识Linux Linux系统可以划分为四个部分 Linux内核GNU工具图形化桌面环境应用软件 1.Linux内核 主要功能有 系统内存管理软件程序管理硬件设备管理文件系统管理 &#xff08;1&#xff09;系统内存管理 内核管理可用物理内存&#xff0c;还可以创建并管理虚拟内存。内…

本地部署AI模型 --- DeepSeek(二)---更新中

目录 FAQ 1.Failed to load the model Exit code: 18446744072635812000 FAQ 1.Failed to load the model Exit code: 18446744072635812000 问题描述&#xff1a; &#x1f972; Failed to load the model Error loading model. (Exit code: 18446744072635812000). Unkn…

react路由总结

目录 一、脚手架基础语法(16~17) 1.1、hello react 1.2、组件样式隔离(样式模块化) 1.3、react插件 二、React Router v5 2.1、react-router-dom相关API 2.1.1、内置组件 2.1.1.1、BrowserRouter 2.1.1.2、HashRouter 2.1.1.3、Route 2.1.1.4、Redirect 2.1.1.5、L…

详解 @符号在 PyTorch 中的矩阵乘法规则

详解 符号在 PyTorch 中的矩阵乘法规则 在 PyTorch 和 NumPy 中&#xff0c; 符号被用作矩阵乘法运算符&#xff0c;它本质上等价于 torch.matmul() 或 numpy.matmul()&#xff0c;用于执行张量之间的矩阵乘法。 在本篇博客中&#xff0c;我们将深入探讨&#xff1a; 运算符…

Kafka系列之:记录一次源头数据库刷数据,造成数据丢失的原因

Kafka系列之:记录一次源头数据库刷数据,造成数据丢失的原因 一、背景二、查看topic日志信息三、结论四、解决方法一、背景 源头数据库在很短的时间内刷了大量的数据,部分数据在hdfs丢失了 理论上debezium数据采集不会丢失,就需要排查数据链路某个节点是否有数据丢失。 数据…