场景描述

类似音视频配音功能,适用于给视频配音,配乐。

场景1:输入一个视频文件和一个音频文件,将他们合成1个视频文件,要求音频文件合成到视频制定的时间范围。

场景2:输入一个视频文件和多个音频文件,将他们合成1个视频文件,要求将多个音频文件合成到视频制定的时间范围。2.1 多个音频文件串行合成。2.2 多个音频文件并行合成。备注:多个音频文件编码类型要一致,还要确保封装格式是支持的。

方案描述

  1. TS侧通过XComponentController组件控制器来调用NDK侧的合成和播放方法,支持动态配置音频播放的时间点。
  2. NDK侧收到合成请求后,读取resources/rawfile目录中的音视频输入文件保存到配置中,同时创建封装后的输出文件。
  3. NDK侧开始合成:创建音频解封装器和视频解封装器,音频和视频放在两个子线程中分开处理。流程:原始音频(多个) --\>解封装 --\>封装(输出文件)。原始视频 --\>解封装 --\>封装(输出文件)。通过修改pts的值可以实现音频文件合成到视频制定的时间范围。
  4. NDK侧收到播放请求后,从配置文件中读取合成后的输出文件进行播放。流程:解封装--\>音频+视频。音频--\>解码--\>OH\_AudioRenderer播放。视频--\>解码--\>Surface模式给到XComponent送显。

场景实现

场景一:输入一个视频文件和一个音频文件,将他们合成1个视频文件,要求音频文件合成到视频制定的时间范围。

