音视频合成功能
场景描述 类似音视频配音功能,适用于给视频配音,配乐。 场景1:输入一个视频文件和一个音频文件,将他们合成1个视频文件,要求音频文件合成到视频制定的时间范围。 场景2:输入一个视频文件和多个音频文件,将他们合成1个视频文件,要求将多个音频文件合成到视频制定的时间范围。2.1 多个音频文件串行合成。2.2 多个音频文件并行合成。备注:多个音频文件编码类型要一致,还要确保封装格式是支持的。 方案描述
场景描述
类似音视频配音功能,适用于给视频配音,配乐。
场景1:输入一个视频文件和一个音频文件,将他们合成1个视频文件,要求音频文件合成到视频制定的时间范围。
场景2:输入一个视频文件和多个音频文件,将他们合成1个视频文件,要求将多个音频文件合成到视频制定的时间范围。2.1 多个音频文件串行合成。2.2 多个音频文件并行合成。备注:多个音频文件编码类型要一致,还要确保封装格式是支持的。
方案描述
- TS侧通过XComponentController组件控制器来调用NDK侧的合成和播放方法,支持动态配置音频播放的时间点。
- NDK侧收到合成请求后,读取resources/rawfile目录中的音视频输入文件保存到配置中,同时创建封装后的输出文件。
- NDK侧开始合成:创建音频解封装器和视频解封装器,音频和视频放在两个子线程中分开处理。流程:原始音频(多个) -->解封装 -->封装(输出文件)。原始视频 -->解封装 -->封装(输出文件)。通过修改pts的值可以实现音频文件合成到视频制定的时间范围。
- 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等三方库来实现。
更多推荐
所有评论(0)