ffmpeg视频截取
本文采用的截取方式不对视频进行解码,直接在AVPacket级对视频进行截取。
1. 代码
#include <string> #include <stdio.h> extern "C" { #include "libavformat/avformat.h" #include "libavcodec/bsf.h" }; //释放所有上下文信息 void release_context(AVFormatContext **in, AVFormatContext *out, AVIOContext *ctx, AVBSFContext **bsf_ctx_) { //关闭输入 avformat_close_input(in); if (!((out->oformat->flags) & AVFMT_NOFILE)) { //关闭输出 if (avio_close(ctx) < 0) { printf("failed to close output file\n"); return; } } //释放输出AVFormatContext avformat_free_context(out); //释放AVBSFContext av_bsf_free(bsf_ctx_); } int main1(int argc, char *argv[]) { const char *in_filename = "/data/cmhu/test_data/20041609451310232511_20211128090026~1.mp4"; AVFormatContext *in_fmt_ctx = NULL; //打开输入流,创建并初始化输入AVFormatContext if (avformat_open_input(&in_fmt_ctx, in_filename, NULL, NULL) < 0) { printf("failed to open input file\n"); release_context(&in_fmt_ctx, NULL, NULL, NULL); return -1; } //格式化输出输入流信息 av_dump_format(in_fmt_ctx, 0, in_filename, 0); //寻找流的信息以及编解码信息 if (avformat_find_stream_info(in_fmt_ctx, NULL) < 0) { printf("failed to find media stream info\n"); release_context(&in_fmt_ctx, NULL, NULL, NULL); return -1; } AVFormatContext *out_fmt_ctx = NULL; const char *out_filename = "/data/cmhu/jks_aiplatform_projectx/bin/res/test_trans.mp4"; //创建并初始化输出AVFormatContext //不指定AVOutputFormat,ffmpeg会根据格式名或者文件名猜出上下文信息并初始化输出AVFormatContext if (avformat_alloc_output_context2(&out_fmt_ctx, NULL, NULL, out_filename) < 0) { printf("failed to create output format context\n"); release_context(&in_fmt_ctx, out_fmt_ctx, NULL, NULL); return -1; } int video_index = -1; int audio_index = -1; AVStream *in_video_stream = nullptr; AVStream *in_audio_stream = nullptr; //遍历所有输入流 for (int i = 0; i < in_fmt_ctx->nb_streams; i++) { AVStream *in_stream = in_fmt_ctx->streams[i]; enum AVMediaType avMediaType = in_stream->codecpar->codec_type; if (avMediaType == AVMEDIA_TYPE_AUDIO) { audio_index = i; in_audio_stream = in_stream; } else if (avMediaType == AVMEDIA_TYPE_VIDEO) { video_index = i; in_video_stream = in_stream; } //创建输出流 AVStream *out_stream = avformat_new_stream(out_fmt_ctx, NULL); if (!out_stream) { printf("failed to create new stream\n"); release_context(&in_fmt_ctx, out_fmt_ctx, NULL, NULL); return -1; } //拷贝编解码器的参数设置 if (avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar) < 0) { printf("failed to copy codec parameters from src to dst\n"); release_context(&in_fmt_ctx, out_fmt_ctx, NULL, NULL); return -1; } //不同封装格式码流格式不同,所以要将codec_tag设为0 //这样ffmpeg会自动选择和封装格式匹配的码流格式 out_stream->codecpar->codec_tag = 0; } //格式化输出输出流信息 av_dump_format(out_fmt_ctx, 0, out_filename, 1); //判断该上下文是否依赖于输入输出,1:不依赖,0:依赖 if (!((out_fmt_ctx->oformat->flags) & AVFMT_NOFILE)) { //打开输出文件 if (avio_open2(&out_fmt_ctx->pb, out_filename, AVIO_FLAG_WRITE, NULL, NULL) < 0) { printf("failed to open output file\n"); release_context(&in_fmt_ctx, out_fmt_ctx, out_fmt_ctx->pb, NULL); return -1; } } //写入文件头 if (avformat_write_header(out_fmt_ctx, NULL) < 0) { printf("failed to write header\n"); release_context(&in_fmt_ctx, out_fmt_ctx, out_fmt_ctx->pb, NULL); return -1; } AVStream *out_video_stream = out_fmt_ctx->streams[video_index]; AVBSFContext *bsf_ctx = NULL; //获取比特流过滤器 AVCodecParameters *codecpar = in_video_stream->codecpar; const AVBitStreamFilter * filter = nullptr; if(codecpar->codec_id == AV_CODEC_ID_H264){ filter = av_bsf_get_by_name("h264_mp4toannexb"); }else if(codecpar->codec_id == AV_CODEC_ID_HEVC){ filter = av_bsf_get_by_name("hevc_mp4toannexb"); }else { printf("codec_id is not supported!"); release_context(&in_fmt_ctx, out_fmt_ctx, out_fmt_ctx->pb, NULL); return -1; } //创建AVBSFContext if (av_bsf_alloc(filter, &bsf_ctx) < 0) { printf("failed to alloc AVBSFContext\n"); release_context(&in_fmt_ctx, out_fmt_ctx, out_fmt_ctx->pb, &bsf_ctx); return -1; } //拷贝编解码器参数 if (avcodec_parameters_copy(bsf_ctx->par_in, in_video_stream->codecpar) < 0) { printf("failed to copy codec parameters from src to dst\n"); release_context(&in_fmt_ctx, out_fmt_ctx, out_fmt_ctx->pb, &bsf_ctx); return -1; } //初始化AVBSFContext if (av_bsf_init(bsf_ctx) < 0) { printf("failed to init AVBSFContext\n"); release_context(&in_fmt_ctx, out_fmt_ctx, out_fmt_ctx->pb, &bsf_ctx); return -1; } AVRational ctx_time_base = AVRational{ 1, 25 }; //写入流信息 int counting = 0; int video_frame_index = 0; int audio_frame_index = 0; AVPacket* pkt ; pkt = av_packet_alloc(); av_init_packet( pkt ); while (true) { //读取一帧视频或者几帧音频 int ret = av_read_frame(in_fmt_ctx, pkt); if (ret < 0) { if (ret == AVERROR_EOF) { printf("end of file\n"); //取消对缓缓区的引用,将packet其他字段置为默认值 av_packet_unref(pkt); break; } printf("failed to read frame\n"); av_packet_unref(pkt); release_context(&in_fmt_ctx, out_fmt_ctx, out_fmt_ctx->pb, NULL); return -1; } //限定为输出格式为avi且是视频流 if (video_index == pkt->stream_index){ if ((pkt->flags & AV_PKT_FLAG_KEY) != 0) { counting++; } if (counting < 1) { continue; } if (counting > 2) { break; } video_frame_index++; //将输入时间基表示的时间戳转换为输出时间基表示 pkt->pts = av_rescale_q_rnd(video_frame_index, ctx_time_base, out_video_stream->time_base, (enum AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX)); pkt->dts = av_rescale_q_rnd(video_frame_index, ctx_time_base, out_video_stream->time_base, (enum AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX)); pkt->duration = av_rescale_q(1, ctx_time_base, out_video_stream->time_base); pkt->stream_index = out_video_stream->index; //将数据送入过滤器 ret = av_bsf_send_packet(bsf_ctx, pkt); if (ret < 0) { //单个packet不足以完成过滤,需要继续送入数据 if (ret == AVERROR(EAGAIN)) { av_packet_unref(pkt); continue; } printf("failed to send pakcet to bit stream filter\n"); av_packet_unref(pkt); release_context(&in_fmt_ctx, out_fmt_ctx, out_fmt_ctx->pb, &bsf_ctx); return -1; } //接收过滤后的数据 //由于单个输入packet可能产生多个输出packet,因此需要使用循环 int ans = 0; do { //获取过滤后的数据 ans = av_bsf_receive_packet(bsf_ctx, pkt); if (ans < 0) { if (ans == AVERROR_EOF) { av_packet_unref(pkt); break; } if (ans != AVERROR(EAGAIN)) { printf("failed to reveive filtered packet\n"); av_packet_unref(pkt); release_context(&in_fmt_ctx, out_fmt_ctx, out_fmt_ctx->pb, &bsf_ctx); return -1; } } //交错写入 if (av_interleaved_write_frame(out_fmt_ctx, pkt) < 0) { printf("failed to write frame\n"); av_packet_unref(pkt); release_context(&in_fmt_ctx, out_fmt_ctx, out_fmt_ctx->pb, &bsf_ctx); return -1; } } while (ans == AVERROR(EAGAIN)); //**********************过滤结束**************************** } else if (audio_index == pkt->stream_index){ //交错写入 if (av_interleaved_write_frame(out_fmt_ctx, pkt) < 0) { printf("failed to write frame\n"); av_packet_unref(pkt); release_context(&in_fmt_ctx, out_fmt_ctx, out_fmt_ctx->pb, &bsf_ctx); return -1; } } // av_packet_unref(pkt); } //写入文件尾 if (av_write_trailer(out_fmt_ctx) != 0) { printf("failed to write tail\n"); release_context(&in_fmt_ctx, out_fmt_ctx, out_fmt_ctx->pb, &bsf_ctx); return -1; } release_context(&in_fmt_ctx, out_fmt_ctx, out_fmt_ctx->pb, &bsf_ctx); return 0; }
2. 在实际使用中的一些注意点
(1)保存需要从关键帧开始保存,从非关键帧开始保存,结果的开头部分会播放异常
(2)一个与本代码关联不大,但是比较通用但是容易出错的点:在拷贝AVPacket时,如果想要做的事深拷贝,不要用ffmpeg自带的一些拷贝操作(除非你彻底的了解你用的接口做的是深拷贝),本人在此处被坑过很久,ffmpeg自带的一些复制、拷贝、克隆都是浅拷贝,在一处释放后会把其他处的也给释放,从而导致程序出现严重问题。我的做法是直接用malloc和memcpy根据pkt->data、pkt->size进行拷贝即可实现深拷贝数据