核心代码

  1. TS侧通过XComponentController组件控制器来调用NDK侧的合成和播放方法,支持动态配置音频播放的时间点。

    build() {
      Column() {
        Column() {
          XComponent({
            id: 'xcomponentId',
            type: XComponentType.SURFACE,
            libraryname: 'entry',
            controller: this.mXComponentController
          })
            .onLoad((xComponentContext) => {
              this.xComponentContext = xComponentContext as XComponentContext;
              this.mXComponentController.setXComponentSurfaceRect({
                surfaceWidth: Constants.PREVIEW_HEIGHT, 
                surfaceHeight: Constants.PREVIEW_WIDTH})
            })
            .onDestroy(() => {
              console.log('onDestroy');
            })
            .id('xcomponent')
            .margin(5)
            .layoutWeight(1)
        }
        .layoutWeight(1)
        .width('90%')
    
        Blank(10)
        Row() {
          Column() {
          }.width('95')
          Button(this.buttonCombination)
            .onClick(() => {
              if (this.buttonCombination == this.START_COMBINATION) {
                this.buttonCombination = this.STOP_COMBINATION
                if (this.xComponentContext) {
                  this.textFocusAble = false;
                  this.xComponentContext.StartCombination(getContext(this).resourceManager, this.audioDelay);
                  this.startCheck();
                }
              } else {
                this.buttonCombination = this.START_COMBINATION
                if (this.xComponentContext) {
                  this.stopCheck();
                  this.xComponentContext.StopCombination();
                  this.textFocusAble = true;
                }
              }
            })
          TextInput({placeholder:'音频延时'})
            .type(InputType.Normal)
            .maxLength(3)
            .width('25%')
            .onChange((value: string) => {
              this.audioDelay = Number(value);
            })
            .focusable(this.textFocusAble)
        }
    
        Blank(10)
        Button(this.buttonPlayer)
          .onClick(() => {
            if (this.buttonPlayer == this.START_PLAYER) {
              this.buttonPlayer = this.STOP_PLAYER
              if (this.xComponentContext) {
                this.xComponentContext.StartPlayer();
              }
            } else {
              this.buttonPlayer = this.START_PLAYER
              if (this.xComponentContext) {
                this.xComponentContext.StopPlayer();
              }
            }
          })
      }
      .width('100%')
      .height('100%')
    }
  2. NDK侧收到合成请求后,读取resources/rawfile目录中的音视频输入文件保存到配置文件中,同时创建封装后的输出文件。

    void PluginRender::ReadFileData(napi_env env, NativeResourceManager *ResMmgr, RES_TYPE type)
    {
      int32_t fileCount = 1;
      if (type == RES_TYPE::RES_TYPE_AUDIO_IN) {
      fileCount = AUDIO_FILES_COUNT;
    }
      for (int32_t i = 0; i < fileCount; ++i) {
      napi_value name_napi;
      const std::string name = AppConfig::GetInstance().GetResDir(type, i);
      napi_create_string_utf8(env, name.c_str(), name.length(), &name_napi);
    
      size_t strSize;
      char strBuf[256];
      napi_get_value_string_utf8(env, name_napi, strBuf, sizeof(strBuf), &strSize);
      std::string filename(strBuf, strSize);
      // 获取rawfile指针对象
      RawFile *rawFile = OH_ResourceManager_OpenRawFile(ResMmgr, filename.c_str());
      if (rawFile == nullptr) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "PluginRender", "OH_ResourceManager_OpenRawFile failed");
      }
      // 获取rawfile的描述符RawFileDescriptor {fd, offset, length}
      RawFileDescriptor descriptor;
      OH_ResourceManager_GetRawFileDescriptor(rawFile, descriptor);
      // 关闭打开的指针对象
      OH_ResourceManager_CloseRawFile(rawFile);
      // 保存文件配置
      FdInfo info;
      info.inputFd = descriptor.fd;
      info.inputFileOffset = descriptor.start;
      info.inputFileSize = descriptor.length;
      AppConfig::GetInstance().SetFileData(type, info, i);
    }
    }
    
    napi_value PluginRender::StartCombination(napi_env env, napi_callback_info info)
    {
      PluginRender *render = GetPluginRender(env, info);
      if (render == nullptr) {
        return nullptr;
      }
      size_t argc = 2;
      napi_value args[2] = { nullptr };
      napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
      napi_valuetype valueType;
      napi_typeof(env, args[0], &valueType);
      // 获取native的resourceManager对象
      NativeResourceManager *mNativeResMgr = OH_ResourceManager_InitNativeResourceManager(env, args[0]);
      if(mNativeResMgr != nullptr){
        // 读取音视频输入文件
        ReadFileData(env, mNativeResMgr, RES_TYPE::RES_TYPE_AUDIO_IN);
        ReadFileData(env, mNativeResMgr, RES_TYPE::RES_TYPE_VIDEO_IN);
        // 释放resourceManager对象
        OH_ResourceManager_ReleaseNativeResourceManager(mNativeResMgr);
    
        // 音频延迟时长,单位秒
        int32_t value1;
        napi_get_value_int32(env, args[1], &value1);
        AppConfig::GetInstance().SetAudioDelay(value1);
    
        // 开始合成
        render->StartCombination();
      }
    
      return nullptr;
    }
  3. NDK侧开始合成:创建音频解封装器和视频解封装器,音频和视频放在两个子线程中分开处理。

    void PluginRender::StartCombination(void)
    {
      SampleInfo sampleInfo;
      // 合成后输出文件
      int32_t outputFd =
      open(AppConfig::GetInstance().GetResDir(RES_TYPE::RES_TYPE_VIDEO_OUT).c_str(), O_RDWR | O_CREAT, 0777);
      sampleInfo.outputFd = outputFd;
    
      // 视频输入文件
      AppConfig::GetInstance().GetFileData(RES_TYPE::RES_TYPE_VIDEO_IN, sampleInfo.videoFd);
      // 音频输入文件
      for (int i = 0; i < AUDIO_FILES_COUNT; ++i) {
      AppConfig::GetInstance().GetFileData(RES_TYPE::RES_TYPE_AUDIO_IN, sampleInfo.audioFd[i], i);
    }
    
      // 开始合成
      int32_t ret = Recorder::GetInstance().Start(sampleInfo);
      if (ret != AVCODEC_SAMPLE_ERR_OK) {
        return;
      }
    }
    int32_t Recorder::Start(SampleInfo &sampleInfo)
    {
      std::lock_guard<std::mutex> lock(mutex_);
    
      CHECK_AND_RETURN_RET_LOG(!isStarted_, AVCODEC_SAMPLE_ERR_ERROR, "Already started.");
      for (int32_t i = 0; i < AUDIO_FILES_COUNT; ++i) {
      CHECK_AND_RETURN_RET_LOG(demuxer_audio[i] == nullptr, AVCODEC_SAMPLE_ERR_ERROR,
        "Already started audio demuxer.");
    }
      CHECK_AND_RETURN_RET_LOG(demuxer_video == nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Already started video demuxer.");
      CHECK_AND_RETURN_RET_LOG(muxer_ == nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Already started muxer_.");
    
      sampleInfo_ = sampleInfo;
    
      // 音频和视频解封转,从解封器中读取数据
      demuxer_video = std::make_unique<Demuxer>();
      int32_t ret = demuxer_video->Create(VIDEO_TYPE, sampleInfo_);
      CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Create demuxer_video failed");
      for (int32_t i = 0; i < AUDIO_FILES_COUNT; ++i) {
      demuxer_audio[i] = std::make_unique<Demuxer>();
      ret = demuxer_audio[i]->Create(AUDIO_TYPE, sampleInfo_, i);
      CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Create demuxer_audio failed");
    }
    
      // 封转音视频
      muxer_ = std::make_unique<Muxer>();
      ret = muxer_->Create(sampleInfo_.outputFd);
      CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Create muxer with fd(%{public}d) failed",
        sampleInfo_.outputFd);
      ret = muxer_->Config(sampleInfo_);
      CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Recorder muxer config failed");
      ret = muxer_->Start();
      CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Muxer start failed");
    
      isStarted_ = true;
      videoDemuxerThread_ = std::make_unique<std::thread>(&Recorder::VideoProcessThread, this);
      audioDemuxerThread_ = std::make_unique<std::thread>(&Recorder::AudioProcessThread, this);
      if (videoDemuxerThread_ == nullptr || audioDemuxerThread_ == nullptr) {
        AVCODEC_SAMPLE_LOGE("Create thread failed");
        StartRelease();
        return AVCODEC_SAMPLE_ERR_ERROR;
      }
    
      releaseThread_ = nullptr;
      AVCODEC_SAMPLE_LOGI("Succeed");
      return AVCODEC_SAMPLE_ERR_OK;
    }
    
    void Recorder::VideoProcessThread()
    {
      OH_AVBuffer *buffer = OH_AVBuffer_Create(sampleInfo_.videoWidth * sampleInfo_.videoHeight * 3 >> 1);
      OH_AVCodecBufferAttr attr;
      while (true) {
        CHECK_AND_BREAK_LOG(isStarted_, "Work done, VideoDemuxerThread out");
        demuxer_video->ReadSample(demuxer_video->GetVideoTrackId(), reinterpret_cast<OH_AVBuffer *>(buffer), attr);
        // 从封装器中读取结束
        if (attr.flags == OH_AVCodecBufferFlags::AVCODEC_BUFFER_FLAGS_EOS) {
          videoEnd = true;
          break;
        }
        // 封装视频
        muxer_->WriteSampleVideo(reinterpret_cast<OH_AVBuffer *>(buffer), attr);
      }
    
      OH_AVBuffer_Destroy(buffer);
    }
    
    void Recorder::AudioProcessThread()
    {
      OH_AVBuffer *buffer = OH_AVBuffer_Create(sampleInfo_.videoWidth * sampleInfo_.videoHeight * 3 >> 1);
      OH_AVCodecBufferAttr attr;
      int32_t lastPts = AppConfig::GetInstance().GetAudioDelay() * 1000 * 1000;
      int32_t nextPts = 0;
      // 封装音频
      int32_t fileIndex = 0; // 代表第几个音频文件
      while(fileIndex < AUDIO_FILES_COUNT){
        demuxer_audio[fileIndex]->ReadSample(demuxer_audio[fileIndex]->GetAudioTrackId(),
          reinterpret_cast<OH_AVBuffer *>(buffer), attr);
        if (attr.flags == OH_AVCodecBufferFlags::AVCODEC_BUFFER_FLAGS_EOS) {
          audioEnd[fileIndex++] = true;
          lastPts = nextPts;
        } else {
          attr.pts += lastPts;
          nextPts = attr.pts;
          muxer_->WriteSampleAudio(reinterpret_cast<OH_AVBuffer *>(buffer), attr);
        }
        CHECK_AND_BREAK_LOG(isStarted_, "Work done, AudioDemuxerThread out");
      }
      OH_AVBuffer_Destroy(buffer);
    }
  4. NDK侧收到播放请求后,从配置文件中读取合成后的输出文件进行播放。

    void PluginRender::StartPlayer(void)
    {
      const std::string playerRoot = AppConfig::GetInstance().GetResDir(RES_TYPE::RES_TYPE_VIDEO_OUT);
      int32_t inputFd = open(playerRoot.c_str(), O_RDONLY, 0777);
    
      int64_t fileSize = 0;
      struct stat fileStatus {};
    if (stat(playerRoot.c_str(), &fileStatus) == 0) {
      fileSize = static_cast<int64_t>(fileStatus.st_size);
    } else {
      OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "PluginRender", "StartPlayer: get stat failed");
      return;
    }
    
    SampleInfo sampleInfo;
    sampleInfo.videoFd.inputFd = inputFd;
    sampleInfo.videoFd.inputFileOffset = 0;
    sampleInfo.videoFd.inputFileSize = fileSize;
    sampleInfo.window = nativeWindow_;  // 这里直接用XComponent对应的NativeWindow
    
    int32_t ret = Player::GetInstance().Init(sampleInfo);
    if (ret != AVCODEC_SAMPLE_ERR_OK) {
      return;
    }
    
    Player::GetInstance().Start();
    }
    
    int32_t Player::Start()
    {
      std::lock_guard<std::mutex> lock(mutex_);
      CHECK_AND_RETURN_RET_LOG(!isStarted_, AVCODEC_SAMPLE_ERR_ERROR, "Already started.");
      CHECK_AND_RETURN_RET_LOG(demuxer_video != nullptr && videoDecoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR,
        "Already started.");
      int32_t ret;
      if (videoDecContext_) {
        ret = videoDecoder_->Start();
        CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Decoder start failed");
        isStarted_ = true;
        videoDecInputThread_ = std::make_unique<std::thread>(&Player::VideoDecInputThread, this);
        videoDecOutputThread_ = std::make_unique<std::thread>(&Player::VideoDecOutputThread, this);
        if (videoDecInputThread_ == nullptr || videoDecOutputThread_ == nullptr) {
          AVCODEC_SAMPLE_LOGE("Create thread failed");
          StartRelease();
          return AVCODEC_SAMPLE_ERR_ERROR;
        }
      }
    
      if (audioDecContext_) {
        ret = audioDecoder_->Start();
        CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Audio Decoder start failed");
        isStarted_ = true;
        audioDecInputThread_ = std::make_unique<std::thread>(&Player::AudioDecInputThread, this);
        audioDecOutputThread_ = std::make_unique<std::thread>(&Player::AudioDecOutputThread, this);
        if (audioDecInputThread_ == nullptr || audioDecOutputThread_ == nullptr) {
          AVCODEC_SAMPLE_LOGE("Create thread failed");
          StartRelease();
          return AVCODEC_SAMPLE_ERR_ERROR;
        }
    
        // 清空播放缓存
        if (audioDecContext_) {
          audioDecContext_->CodecUserCache_.ClearCache();
        }
        // 开启音频播放
        audioRenderer_->AudioRendererStart();
      }
    
      AVCODEC_SAMPLE_LOGI("Succeed");
      doneCond_.notify_all();
      return AVCODEC_SAMPLE_ERR_OK;
    }

