HarmonyNext:如何在鸿蒙上录屏后进行音视频编码

前言

最近在做鸿蒙原生录屏功能,想想把踩过的坑及现有的官方示例缺少的音频编码部分串起来后给大家分享一下,希望尽量帮助大家减少踩坑的时间。

官方示例参考链接:gitee.com/harmonyos_s...

本文主要是简单讲解如何在鸿蒙原生应用开发中,对手机进行录屏后并使用编码库对录屏输出buffer进行编码。

在开始之前,首先帮助大家梳理自己需要关注的什么,在后续的文章中也会说明并贴出关键代码:

  1. 录屏输出:录屏后音频及视频buffer如何输出;
  2. 编码器输入:对于音视频编码器,音频和视频buffer分别如何输入至其编码器;
  3. 编码器输出:编码器如何输出编码后buffer。

对于录屏、音视频编码,官方文档中都有相应示例,因此过程只需要copy文档,做好封装,衔接好输入输出便可快速完成录屏后音视频编码整个流程,事半功倍。可能部分小白不清楚音视频编码是为了什么,在这里简单举个例子说一下,博主曾尝试不编码直接将录屏原始buffer写入视频文件,仅仅录制3秒就达到了1G以上的容量,而编码后录制近1分钟也仅10MB以内的大小,因此,音视频编码最直观的目的就是为了在存储或传输的过程中将音视频容量极大的缩小以提高传输效率和减少占用空间。

如果你有以下经验,那么你将更快掌握:

  1. 了解鸿蒙开发基本开发流程。
  2. 了解NAPI,掌握一定C++语法。

使用场景

本案例的场景是投屏功能,需要将录屏后获取到的音频和视频buffer编码后分别发送到对端,因此无需使用复用器做音视频同轨保存文件,因此该示例结合官方示例做裁剪、修改补充,暂只说明录屏、音频编码、视频编码实现方式,不涉及编码后发送buffer逻辑,基于本示例可帮助应用开发需要进行录屏、音视频编码场景,音视频解码后播放场景后续补充。

开发环境

  • IDE:DevEco Studio 5.0.2 Release
  • SDK:5.0.2(14)

开发示例

工程结构 复制代码
├──entry/src/main/cpp                 // Native层
│  ├──capbilities                         // 能力接口和实现
│  │  ├──include                          // 能力接口
│  │  ├──AudioEncoder.cpp           // 音频编码实现
│  │  └──VideoEncoder.cpp           // 视频编码实现
│  ├──common                            // 公共模块
│  │  ├──dfx                               // 日志工具
│  │  ├──SampleCallback.cpp         // 编解码回调实现   
│  │  ├──SampleCallback.h            // 编解码回调定义
│  │  └──SampleInfo.h                  // 功能实现公共类  
│  ├──recorder                            // Native层录制接口和实现
│  │  ├──Recorder.cpp                  // Native层录制功能调用逻辑的实现
│  │  ├──Recorder.h                     // Native层录制功能调用逻辑的接口
│  │  ├──RecorderNative.cpp        // Native层 录制的入口
│  │  └──RecorderNative.h       
│  ├──types                                // Native层暴露上来的接口
│  │  └──librecorder                    // 录制模块暴露给UI层的接口
│  └──CMakeLists.txt                   // 编译入口       
├──ets                                       // UI层
│  ├──entryability                       // 应用的入口
│  │  └──EntryAbility.ets                
│  └──pages                               // EntryAbility 包含的页面
│     ├──Index.ets                        // 首页/调试页面
└──module.json5                         // 模块配置信息

代码调用逻辑:Index-> librecorder/Index.d.ts-> RecorderNatice.cpp-> Recorder.cpp-> AudioEncoder.cpp/VideoEncoder.cpp

具体实现

UI层

  1. UI只简单写启动录屏及停止录屏两个按钮,用于native层调试录屏;
  2. 点击启动录屏,首先申请后台长时任务(涉及内录音频则需申请,若不申请,仍可正常单独录制视频),申请成功后调用native层启动录屏;
  3. 在录制一段时间后,点击停止录屏,则停止并释放相关资源。

