问题场景:
使用GPU编码(Opencv)生成的h264视频片段中不包含时间戳信息,且含有B帧,直接合成mp4后会导致播放出现问题(瞬间播放完成)。因此,在合成时需要手动添加时间戳。
心路历程:
发现生成的视频会瞬间播放完成后,意识到是时间戳的问题,检查时间戳代码:
while (av_read_frame(inputFormatContext, &packet) >= 0) {
if (packet.pts == AV_NOPTS_VALUE){
// 判断pts是否为空
// 添加时间戳...
}
}
一开始没注意到有B帧,想着把pts、dts设置相等,并且按照间隔递增就可以了:
AVStream *inStream = inputFormatContext->streams[packet.stream_index];
AVStream *outStream = outputFormatContext->streams[packet.stream_index];
// 计算两帧之间的间隔,以输入流的时间基计算
int64_t frameDuration = av_rescale_q(1, av_inv_q(inStream->time_base), inStream->r_frame_rate);
// 转换时间基为输出流的时间基
int64_t _t = av_rescale_q(frameDuration, inStream->time_base, outStream->time_base);
// 当前帧的时间戳等于上一帧的时间戳加上两帧之间的间隔
packet.pts = last_pts + _t;
// dts设置成一样
packet.dts = packet.pts;
对于只有I和P帧的h264流,上述方法可以领流正常播放,但如果h264中有B帧,则会导致在某些播放器上播放异常——帧显示顺序错误,物体“颤抖”着移动。
对于含有B帧的视频,其dts和pts应该是按照这种顺序进行的:
帧 | I | B | P | B | P | B | P |
dts | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
pts | 1 | 3 | 2 | 5 | 4 | 7 | 6 |
注:dts需要递增,解码需要按照顺序解码。任何一帧的pts应大于等于dts,因为需要先解码后才能显示。因此上述需要调整:
帧 | I | B | P | B | P | B | P |
dts | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
pts | 2 | 4 | 3 | 6 | 5 | 8 | 7 |
为了使视频打开时就有画面能播放,因此需要将第一帧的pts调整为0,dts是可以为负数的,但需要保持递增。调整后:
帧 | I | B | P | B | P | B | P |
dts | -1 | 0 | 1 | 2 | 3 | 4 | 5 |
pts | 0 | 2 | 1 | 4 | 3 | 6 | 5 |
以上是最简单的例子,也可以推导出变形:
帧 | I | B | B | P | B | B | P |
dts | -2 | -1 | 0 | 1 | 2 | 3 | 4 |
pts | 0 | 2 | 3 | 1 | 5 | 6 | 4 |
因此,时间戳的规律和我们的视频中有多少连续的B帧有关。
以上讨论的假设都是一个帧组中只有一个I帧的情况下,如果一个GOP中有多个I帧时情况会更复杂一点(没做整理,本文暂不讨论):
帧 | I(IDR) | B | P | B | P | B | I | B | P | B | P | I(IDR) | ... |
dts | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | |
pts | 1 | 3 | 2 | 5 | 4 | 7 | 6 | 9 | 8 | 11 | 10 | 12 |
总结:加时间戳的前提是要知道h264文件帧的类型结构(是否有B帧、连续的B帧数量、一个GOP是否有多个I帧)
后记:
由于我的文件都是我自己生成的,帧结构都是统一的,因此可以轻松的找出添加时间戳的规律。但对于不同帧结构的原始流,找出统一规律还是比较麻烦,本文不再阐述。
附对于最简单的含有B帧的处理代码(IBPBPBPBP):
// 将多个h264文件合并成mp4文件
int mutexMp4File(){
// 创建输出文件
// m_outputFile:输出文件名 xxx.mp4
AVFormatContext* outputFormatContext = nullptr;
if (avformat_alloc_output_context2(&outputFormatContext, nullptr, nullptr, m_outputFile.c_str()) < 0) {
return -1;
}
// 准备工作:遍历输入文件列表
// m_inputFiles:输入文件名列表
for (const auto& inputFile : m_inputFiles) {
// 打开输入文件
AVFormatContext* inputFormatContext = nullptr;
if (avformat_open_input(&inputFormatContext, inputFile.c_str(), nullptr, nullptr) != 0) {
return -1;
}
// 查找输入文件的流信息
if (avformat_find_stream_info(inputFormatContext, nullptr) < 0) {
avformat_close_input(&inputFormatContext);
avformat_free_context(outputFormatContext);
return -1;
}
// 复制输入文件的流到输出文件
for (unsigned int i = 0; i < inputFormatContext->nb_streams; ++i) {
// 获取输入流
AVStream* inputStream = inputFormatContext->streams[i];
// 创建输出流
AVStream* outputStream = avformat_new_stream(outputFormatContext, nullptr);
if (outputStream == nullptr) {
avformat_close_input(&inputFormatContext);
avformat_free_context(outputFormatContext);
return -1;
}
// 复制输入流的编解码器参数到输出流
if (avcodec_parameters_copy(outputStream->codecpar, inputStream->codecpar) < 0) {
avformat_close_input(&inputFormatContext);
avformat_free_context(outputFormatContext);
return -1;
}
// 设置输出流的编解码器标志为"copy"
outputStream->codecpar->codec_tag = 0;
}
// 关闭输入文件
avformat_close_input(&inputFormatContext);
}
// 打开输出文件
AVIOContext* outputIOContext = nullptr;
if (avio_open(&outputFormatContext->pb, m_outputFile.c_str(), AVIO_FLAG_WRITE) < 0) {
avformat_free_context(outputFormatContext);
return -1;
}
// 写入文件头部
if (avformat_write_header(outputFormatContext, nullptr) < 0) {
avformat_free_context(outputFormatContext);
return -1;
}
int64_t gdts_notime = 0;//拼接多个文件的时间戳
// 遍历输入文件列表,写入数据到输出文件
for (const auto& inputFile : m_inputFiles) {
// 打开输入文件
AVFormatContext* inputFormatContext = nullptr;
if (avformat_open_input(&inputFormatContext, inputFile.c_str(), nullptr, nullptr) != 0) {
avformat_free_context(outputFormatContext);
return -1;
}
// 查找输入文件的流信息
if (avformat_find_stream_info(inputFormatContext, nullptr) < 0) {
avformat_close_input(&inputFormatContext);
avformat_free_context(outputFormatContext);
return -1;
}
// 从输入文件读取数据并写入输出文件
AVPacket packet;
int64_t p_max_dts = 0;
int i = 0;
while (av_read_frame(inputFormatContext, &packet) >= 0) {
AVStream *inStream = inputFormatContext->streams[packet.stream_index];
AVStream *outStream = outputFormatContext->streams[packet.stream_index];
if (packet.pts == AV_NOPTS_VALUE){
// 起始 只能针对 IBPBPBP 连续B帧为1的流
int nalu_type = 0,startIndex = 0;
if (packet.data[0]==0x00 && packet.data[1]==0x00 && packet.data[2]==0x01){
nalu_type = 0x1f & packet.data[3];
startIndex = 3;
}else if(packet.data[0]==0x00 && packet.data[1]==0x00 && packet.data[2]==0x00 && packet.data[3]==0x01){
nalu_type = 0x1f & packet.data[4];
startIndex = 4;
}
int64_t frameDuration = av_rescale_q(1, av_inv_q(inStream->time_base), inStream->r_frame_rate);
int64_t _t = av_rescale_q(frameDuration, inStream->time_base, outStream->time_base);
p_max_dts = _t*(i+1);
packet.dts = p_max_dts + gdts_notime - _t;
int xt = (int)packet.data[startIndex+1] & 0xC0;
if (nalu_type == 0x05 || nalu_type == 0x06 || nalu_type == 0x07 || nalu_type == 0x08){ //I帧开头
packet.pts = packet.dts + _t;
}else if(nalu_type == 0x01 && xt == 0x80){ //P帧
packet.pts = packet.dts;
}else if(nalu_type == 0x01 && xt == 0xC0){ //B帧
packet.pts = packet.dts + _t*2;
}else{
// Error!
packet.pts = packet.dts;
}
}else{
// Error!
}
// 将包的流索引设置为输出流索引
packet.stream_index = inStream->index;
// 写入输出文件
av_interleaved_write_frame(outputFormatContext, &packet);
av_packet_unref(&packet);
i++;
}
gdts_notime += p_max_dts;
// 关闭输入文件
avformat_close_input(&inputFormatContext);
}
// 写入文件尾部
av_write_trailer(outputFormatContext);
// 关闭输出文件
avio_close(outputFormatContext->pb);
// 释放资源
avformat_free_context(outputFormatContext);
return 0;
}
代码中的求帧类型的方法是旁门左道且有针对性的,可能不适合其他场景。
文章来源:https://www.toymoban.com/news/detail-811101.html
对于文中任何错误,欢迎指正。文章来源地址https://www.toymoban.com/news/detail-811101.html
到了这里,关于[解决思路]关于h264裸流合成mp4时时间戳添加问题的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!