场景二:输入一个视频文件和多个音频文件,将他们合成1个视频文件,要求将多个音频文件合成到视频制定的时间范围。

2.1 多个音频文件串行合成。

可以实现,在如下配置文件中可设置音频文件数和音频源文件,多个音频文件可串行合入。

#ifndef APP_CONFIG_H
#define APP_CONFIG_H

#include <cstdint>
#include <string>

  const int32_t AUDIO_FILES_COUNT = 3;

enum class RES_TYPE { RES_TYPE_AUDIO_IN, RES_TYPE_VIDEO_IN, RES_TYPE_VIDEO_OUT };

struct FdInfo {
  int32_t inputFd = -1;
  int64_t inputFileOffset = 0;
  int64_t inputFileSize = 0;
};

class AppConfig {
  public:
    static AppConfig &GetInstance()
{
  static AppConfig config_;
  return config_;
}

  int32_t GetAudioDelay();
  void SetAudioDelay(int32_t value);

  const std::string &GetResDir(RES_TYPE type, int32_t index = 0);

  void SetFileData(RES_TYPE type, FdInfo& data, int32_t index = 0);
  void GetFileData(RES_TYPE type, FdInfo& data, int32_t index = 0);

  private:
    AppConfig() {}
~AppConfig() {}

private:
  int32_t audioDelay_ = 0;  //合成后的视频播放几秒后再播放音频

// 音视频输入文件在rawfile目录
std::string videoInName = "video.mp4";
FdInfo videoInData;
std::string audioInName[AUDIO_FILES_COUNT] = {
  "boisterous.wav",
  "boisterous.wav",
  "boisterous.wav"
};
FdInfo audioInData[AUDIO_FILES_COUNT];

// 合成后输出文件在应用沙箱目录
std::string videoOut = "/data/storage/el2/base/haps/entry/files/recorder01.mp4";
};