Natice层

  1. 在点击启动录屏后,首先对录屏及音视频编码器进行配置初始化,并创建音视频输出文件,在初始化完成后调用启动接口,启动录屏,与此同时启动音视频编码器;
  2. 编码器每编码成功一帧,sample_callback.cpp的输出回调OnNewOutputBuffer()就会调起一次,此时用户可以拿到AVCodec框架给出的OH_AVBuffer;
  3. 在输出回调中,用户需手动把帧buffer、index存入输出队列中,并通知输出线程解锁;
  4. 在输出线程中,把上一步的帧信息储存为bufferInfo后,pop出队;
  5. 在输出线程中,使用上一步的bufferInfo,调用写入音视频文件接口,将每一帧被分别写入h264(视频)和acc(音频)文件中;
  6. 最后调用FreeOutputBuffer接口后,这一帧buffer释放回AVCodec框架,实现buffer轮转。

相关权限

  1. 允许Service Ability在后台持续运行:ohos.permission.KEEP_BACKGROUND_RUNNING

长时任务申请

涉及采集麦克风音频数据或内录音频则需申请,若不申请将应用切到后台则会自动停止录屏

申请权限

system_grant的权限只需配置,无需代码手动申请

ArkTs 复制代码
// module.json5
"requestPermissions": [
  {
    "name" : "ohos.permission.KEEP_BACKGROUND_RUNNING"
  }
]
Ability配置
ArkTs 复制代码
// module.json5
"abilities": [
 {
   "name": "EntryAbility",
    ...

   "backgroundModes": [
      "audioRecording"
   ]
 }
]
启动长时任务
ArkTs 复制代码
// Index.ets
async startContinuousTask(callback: VoidCallback) {
  let wantAgentInfo: wantAgent.WantAgentInfo = {
    // 点击通知后,将要执行的动作列表
    // 添加需要被拉起应用的bundleName和abilityName
    wants: [
      {
        bundleName: "com.example.recorderavcodedemo",
        abilityName: "EntryAbility"
      }
    ],
    // 指定点击通知栏消息后的动作是拉起ability
    actionType: wantAgent.OperationType.START_ABILITY,
    // 使用者自定义的一个私有值
    requestCode: 0,
    // 点击通知后,动作执行属性
    actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
  };

  // 通过wantAgent模块下getWantAgent方法获取WantAgent对象
  wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj: WantAgent) => {
    backgroundTaskManager.startBackgroundRunning(this.context,
      backgroundTaskManager.BackgroundMode.AUDIO_RECORDING, wantAgentObj).then(() => {
      // 此处执行具体的长时任务逻辑,如放音等。
      console.info(`Succeeded in operationing startBackgroundRunning.`);
      callback();
    }).catch((err: Error) => {
      console.error(`Failed to operation startBackgroundRunning. name is ${err.name}, message is ${err.message}`);
    });
  });
}
停止长时任务
ArkTs 复制代码
// Index.ets
stopContinuousTask() {
  backgroundTaskManager.stopBackgroundRunning(this.context).then(() => {
    console.info(`Succeeded in operationing stopBackgroundRunning.`);
  }).catch((err: Error) => {
    console.error(`Failed to operation stopBackgroundRunning. name is ${err.name}, message is ${err.message}`);
  });
}

录屏

对于录屏,官方文档中存在三种方式

  1. 应用层启动录屏:需要主动创建文件,传入文件fd,无需自行编码;
  2. Native层启动录屏方式一(录屏写文件):需要主动创建文件,传入文件fd,需要自行编码;
  3. Native层启动录屏方式二(录屏取码流):不需要传入文件fd,自行创建文件使用接口写入buffer,需要自行编码。

由于投屏需求需要将编码后buffer传输到对端,因此这里使用第三种方式,请根据场景自行选择录屏方式。

Native层录屏与视频编码都存在两种模式,buffer模式与surface模式,这里简单说明两者模式的区别:

  1. Buffer模式:在这种模式下,录屏的数据会直接保存到内存中的缓冲区(buffer)。这种方式适合于需要对录制的数据进行进一步处理的情况,比如实时分析、编辑等
  2. Surface模式:这种模式下,录屏的数据会被渲染到一个特殊的Surface上,然后通过这个Surface进行后续的操作,如编码输出视频文件。这种方式更适合于直接生成视频文件或者流媒体传输的应用场景

选择哪种模式主要考虑以下几点:

  1. 如果你需要对录制的内容进行复杂的后期处理或实时分析,那么Buffer模式可能更合适。
  2. 如果你只是想简单地将屏幕内容录制下来并保存为视频文件,或直接传输,那么Surface模式可能更加方便快捷。

因此,基于以上考量,这里选择使用surface模式启动录屏及编码

官方示例:使用AVScreenCapture录屏取码流(C/C++)-录屏

