由于项目需要,需要将一个mp4文件中的视频和另一个mp4文件的音频合成为一个mp4文件。因此试着将合成过程中解决问题的方法记录下来,以便以后进行查看。
合成中需要处理的问题:
1.当视频和音频时长不一致时,如何处理?
以视频的时长为标准,音频时长比视频短时,添加静音帧作为补充;
开始时需要处理的问题:
第一种情况:视频的开始时间要比音频晚,因此要过滤掉一部分音频,等到达视频开始时间时,才开始将视频和音频数据写入MP4文件。
第二种情况:音频的开始时间比视频晚,因此为了保证同步,用静音帧来填补音频数据的缺失。
结尾时需要处理的问题:
结尾也同样有两种情况需要进行处理。
第一种情况:音频先于视频结束,为了与视频对齐,因此需要添加静音帧来弥补音频时长过短的问题。
第二种情况:音频结束时间比视频晚,此时与视频进行看齐,丢失多余的音频帧数据。
只要在合并视频和音频之前,建立好相应的规则,才能更有效保证合成后的文件中,音频和视频是同步的。
2.是否需要重新对音视频进行解码和编码?
考虑到现有的文件中视频为H264编码,音频为AAC编码,为了节约时间,提高合并效率,不再对视频、音频进行重新解码和编码。具体可以参考最简单的基于FFmpeg的封装格式处理:视音频复用器(muxer这篇文章。
由于不需要进行编解码,只需要将原文件中的视频和音频数据直接写入到新文件中。为了能够只获取纯数据,因此分别采用了"h264_mp4toannexb"和"aac_adtstoasc"来得到原始的纯视频和音频数据,其用法,在参考文章中讲解的也很清楚,此处不再细述。
3.新生成文件中音频和视频时间戳如何写?
此部分也是比较重要的部分,因为这直接关系到合成文件中视频和音频是否同步。在实际操作过程中,为了更好的实现音频和视频的同步,分别采用变帧率采集音频和视频,这样就可以避免采集的视频和音频由于设备不一致的原因而导致的音视频本身不同步。至于视频和音频采集的方法可以参考RTSP流封装成MP4方法总结中的源码,里面提供了变帧率和固定帧率两种录制方式。
经过实验验证,新文件采用变帧率的录制方式进行,同时新生成视频文件的时间戳按照原来视频和音频中的时间戳来写所得到的最终文件音视频同步效果较好,这里用到了MP4v2对H264视频和AAC音频进行封装,主要是由于个人原因(MP4v2调用方便以及生成的视频文件质量较高)。因此需要对MP4v2库有一个很好的理解,这部分可以参考利用ffmpeg录制rtsp流的方法总结(三)。
注意:这部分是从原始文件中获取时间戳的,因此MP4WriteSample()函数中的duration设置有些不太一样,此处的duration为两连续帧之间的时间戳即可,不需要除以timeScale,这个原因,只需要明白duration的含义就懂了。
4.如何生成静音帧?
由于一帧AAC音频格式frame_size为1024,因此静音帧中的frame_size也设置为1024,AVFrame中的其它参数和输入的音频中参数保持一致,其初始化代码如下:
int ret=NO_VALUE;
int frame_size_out_encode = output_codec_context->frame_size;
printf("frame_size_out_encode %d \n", frame_size_out_encode);
if(!frame_size_out_encode)
frame_size_out_encode = 1024;
(*frame)->nb_samples = frame_size_out_encode;
(*frame)->channel_layout = output_codec_context->channel_layout;
(*frame)->channels = av_get_channel_layout_nb_channels((*frame)->channel_layout);
(*frame)->format = output_codec_context->sample_fmt;
(*frame)->sample_rate = output_codec_context->sample_rate;
ret=av_frame_get_buffer((*frame), 0);
if(ret<0)
return AV_FRAME_GET_BUFFER_ERROR;
//根据参数填充静音帧数据,常说的PCM数据
av_samples_set_silence((*frame)->data, 0, (*frame)->nb_samples, (*frame)->channels, (enum AVSampleFormat)(*frame)->format);
对静音frame初始好后,后面只需要调用aac音频编码器进行编码,时间戳按照1024递增。其编码代码如下:
av_init_packet(&output_packet);
output_packet.data=NULL;
output_packet.size=0;
if((!frame)||(!out_codec_ctx))
return AV_NO_FRAME;
frame->pts = pts;
pts += frame->nb_samples;
ret=avcodec_send_frame(out_codec_ctx, frame);
if (ret == AVERROR_EOF)
{
ret=0;
goto end;
}else if(ret<0)
{
av_packet_unref(&output_packet);
return AV_AUDIO_SEND_FRAME_ERROR;
}
ret=avcodec_receive_packet(out_codec_ctx,&output_packet);
if (ret==AVERROR(EAGAIN))
{
ret=0;
goto end;
}else if(ret==AVERROR_EOF)
{
ret=0;
goto end;
}else if(ret<0)
{
av_packet_unref(&output_packet);
return AUDIO_ENCODE_DATA_ERROR;
}
nowvoltime_audio=1024;
if(mp4encod.MP4WriteAACSlient(output_packet.data,output_packet.size,nowvoltime_audio)<0)
{
av_packet_unref(&output_packet);
return WRITE_AUDIO_DATA_ERROR;
}
MP4WriteAACSlient()函数代码为:
int MP4Encord::MP4WriteAACSlient(const uint8_t *sData, int nSize,uint64_t audio_cur_pts)
{
bool result = false;
if(nSize<0||sData==NULL)
return -1;
result = MP4WriteSample(m_hFile, m_audioTrack, sData, nSize,audio_cur_pts,0,true);
if (!result)
{
printf("write aac frame error!\n");
return -1;
}
return 0;
}
5.这里给出了视频开始时间早于音频开始时间,并且视频结束时间要晚于音频结束时间的示例:
if(video_start_time<audio_start_time)//视频起始时间早于音频起始时间
{
delay_time=audio_start_time-video_start_time;
delay_time=delay_time*out_audio_ctx->sample_rate/1024;
while(merge_state)
{
av_init_packet(&packet);
packet.data=NULL;
packet.size=0;
if(av_compare_ts(cur_pts_v,(*ic_video)->streams[video_index]->time_base,cur_pts_a,(*ic_audio)->streams[audio_index]->time_base) <= 0)
{
//写视频
ret=av_read_frame(*ic_video, &packet);
if(ret>=0)
{
do
{
if(packet.stream_index==video_index)
{
ret=av_bitstream_filter_filter(h264bsfc,in_video_ctx,NULL,&packet.data,&packet.size,packet.data,packet.size,0);
if(ret<0)
{
ret=AV_BITSTREAM_FILTER_ERROR;
goto end;
}
ret=Wrtie_H264_MP4v2(&packet,mp4encord);
if(ret<0)
goto end;
cur_pts_v=packet.pts;
break;
}
}while(av_read_frame(*ic_video, &packet)>= 0);
}
else if(ret==AVERROR_EOF)
{
ret=0;
break;
}
else
break;
}
else //音频快于视频
{
//音频前delay_time为静音
if(num_frame_start<delay_time)
{
ret=Encode_Silent_Audio_Frame(Silent_Frame,ofmt,audio_stream,out_audio_ctx,(*ic_audio),mp4encord,pts);
if(ret<0)
break;
num_frame_start++;
cur_pts_a=num_frame_start*1024;
}
else
{
ret=av_read_frame(*ic_audio, &packet);
if(ret>=0)
{
do
{
if(packet.stream_index==audio_index)
{
ret=av_bitstream_filter_filter(aacbsfc,in_audio_ctx,NULL,&packet.data,&packet.size,packet.data,packet.size,0);
if(ret<0)
{
ret=AV_BITSTREAM_FILTER_ERROR;
break;
}
ret=mp4encord.MP4WriteAACData(packet.data,packet.size,packet.pts);
if(ret<0)
{
ret=WRITE_AUDIO_DATA_ERROR;
av_packet_unref(&packet);
goto end;
}
cur_pts_a=packet.pts+num_frame_start*1024;
last_audio_pts=cur_pts_a;
break;
}
}while(av_read_frame(*ic_audio, &packet)>= 0);
}
else if(ret==AVERROR_EOF)//判断音频文件结束后,添加静音帧
{
ret=Encode_Silent_Audio_Frame(Silent_Frame,ofmt,audio_stream,out_audio_ctx,(*ic_audio),mp4encord,pts);
if(ret<0)
break;
num_frame_end++;
cur_pts_a=last_audio_pts+num_frame_end*1024;
}
else
break;
}
}
av_packet_unref(&packet);
}
}
av_compare_ts()函数起着关键作用,主要对当前的视频和音频的时间戳进行判断,如果视频时间戳小于音频时间戳,就应该读取视频数据;否则读取音频数据。
注意:在这里需要留意的是,当音频结束后,需要记录下最后一帧的音频时间戳,以方便后面的静音帧在此基础上进行叠加,保证新生成MP4文件的完整性。如果时间戳设置有问题,会出现音视频不同步,视频播放时,进度条显示速度不正常。
为了使用方便,将原始方法封装成了MFC可执行文件。其简要界面为
该部分下载链接为:可执行程序下载,测试视频下载
如需完整源码,请前往下载。