#endif  // APP_CONFIG_H

音频多文件合成核心逻辑。

void Recorder::AudioProcessThread()
{
  OH_AVBuffer *buffer = OH_AVBuffer_Create(sampleInfo_.videoWidth * sampleInfo_.videoHeight * 3 >> 1);
  OH_AVCodecBufferAttr attr;
  int32_t lastPts = AppConfig::GetInstance().GetAudioDelay() * 1000 * 1000;
  int32_t nextPts = 0;
  // 封装音频
  int32_t fileIndex = 0; // 代表第几个音频文件
  while(fileIndex < AUDIO_FILES_COUNT){
    demuxer_audio[fileIndex]->ReadSample(demuxer_audio[fileIndex]->GetAudioTrackId(),
      reinterpret_cast<OH_AVBuffer *>(buffer), attr);
    if (attr.flags == OH_AVCodecBufferFlags::AVCODEC_BUFFER_FLAGS_EOS) {
      audioEnd[fileIndex++] = true;
      lastPts = nextPts;
    } else {
      attr.pts += lastPts;
      nextPts = attr.pts;
      muxer_->WriteSampleAudio(reinterpret_cast<OH_AVBuffer *>(buffer), attr);
    }
    CHECK_AND_BREAK_LOG(isStarted_, "Work done, AudioDemuxerThread out");
  }
  OH_AVBuffer_Destroy(buffer);
}

2.2 多个音频文件并行合成。

封装器虽然可以创建多个音频轨,但是播放时播放器默认只会选择一个音频轨播放。所以,要想实现并行合成后播放,只能混音,即多个音频文件先混音成一个文件。但是框架目前没有提供实现该能力的系统API,只能通过FFmpeg等三方库来实现。


HarmonyOS码上奇行
11.3k 声望4.1k 粉丝

欢迎关注 HarmonyOS 开发者社区:[链接]