链接so库
C++ 复制代码
target_link_libraries(recorder PUBLIC libnative_avscreen_capture.so libnative_buffer.so libnative_media_core.so)
添加头文件
C++ 复制代码
#include "napi/native_api.h"
#include <multimedia/player_framework/native_avscreen_capture.h>
#include <multimedia/player_framework/native_avscreen_capture_base.h>
#include <multimedia/player_framework/native_avscreen_capture_errors.h>
#include <native_buffer/native_buffer.h>
创建AVScreenCapture实例capture
C++ 复制代码
OH_AVScreenCapture* capture = OH_AVScreenCapture_Create();
回调注册
C++ 复制代码
OH_AVScreenCapture_SetErrorCallback(capture, SampleCallback::OnError, nullptr);
OH_AVScreenCapture_SetStateCallback(capture, SampleCallback::OnStateChange, nullptr);
OH_AVScreenCapture_SetDataCallback(capture, Recorder::OnBufferAvailable, nullptr);
添加过滤器(可选)
C++ 复制代码
// [过滤音频]
OH_AVScreenCapture_ContentFilter *contentFilter= OH_AVScreenCapture_CreateContentFilter();
// 添加过滤通知音
OH_AVScreenCapture_ContentFilter_AddAudioContent(contentFilter, OH_SCREEN_CAPTURE_NOTIFICATION_AUDIO);
// 排除指定窗口id
OH_AVScreenCapture_ContentFilter_AddWindowContent(contentFilter, windowIDs, windowCount);
OH_AVScreenCapture_ExcludeContent(capture, contentFilter);
录屏配置
C++ 复制代码
OH_AudioCaptureInfo miccapinfo = {.audioSampleRate = sampleInfo.audioSampleRate, .audioChannels = sampleInfo.audioChannelCount, .audioSource = OH_ALL_PLAYBACK};
OH_VideoCaptureInfo videocapinfo = {.videoFrameWidth = sampleInfo.videoWidth, .videoFrameHeight = sampleInfo.videoHeight, .videoSource = OH_VIDEO_SOURCE_SURFACE_RGBA};
OH_AudioInfo audioinfo = {.micCapInfo = miccapinfo};
OH_VideoInfo videoinfo = {.videoCapInfo = videocapinfo};
OH_AVScreenCaptureConfig config = {.captureMode = OH_CAPTURE_HOME_SCREEN,
                                   .dataType = OH_ORIGINAL_STREAM,
                                   .audioInfo = audioinfo,
                                   .videoInfo = videoinfo};
初始化
C++ 复制代码
OH_AVScreenCapture_Init(capture, config);
surface模式启动录屏
C++ 复制代码
OH_AVScreenCapture_StartScreenCaptureWithSurface(capture, sampleInfo_.window); 
录屏回调

涉及音频编码输入

C++ 复制代码
void Recorder::OnBufferAvailable(OH_AVScreenCapture *capture, OH_AVBuffer *buffer, OH_AVScreenCaptureBufferType bufferType, int64_t timestamp, void *userData) {
    int bufferLen = OH_AVBuffer_GetCapacity(buffer);
    if (bufferType == OH_SCREEN_CAPTURE_BUFFERTYPE_VIDEO) {
        // 处理视频buffer(此处由于surface模式录屏,与编码器绑定同一surface,因此无需自行处理)
        COMMON_LOGI("==============OnBufferAvailable:处理视频buffer===============");
        COMMON_LOGI("OnBufferAvailable:buffer=%{public}d", bufferLen);
    } else if (bufferType == OH_SCREEN_CAPTURE_BUFFERTYPE_AUDIO_INNER) {
        // 处理内录buffer
        COMMON_LOGI("==============OnBufferAvailable:处理内录buffer===============");
        COMMON_LOGI("OnBufferAvailable:bufferLen=%{public}d", bufferLen);
        CodecBufferInfo bufferInfo = audioEncContext_->inputBufferInfoQueue.front();
        audioEncContext_->inputBufferInfoQueue.pop();
        audioEncContext_->inputFrameCount++;
        memcpy(OH_AVBuffer_GetAddr(reinterpret_cast<OH_AVBuffer *>(bufferInfo.buffer)), OH_AVBuffer_GetAddr(buffer), bufferLen);
        bufferInfo.attr.size = bufferLen;
        COMMON_LOGI("OnBufferAvailable INNER bufferIndex = %{public}d", bufferInfo.bufferIndex);
        audioEncoder_->PushInputBuffer(bufferInfo);
    } else if (bufferType == OH_SCREEN_CAPTURE_BUFFERTYPE_AUDIO_MIC) {
        // 处理麦克风buffer(未开启麦克风,无该类型buffer)
    }
}
停止录屏
C++ 复制代码
// 结束录屏
OH_AVScreenCapture_StopScreenCapture(capture);
OH_AVScreenCapture_Release(capture);

