ffmpeg 实现多视频轨录制到同一个文件
引言
在视频录制中,有时会碰到这样一个需求,将不同摄像头的画面写入到一个视频文件,这个叫法很多,有的厂家叫合流模式,有的叫多画面多流模式。无论如何,它们的实质都是在一个视频文件上实现多路不同分辨率视频的保存。
经过调查,支持这种需求封装格式的有MP4、MOV、MKV 等,这里因为MP4 格式应用最广泛。
原理
ffmpeg 有一个map命令,可以将多路视频轨封装在一个视频容器,掰ffmpeg源码发现其实新建一个新的AVStream,修改stream->index,就可以实现多流录制的目的。
ffmpeg -i input.mp4 -i test.mp4 -map 0:v:0 -map 1:v -map 0:a -map 1:a -c copy -y mix.mp4
#include <iostream>
#include <string>
extern "C"
{
#include <libavutil/timestamp.h>
#include <libavformat/avformat.h>
}
typedef struct
{
char* file_name;
AVFormatContext* fmt_ctx;
int video_index;
int audio_index;
int source_index;
double last_pts[2];
double last_dts[2];
AVRational video_time_base;
AVRational audio_time_base;
bool is_end;
}InputStream;
int create_stream(InputStream *input_stream, AVFormatContext* out_fmt_ctx, int &stream_num)
{
int i = 0;
int ret = 0;
if ((ret = avformat_open_input(&input_stream->fmt_ctx, input_stream->file_name, 0, 0)) < 0) //打开输出文件
{
fprintf(stderr, "Could not open input file '%s'", input_stream->file_name);
goto end;
}
if ((ret = avformat_find_stream_info(input_stream->fmt_ctx, 0)) < 0) //打开输入
{
fprintf(stderr, "Failed to retrieve input stream information");
goto end;
}
//av_dump_format(input_stream->fmt_ctx, 0, input_stream->file_name, 0);//打印信息
for (size_t i = 0; i < input_stream->fmt_ctx->nb_streams; i++)
{
AVStream* in_stream = input_stream->fmt_ctx->streams[i];
AVStream* out_stream = avformat_new_stream(out_fmt_ctx, in_stream->codec->codec);
if (in_stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
{
input_stream->audio_index = i;
input_stream->audio_time_base = in_stream->time_base;
printf("流 %d 是音频 \n", input_stream->source_index + i);
}
if (in_stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{
input_stream->video_index = i;
input_stream->video_time_base = in_stream->time_base;
printf("流 %d 是视频 \n", input_stream->source_index + i);
}
if (!out_stream)
{
fprintf(stderr, "Failed allocating output stream\n");
goto end;
}
ret = avcodec_copy_context(out_stream->codec, in_stream->codec);
if (ret < 0)
{
fprintf(stderr, "Failed to copy context from input to output stream codec context\n");
goto end;
}
out_stream->codec->codec_tag = 0;
if (out_fmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
out_stream->codec->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
stream_num++;
}
end:
return ret;
}
int write_a_channel(InputStream *stream, AVFormatContext *out_fmt_ctx)
{
int ret = -1;
AVPacket packet;
ret = av_read_frame(stream->fmt_ctx, &packet);
if (ret < 0)
{
if (ret == AVERROR_EOF)
{
//printf("readFrame报错 %d\n", ret);
av_free_packet(&packet);
return ret;
}
}
AVRational time_base = {0};
AVStream* in_stream = stream->fmt_ctx->streams[packet.stream_index];
AVStream* out_stream = out_fmt_ctx->streams[packet.stream_index];
packet.stream_index = stream->source_index + packet.stream_index;
if (in_stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{
time_base = stream->video_time_base;
}
if (in_stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
{
time_base = stream->audio_time_base;
}
packet.pts = av_rescale_q_rnd(packet.pts, time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
packet.dts = av_rescale_q_rnd(packet.dts, time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
if (packet.pts < 0)
{
packet.pts = 0;
}
if (packet.dts < 0)
{
packet.dts = 0;
}
//printf("%d, pts %d dts %d \n", packet.stream_index, packet.pts, packet.dts);
ret = av_interleaved_write_frame(out_fmt_ctx, &packet);
if (ret < 0)
{
printf("写入错误\n");
}
av_free_packet(&packet);
return ret;
}
int main(int argc, char** argv)
{
int ret = -1;
char file_path[100][100] = {"D:\\素材\\test1.mp4", "D:\\素材\\test2.mp4", "D:\\素材\\test3.mp4", "D:\\素材\\test4.mp4"};
int count = sizeof(file_path) / sizeof(char);
const char *out_fileName = "my_muxing.mp4";
av_register_all();
AVOutputFormat* ofmt = NULL;
AVFormatContext* out_fmt_ctx = NULL;
const int channel_num = 4;
avformat_alloc_output_context2(&out_fmt_ctx, NULL, NULL, out_fileName);
//创建streams
InputStream stream_arry[channel_num] = {0};
int stream_index = 0;
for (int i = 0; i < channel_num; i++)
{
int stream_num = 0;
stream_arry[i].file_name = file_path[i];
stream_arry[i].source_index = stream_index;
ret = create_stream(&stream_arry[i], out_fmt_ctx, stream_index);
stream_index += stream_num;
}
if (!out_fmt_ctx)
{
return -1;
}
ofmt = out_fmt_ctx->oformat;
if (!(ofmt->flags & AVFMT_NOFILE))
{
ret = avio_open(&out_fmt_ctx->pb, out_fileName, AVIO_FLAG_WRITE);
if (ret < 0)
{
fprintf(stderr, "Could not open output file '%s'", out_fileName);
return -1;
}
}
AVDictionary* opts = NULL;
av_dict_set(&opts, "movflags", "frag_keyframe+empty_moov", 0);//fmp4输出
ret = avformat_write_header(out_fmt_ctx, &opts);
if (ret < 0)
{
fprintf(stderr, "Error occurred when opening output file\n");
return -1;
}
while (true)
{
int finish_count = 0;
for (int i = 0; i < channel_num; i++)
{
if (!stream_arry[i].is_end)
{
ret = write_a_channel(&stream_arry[i], out_fmt_ctx);
if (ret == AVERROR_EOF)
{
stream_arry[i].is_end = true;
finish_count++;
}
else
{
continue;
}
}
}
if (finish_count >= channel_num - 1)
{
break;
}
}
//清理
for (size_t i = 0; i < channel_num; i++)
{
avformat_close_input(&stream_arry[i].fmt_ctx);
}
ret = av_write_trailer(out_fmt_ctx);
printf("==============合并完毕,写文件尾部 %d=============\n", ret);
return 0;
}
设置flags 避免播放器拖动的时候出现花屏。
packet->flags = isKeyFrame ? packet->flags | AV_PKT_FLAG_KEY : packet->flags;