视频编码

官方示例:视频编码(surface模式)-音视频编解码-AVCodec Kit(音视频编解码服务)

视频编码调用关系图:

链接so库
C++ 复制代码
target_link_libraries(recorder PUBLIC libnative_media_codecbase.so)
target_link_libraries(recorder PUBLIC libnative_media_core.so)
target_link_libraries(recorder PUBLIC libnative_media_venc.so)
添加头文件
C++ 复制代码
#include <multimedia/player_framework/native_avcodec_videoencoder.h>
#include <multimedia/player_framework/native_avcapability.h>
#include <multimedia/player_framework/native_avcodec_base.h>
#include <multimedia/player_framework/native_avformat.h>
#include <multimedia/player_framework/native_avbuffer.h>
创建视频编码器
C++ 复制代码
// VideoEncoder.cpp
const char * videoCodecMime = OH_AVCODEC_MIMETYPE_VIDEO_AVC;
int32_t VideoEncoder::Create(const char * videoCodecMime)
{
    encoder_ = OH_VideoEncoder_CreateByMime(videoCodecMime);
    COMMON_LOGI("VideoEncoder On Create CreateByMime = %{public}s", videoCodecMime);
    CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, COMMON_ERR_ERROR, "Create failed");
    return COMMON_ERR_OK;
}
编码器配置
C++ 复制代码
// VideoEncoder.cpp
int32_t VideoEncoder::Configure(const SampleInfo &sampleInfo)
{
    OH_AVFormat *format = OH_AVFormat_Create();
    CHECK_AND_RETURN_RET_LOG(format != nullptr, COMMON_ERR_ERROR, "AVFormat create failed");

![image.png](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a4082fab52f64235b28af8a1c4384a81~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YyX5rW3eng=:q75.awebp?rk3s=f64ab15b&x-expires=1746509319&x-signature=QanDoUU262cfXBZ%2FCOSkzCNY%2Bhg%3D)
    OH_AVFormat_SetIntValue(format, OH_MD_KEY_WIDTH, sampleInfo.videoWidth);
    OH_AVFormat_SetIntValue(format, OH_MD_KEY_HEIGHT, sampleInfo.videoHeight);
    OH_AVFormat_SetDoubleValue(format, OH_MD_KEY_FRAME_RATE, sampleInfo.frameRate);
    OH_AVFormat_SetIntValue(format, OH_MD_KEY_PIXEL_FORMAT, sampleInfo.pixelFormat);
    OH_AVFormat_SetIntValue(format, OH_MD_KEY_VIDEO_ENCODE_BITRATE_MODE, sampleInfo.bitrateMode);
    OH_AVFormat_SetLongValue(format, OH_MD_KEY_BITRATE, sampleInfo.bitrate);
    OH_AVFormat_SetIntValue(format, OH_MD_KEY_PROFILE, sampleInfo.hevcProfile);
    if (sampleInfo.isHDRVivid) {
        OH_AVFormat_SetIntValue(format, OH_MD_KEY_I_FRAME_INTERVAL, sampleInfo.iFrameInterval);
        OH_AVFormat_SetIntValue(format, OH_MD_KEY_RANGE_FLAG, sampleInfo.rangFlag);
        OH_AVFormat_SetIntValue(format, OH_MD_KEY_COLOR_PRIMARIES, sampleInfo.primary);
        OH_AVFormat_SetIntValue(format, OH_MD_KEY_TRANSFER_CHARACTERISTICS, sampleInfo.transfer);
        OH_AVFormat_SetIntValue(format, OH_MD_KEY_MATRIX_COEFFICIENTS, sampleInfo.matrix);
    }
    COMMON_LOGI("====== VideoEncoder config ======");
    COMMON_LOGI("%{public}d*%{public}d, %{public}.1ffps",
        sampleInfo.videoWidth, sampleInfo.videoHeight, sampleInfo.frameRate);
    // 1024: ratio of kbps to bps
    COMMON_LOGI("BitRate Mode: %{public}d, BitRate: %{public}ld" "kbps",
        sampleInfo.bitrateMode, sampleInfo.bitrate / 1024);
    COMMON_LOGI("====== VideoEncoder config ======");

    int ret = OH_VideoEncoder_Configure(encoder_, format);
    CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, COMMON_ERR_ERROR, "Config failed, ret: %{public}d", ret);
    OH_AVFormat_Destroy(format);
    format = nullptr;
    return COMMON_ERR_OK;
获取需要输入的surface

此处获取的surface传入启动录屏的接口中,可理解为绑定同一surface,主动消费录屏输出的视频buffer

C++ 复制代码
// VideoEncoder.cpp
int32_t VideoEncoder::GetSurface(SampleInfo &sampleInfo)
{
    int32_t ret = OH_VideoEncoder_GetSurface(encoder_, &sampleInfo.window);
    CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK && sampleInfo.window, COMMON_ERR_ERROR,
        "Get surface failed, ret: %{public}d", ret);
    return COMMON_ERR_OK;
}
回调注册

OnNeedInputBuffer可理解为用来准备编码空间,写入需要编码的buffer队列中,等待输入;

OnNewOutputBuffer则需要处理已经编码完成的数据,写入编码后的buffer队列中,等待输出;

CodecUserData做为上下文对象,在整个编码过程中用来做数据传递;

C++ 复制代码
// VideoEncoder.cpp
int32_t VideoEncoder::SetCallback(CodecUserData *codecUserData)
{
    int32_t ret = OH_VideoEncoder_RegisterCallback(encoder_,
    {SampleCallback::OnCodecError, SampleCallback::OnCodecFormatChange,
        SampleCallback::OnNeedInputBuffer, SampleCallback::OnNewOutputBuffer},
        codecUserData);
    CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, COMMON_ERR_ERROR, "Set callback failed, ret: %{public}d", ret);

    return COMMON_ERR_OK;
}

// SampleCallback.cpp
void SampleCallback::OnNeedInputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData) {
    COMMON_LOGI("OnNeedInputBuffer begin");
    if (userData == nullptr) {
        return;
    }
    (void)codec;
    CodecUserData *codecUserData = static_cast<CodecUserData *>(userData);
    COMMON_LOGI("OnNeedInputBuffer =%{public}p", codecUserData);
    std::unique_lock<std::mutex> lock(codecUserData->inputMutex);
    codecUserData->inputBufferInfoQueue.emplace(index, buffer);
    codecUserData->inputCond.notify_all();
}

void SampleCallback::OnNewOutputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData) {
    COMMON_LOGI("OnNewOutputBuffer begin");
    if (userData == nullptr) {
        return;
    }
    (void)codec;
    CodecUserData *codecUserData = static_cast<CodecUserData *>(userData);
    std::unique_lock<std::mutex> lock(codecUserData->outputMutex);
    codecUserData->outputBufferInfoQueue.emplace(index, buffer);
    codecUserData->outputCond.notify_all();
}
预处理
C++ 复制代码
// VideoEncoder.cpp
OH_VideoEncoder_Prepare(encoder_);
启动编码器
C++ 复制代码
// VideoEncoder.cpp
int32_t VideoEncoder::Start()
{
    CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, COMMON_ERR_ERROR, "Encoder is null");

    int ret = OH_VideoEncoder_Start(encoder_);
    CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, COMMON_ERR_ERROR, "Start failed, ret: %{public}d", ret);
    return COMMON_ERR_OK;
}
视频编码输出

输出的视频帧中会有不同类型,需要注意每帧的属性值,属性中的flags会表明该帧类型,如flag=0为普通帧,flag=2为关键帧,flag=8是带有特殊数据值的帧,一般会在首帧带有部分特殊数据,即首帧的属性中flag=8,带有sps/pps数据,需要做特殊处理的可在首帧中提取sps/pps值

这里博主暂未处理sps/pps数据。

C++ 复制代码
/**
 * 视频编码输出流
 */
 // Recorder.cpp
void Recorder::VideoEncOutputThread() {
    while (true) {
        CHECK_AND_BREAK_LOG(isStarted_, "Work done, thread out");
        std::unique_lock<std::mutex> lock(videoEncContext_->outputMutex);
        bool condRet = videoEncContext_->outputCond.wait_for(lock, 5s, [this]() { return !isStarted_ || !videoEncContext_->outputBufferInfoQueue.empty(); });
        CHECK_AND_BREAK_LOG(isStarted_, "Work done, thread out");
        CHECK_AND_CONTINUE_LOG(!videoEncContext_->outputBufferInfoQueue.empty(), "Video Buffer queue is empty, continue, cond ret: %{public}d", condRet);

        CodecBufferInfo bufferInfo = videoEncContext_->outputBufferInfoQueue.front();
        videoEncContext_->outputBufferInfoQueue.pop();
        CHECK_AND_BREAK_LOG(!(bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_EOS), "Catch EOS, thread out");
        
        lock.unlock();
        if ((bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_SYNC_FRAME) ||
            (bufferInfo.attr.flags == AVCODEC_BUFFER_FLAGS_NONE)) {
            videoEncContext_->outputFrameCount++;
            bufferInfo.attr.pts = videoEncContext_->outputFrameCount * MICROSECOND / sampleInfo_.frameRate;
        } else {
            bufferInfo.attr.pts = 0;
        }
        COMMON_LOGW("Out buffer count: %{public}u, size: %{public}d, flag: %{public}u, pts: %{public}ld",
                            videoEncContext_->outputFrameCount, bufferInfo.attr.size, bufferInfo.attr.flags,
                            bufferInfo.attr.pts);
        
        if (videoOutputFile_.is_open()) {
            videoOutputFile_.write((const char *)OH_AVBuffer_GetAddr(reinterpret_cast<OH_AVBuffer *>(bufferInfo.buffer)), bufferInfo.attr.size);
        }
        int32_t ret = videoEncoder_->FreeOutputBuffer(bufferInfo.bufferIndex);
        CHECK_AND_BREAK_LOG(ret == COMMON_ERR_OK, "Encoder output thread out");
    }
    COMMON_LOGI("Exit, frame count: %{public}u", videoEncContext_->outputFrameCount);
    StartRelease();
}
资源释放
C++ 复制代码
// Recorder.cpp
if (videoEncoder_ != nullptr) {
    videoEncoder_->Stop();
    if (sampleInfo_.window != nullptr) {
        OH_NativeWindow_DestroyNativeWindow(sampleInfo_.window);
        sampleInfo_.window = nullptr;
    }
    videoEncoder_->Release();
    videoEncoder_.reset();
    COMMON_LOGI("Video encoder release successful");
}
if (videoEncContext_ != nullptr) {
    delete videoEncContext_;
    videoEncContext_ = nullptr;
}

音频编码

官方示例:音频编码-音视频编解码-AVCodec Kit(音视频编解码服务)

音频编码调用关系图:

链接so库
C++ 复制代码
target_link_libraries(recorder PUBLIC libnative_media_codecbase.so)
target_link_libraries(recorder PUBLIC libnative_media_core.so)
target_link_libraries(recorder PUBLIC libnative_media_acodec.so)
添加头文件
C++ 复制代码
#include <multimedia/player_framework/native_avcodec_audiocodec.h> 
#include <multimedia/native_audio_channel_layout.h> 
#include <multimedia/player_framework/native_avcapability.h> 
#include <multimedia/player_framework/native_avcodec_base.h>
#include <multimedia/player_framework/native_avformat.h>
#include <multimedia/player_framework/native_avbuffer.h>
创建音频编码器
C++ 复制代码
// AudioEncoder.cpp
const char * audioCodecMime = OH_AVCODEC_MIMETYPE_AUDIO_AAC;
int32_t AudioEncoder::Create(const char * audioCodecMime)
{
    audioEnc_ = OH_AudioCodec_CreateByMime(OH_AVCODEC_MIMETYPE_AUDIO_AAC, true);
    COMMON_LOGI("AudioEncoder On Create CreateByMime = %{public}s", audioCodecMime);
    CHECK_AND_RETURN_RET_LOG(audioEnc_ != nullptr, COMMON_ERR_ERROR, "Create failed");
    return COMMON_ERR_OK;
}
编码器配置
C++ 复制代码
// AudioEncoder.cpp
int32_t AudioEncoder::Configure(const SampleInfo &sampleInfo)
{
    OH_AVFormat *format = OH_AVFormat_Create();
    CHECK_AND_RETURN_RET_LOG(format != nullptr, COMMON_ERR_ERROR, "AVFormat create failed");
    
    OH_AVFormat_SetIntValue(format,OH_MD_KEY_AUD_CHANNEL_COUNT, sampleInfo.audioChannelCount);
    OH_AVFormat_SetIntValue(format,OH_MD_KEY_AUD_SAMPLE_RATE, sampleInfo.audioSampleRate);
    OH_AVFormat_SetLongValue(format,OH_MD_KEY_BITRATE, sampleInfo.audioSampleBitrate);
    OH_AVFormat_SetIntValue(format, OH_MD_KEY_AUDIO_SAMPLE_FORMAT, sampleInfo.audioSampleFormat);
    OH_AVFormat_SetLongValue(format,OH_MD_KEY_CHANNEL_LAYOUT, sampleInfo.audioChannelLayout);
    OH_AVFormat_SetIntValue(format,OH_MD_KEY_MAX_INPUT_SIZE, sampleInfo.defaultMaxInputSize);
    COMMON_LOGI("====== AudioEncoder config ======");
    COMMON_LOGI("SampleRate:%{public}d, SampleBitrate:%{public}ld, defaultMaxInputSize:%{public}d",
        sampleInfo.audioSampleRate, sampleInfo.audioSampleBitrate, sampleInfo.defaultMaxInputSize);
    COMMON_LOGI("====== AudioEncoder config ======");

    int ret = OH_AudioCodec_Configure(audioEnc_, format);
    CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, COMMON_ERR_ERROR, "Config failed, ret: %{public}d", ret);
    OH_AVFormat_Destroy(format);
    format = nullptr;
    return COMMON_ERR_OK;
}
回调注册

OnNeedInputBuffer、OnNewOutputBuffer、CodecUserData解释同视频编码。

C++ 复制代码
// AudioEncoder.cpp
int32_t AudioEncoder::SetCallback(CodecUserData *codecUserData)
{
    int32_t ret = OH_AudioCodec_RegisterCallback(audioEnc_,
        { SampleCallback::OnCodecError, SampleCallback::OnCodecFormatChange, SampleCallback::OnNeedInputBuffer, SampleCallback::OnNewOutputBuffer },
          codecUserData);
    CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, COMMON_ERR_ERROR, "Set callback failed, ret: %{public}d", ret);

    return COMMON_ERR_OK;
}
预处理
C++ 复制代码
// AudioEncoder.cpp
OH_AudioCodec_Prepare(audioEnc_)
启动编码器
C++ 复制代码
// AudioEncoder.cpp
int32_t AudioEncoder::Start()
{
    CHECK_AND_RETURN_RET_LOG(audioEnc_ != nullptr, COMMON_ERR_ERROR, "Encoder is null");

    int ret = OH_AudioCodec_Start(audioEnc_);
    CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, COMMON_ERR_ERROR, "Start failed, ret: %{public}d", ret);
    return COMMON_ERR_OK;
}
音频编码输出
C++ 复制代码
/**
 * 音频编码输出流
 */
 // Recorder.cpp
void Recorder::AudioEncOutputThread() {
     while (true) {
        CHECK_AND_BREAK_LOG(isStarted_, "Work done, thread out");
        std::unique_lock<std::mutex> lock(audioEncContext_->outputMutex);
        bool condRet = audioEncContext_->outputCond.wait_for(lock, 5s, [this]() { return !isStarted_ || !audioEncContext_->outputBufferInfoQueue.empty(); });
        CHECK_AND_BREAK_LOG(isStarted_, "Work done, thread out");
        CHECK_AND_CONTINUE_LOG(!audioEncContext_->outputBufferInfoQueue.empty(),"Audio Buffer queue is empty, continue, cond ret: %{public}d", condRet);

        CodecBufferInfo bufferInfo = audioEncContext_->outputBufferInfoQueue.front();
        audioEncContext_->outputBufferInfoQueue.pop();
        CHECK_AND_BREAK_LOG(!(bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_EOS), "Catch EOS, thread out");
        
        lock.unlock();
        audioEncContext_->outputFrameCount++;
        COMMON_LOGW("Out buffer count: %{public}u, size: %{public}d", audioEncContext_->outputFrameCount, bufferInfo.attr.size);
        
        if (audioOutputFile_.is_open()) {
            audioOutputFile_.write((const char *)OH_AVBuffer_GetAddr(reinterpret_cast<OH_AVBuffer *>(bufferInfo.buffer)), bufferInfo.attr.size);
        }
        int32_t ret = audioEncoder_->FreeOutputBuffer(bufferInfo.bufferIndex);
        CHECK_AND_BREAK_LOG(ret == COMMON_ERR_OK, "Encoder output thread out");
    }
    COMMON_LOGI("Exit, frame count: %{public}u", audioEncContext_->outputFrameCount);
    StartRelease();
}
资源释放
C++ 复制代码
// Recorder.cpp
if (audioEncoder_ != nullptr) {
	audioEncoder_->Stop();
	audioEncoder_->Release();
	audioEncoder_.reset();
	COMMON_LOGI("Audio encoder release successful");
}
if (audioEncContext_ != nullptr) {
	delete audioEncContext_;
	audioEncContext_ = nullptr;
}

音视频文件输出

该步骤用来验证音视频编码是否成功,真实场景在写入的步骤应该改为将编码后buffer发送给对端。

添加头文件
C++ 复制代码
#include <fstream>
文件创建

此处创建为沙箱路径,对应在应用中的hdc真实路径为:

/data/app/el2/100/base/com.example.recorderavcodedemo/haps/entry/files

可下载至PC使用相关播放器打开验证。

C++ 复制代码
// Recorder.cpp
videoOutputFile_.open("/data/storage/el2/base/haps/entry/files/video_decode_out.h264", std::ios::out | std::ios::binary);
audioOutputFile_.open("/data/storage/el2/base/haps/entry/files/audio_decode_out.aac", std::ios::out | std::ios::binary);
视频编码buffer写入视频文件
C++ 复制代码
// Recorder.cpp
if (videoOutputFile_.is_open()) {
    videoOutputFile_.write((const char *)OH_AVBuffer_GetAddr(reinterpret_cast<OH_AVBuffer *>(bufferInfo.buffer)), bufferInfo.attr.size);
}
音频编码buffer写入音频文件
C++ 复制代码
// Recorder.cpp
if (audioOutputFile_.is_open()) {
    audioOutputFile_.write((const char *)OH_AVBuffer_GetAddr(reinterpret_cast<OH_AVBuffer *>(bufferInfo.buffer)), bufferInfo.attr.size);
}
资源释放
C++ 复制代码
// Recorder.cpp
if (videoOutputFile_.is_open()) {
    videoOutputFile_.close();
}
if (audioOutputFile_.is_open()) {
    audioOutputFile_.close();
}

完整示例

基于官方示例AVCodecVideo修改补充,示例代码拉到本地后需自行签名后再运行。

项目地址: zhaoxin/RecorderAVCodeDemo

结语

鸿蒙的发展任重道远,在部分系统能力上可能并没有安卓如此完善,但社区的代码从未停止维护,鸿蒙的目标不止是移动端,在未来相对于安卓只会有过之而无不及,各开发者需要给予国产操作系统耐心和支持。目前三方相关示例及资料并未像安卓一样富余,但社区与华为有在尽量完善示例,覆盖各系统能力,开发者在开发应用能力前可参考OpenHarmony与HarmonyOS的各samples和codelabs,有问题也可向开发者论坛提问或提工单,内容至此希望对各位开发者有所帮助。

链接如下:

HarmonyOS Next:HarmonyOS_SamplesHarmonyOS_Codelabs

OpenHarmony:OpenHarmony_SamplesOpenHarmony_Codelabs

开发者论坛:问答专区-华为/鸿蒙开发者论坛-华为开发者联盟

有问题请留言,虚心请教。

相关推荐
京东云开发者7 分钟前
Taro on Harmony :助力业务高效开发纯血鸿蒙应用
harmonyos
前端付豪1 小时前
2、ArkTS 是什么?鸿蒙最强开发语言语法全讲解(附实操案例)
前端·后端·harmonyos
zhujiaming1 小时前
鸿蒙端应用适配使用开源flutter值得注意的一些问题
前端·flutter·harmonyos
前端付豪1 小时前
8、鸿蒙动画开发实战:做一个会跳舞的按钮!(附动效示意图)
前端·后端·harmonyos
前端付豪1 小时前
3、构建你的第一个鸿蒙组件化 UI 页面:实现可复用的卡片组件(附实战代码)
前端·后端·harmonyos
前端付豪1 小时前
7、打造鸿蒙原生日历组件:自定义 UI + 数据交互(附实操案例与效果图)
前端·后端·harmonyos
别说我什么都不会2 小时前
【仓颉三方库】音视频开发—— ijkplayer-ffi
harmonyos
王二蛋与他的张大花6 小时前
HarmonyOS运动开发:如何监听用户运动步数数据
harmonyos
冯志浩6 小时前
HarmonyOS - 实现 ArkTS 和 web 页面的数据交互
harmonyos·掘金·金石计划