ffmpeg音视频裁剪

音视频裁剪,通常会依据时间轴为基准,从某个起始点到终止点的音视频截取出来,当然音视频文件中存在多路流,所对每一组流进行裁剪

基础概念:

编码帧的分类:

I帧 (Intra coded frames): 关键帧,采用帧内压缩技术,所占数据的信息量比较大,I帧不需要参考其他画面而生成,解码时仅靠自己就重构完整图像;

P 帧 (forward Predicted frames): 向前参考帧,根据本帧与相邻的前一帧(l帧或P帧)的不同点来压缩本帧数据,同时利用了空间和时间上的相关性。压缩时,只参考前面已经处理的帧(I帧或P帧),采用帧间压缩技术。它占I帧的一半大小

B 帧 (Bidirectional predicted frames): 双向参考帧,B 帧图像采用双向时间预测,可以大大提高压缩倍数。压缩时,既参考前面已经处理的帧,也参考后面的帧,帧间压缩技术。它占I帧四分之一大小。

I帧图像是周期性出现在图像序列中的,出现频率可由编码器选择;I帧是P帧和B帧的参考帧(其质量直接影响到同组中以后各帧的质量);I帧是帧组GOP(Group of Pictures)的基础帧(第一帧),且每组只有一个I帧。

对于一个视频文件,帧的显示顺序:IBBP,但是帧的存储方式可能是:IPBB。现在我们需要在显示B帧之前知道P帧中的信息,这时就需要一个解码时间戳(dts(Decoding Time Stamp))和一个显示时间戳(pts(Presentation Time Stamp))。解码时间戳告诉我们什么时候需要解码,显示时间戳告诉我们什么时间需要显示。通常pts和dts只有在流中有B帧的时候才不同。

FFmpeg中用AVPacket结构体来描述解码前、后的压缩包 ,用AVFrame结构体来描述解码后、前的信号帧。 对于视频来说,AVFrame就是视频的一帧图像。这帧图像什么时候显示给用户,就取决于它的PTS。DTS是AVPacket里的一个成员,表示这个压缩包应该什么时候被解码。 如果视频里各帧的编码是按输入顺序(也就是显示顺序)依次进行的,那么解码和显示时间应该是一致的。可事实上,在大多数编解码标准(如H.264或HEVC)中,编码顺序和输入顺序并不一致,于是才会需要PTS和DTS这两种不同的时间戳。所以视频流中的时间总是pts(显示时间) >= dts(解码时间)。

ffmpeg中时间相关时间单位:

ffmepg中的内部计时单位(时间基),ffmepg中的所有时间都是于它为一个单位,比如AVStream中的duration即以为着这个流的长度为duration个AV_TIME_BASE。AV_TIME_BASE定义为:

cpp 复制代码
#define         AV_TIME_BASE   1000000

ffmpeg提供了一个把AVRatioal结构转换成double的函数:

cpp 复制代码
static inline double av_q2d(AVRational a){
/**
* Convert rational to double.
* @param a rational to convert
**/
    return a.num / (double) a.den;
}

可以根据pts来计算一桢在整个视频中的时间位置:

cpp 复制代码
timestamp(秒) = pts * av_q2d(st->time_base);    //这里的st是一个AVStream对象指针。

计算视频长度的方法:

cpp 复制代码
time(秒) = st->duration * av_q2d(st->time_base);    // 这里的st是一个AVStream对象指针。

时间基转换公式

  • timestamp(ffmpeg内部时间戳) = AV_TIME_BASE * time(秒)
  • time(秒) = AV_TIME_BASE_Q * timestamp(ffmpeg内部时间戳)

所以当需要把视频跳转到N秒的时候可以使用下面的方法:

cpp 复制代码
int64_t timestamp = N * AV_TIME_BASE; // N秒转换为内部时间戳

av_seek_frame(fmtctx, index_of_video, timestamp, AVSEEK_FLAG_BACKWARD);    //  // AVSEEK_FLAG_BACKWARD 向后找到I帧

不同时间基之间的转换函数(作用是计算a * bq / cq,来把时间戳从一个时基调整到另外一个时基。在进行时基转换的时候,我们应该首选这个函数,因为它可以避免溢出的情况发生。)

cpp 复制代码
int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq)

裁剪音视频代码实例:

cpp 复制代码
//裁剪多媒体文件(因为视频存在I帧B帧P帧,所以裁剪结果和输入时长有误差)

//编译链接:gcc -o cut cut.c `pkg-config --libs --cflags libavutil libavformat libavcodec`

//执行 ./cut test.mp4 cut.mp4  (starttime)  (endtime)(单位秒)

#include<stdio.h>
#include<stdlib.h>
#include<libavutil/log.h>
#include <libavformat/avformat.h>


int main(int argc, char* argv[])
{

	int ret = -1;
	int idx = -1;
	int i = 0;
	int stream_idx = 0;

	// 处理输入参数
	char* src, * dst;
	double starttime, endtime;
	int64_t* dts_start_time, * pts_start_time;

	int* stream_map = NULL;

	AVFormatContext* pFmtCtx = NULL;	// 多媒体上下文
	AVFormatContext* oFmtCtx = NULL;	// 目标文件上下文信息

	const AVOutputFormat* outFmt = NULL;		// 输出文件格式信息

	AVPacket pkt;		// 包


	av_log_set_level(AV_LOG_DEBUG);
	if (argc < 5) {	//该可执行程序  源文件   目标文件 起始时间 结束时间
		av_log(NULL, AV_LOG_INFO, "Arguments must be more than 5.");
		exit(-1);
	}
	src = argv[1];
	dst = argv[2];
	starttime = atof(argv[3]);
	endtime = atof(argv[4]);
	if (endtime < starttime) {
		av_log(NULL, AV_LOG_INFO, "Cut time error!.");
		exit(-1);
	}

	// 打开多媒体文件(包含文件头和文件体)
	if ((ret = avformat_open_input(&pFmtCtx, src, NULL, NULL)))
	{
		av_log(NULL, AV_LOG_ERROR, "%s\n", av_err2str(ret));
		exit(-1);
	}



	// 打开目的文件的上下文
	avformat_alloc_output_context2(&oFmtCtx, NULL, NULL, dst);
	if (!oFmtCtx) {
		av_log(NULL, AV_LOG_ERROR, "NO Memory!\n");
		goto _ERROR;
	}

	stream_map = av_calloc(pFmtCtx->nb_streams, sizeof(int));
	if (!stream_map) {
		av_log(NULL, AV_LOG_ERROR, "NO Memory!\n");
		goto _ERROR;
	}

	// 遍历源文件每一条流
	for (i = 0; i < pFmtCtx->nb_streams; i++) {
		AVStream* outStream = NULL;
		AVStream* inStream = pFmtCtx->streams[i];
		AVCodecParameters* inCodecPar = inStream->codecpar;

		// 只处理音、视频、字幕数据
		if (inCodecPar->codec_type != AVMEDIA_TYPE_AUDIO &&
			inCodecPar->codec_type != AVMEDIA_TYPE_VIDEO &&
			inCodecPar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
			stream_map[i] = -1;
			continue;
		}
		stream_map[i] = stream_idx++;

		// 为目的文件创建一个新的视频流
		outStream = avformat_new_stream(oFmtCtx, NULL);
		if (!outStream) {
			av_log(oFmtCtx, AV_LOG_ERROR, "NO Memory!\n");
			goto _ERROR;
		}

		avcodec_parameters_copy(outStream->codecpar, inStream->codecpar);	//将源文件的内容复制到目的文件 
		outStream->codecpar->codec_tag = 0;	// 根据多媒体文件自动识别编解码器


	}

	//上下文信息与输出文件绑定
	ret = avio_open2(&oFmtCtx->pb, dst, AVIO_FLAG_WRITE, NULL, NULL);
	if (ret < 0) {
		av_log(NULL, AV_LOG_ERROR, "%s", av_err2str(ret));
		goto _ERROR;

	}

	// 写多媒体文件头(包含多媒体的类型、版本等信息)到目标文件
	ret = avformat_write_header(oFmtCtx, NULL);
	if (ret < 0) {
		av_log(oFmtCtx, AV_LOG_ERROR, "%s", av_err2str(ret));
		goto _ERROR;

	}

	// 跳转到时间点
	ret = av_seek_frame(pFmtCtx, -1, starttime * AV_TIME_BASE, AVSEEK_FLAG_BACKWARD); // AVSEEK_FLAG_BACKWARD 向后找到I帧
	if (ret < 0) {
		av_log(oFmtCtx, AV_LOG_ERROR, "%s", av_err2str(ret));
		goto _ERROR;
	}

	// 记录第一个包的时间戳
	dts_start_time = av_calloc(pFmtCtx->nb_streams, sizeof(int64_t));
	pts_start_time = av_calloc(pFmtCtx->nb_streams, sizeof(int64_t));
	for (int t = 0; t < pFmtCtx->nb_streams; t++) {
		dts_start_time[t] = -1;
		pts_start_time[t] = -1;
	}

	// 从源多媒体文件中读到音、视频、字幕数据
	while (av_read_frame(pFmtCtx, &pkt) >= 0) {  // 从多媒体文件读取到帧数据,读取码流中的音频若干帧或者视频一帧
		AVStream* inStream, * outStream;

		// 记录每组流截取开始的时间戳
		if (dts_start_time[pkt.stream_index] == -1 && pkt.dts > 0) {
			dts_start_time[pkt.stream_index] = pkt.dts;
		}
		if (pts_start_time[pkt.stream_index] == -1 && pkt.pts > 0) {
			pts_start_time[pkt.stream_index] = pkt.pts;
		}


		inStream = pFmtCtx->streams[pkt.stream_index];
		if (av_q2d(inStream->time_base) * pkt.pts > endtime) {	// 结束时间
			av_log(oFmtCtx, AV_LOG_INFO, "cut success!\n");
			break;
		}
		if (stream_map[pkt.stream_index] < 0) {		// 流编号为-1, 不是音、视频、字幕流数据
			av_packet_unref(&pkt);	// 释放packet
			continue;
		}
		
		// 相对时间
		pkt.pts = pkt.pts - pts_start_time[pkt.stream_index];
		pkt.dts = pkt.dts - dts_start_time[pkt.stream_index];
		if (pkt.dts > pkt.pts) {	// 音频dts、pts 相等,视频的pts >= dts
			pkt.pts = pkt.dts;
		}

		pkt.stream_index = stream_map[pkt.stream_index];

		outStream = oFmtCtx->streams[pkt.stream_index];
		av_packet_rescale_ts(&pkt, inStream->time_base, outStream->time_base);	// 修改时间戳
		
		pkt.pos = -1;			// 偏移位置
		av_interleaved_write_frame(oFmtCtx, &pkt);		// 将视频帧写入目标文件中
		av_packet_unref(&pkt);

	}
	// 写多媒体文件尾到文件中
	av_write_trailer(oFmtCtx);

	// 将申请的资源释放掉
_ERROR:
	if (pFmtCtx) {
		avformat_close_input(&pFmtCtx);
		pFmtCtx = NULL;
	}
	if (oFmtCtx->pb) {
		avio_close(oFmtCtx->pb);
	}
	if (oFmtCtx) {
		avformat_free_context(oFmtCtx);
		oFmtCtx = NULL;
	}
	if (stream_map) {
		av_free(stream_map);
	}
	if (dts_start_time) {
		av_free(dts_start_time);
	}
	if (pts_start_time) {
		av_free(pts_start_time);
	}
	return 0;
}

参考:

ffmpeg中的时间单位_pkt.duration的值-CSDN博客

https://blog.51cto.com/moonfdd/6266754?articleABtest=0

相关推荐
阿道夫小狮子1 天前
android 音频抢占问题
android·音视频
光锥智能1 天前
火山引擎发布豆包大模型1.8和音视频创作模型Seedance 1.5 pro
音视频·火山引擎
lusasky1 天前
批量压缩对象存储中视频
音视频
千殇华来1 天前
音频基础知识(一)
音视频
山西茄子1 天前
Issac sim 做测试视频
音视频·deepstream
Black蜡笔小新1 天前
视频汇聚平台EasyCVR如何赋能重塑安防与物联可视化
音视频
daidaidaiyu1 天前
FFmpeg 关键的结构体
c++·ffmpeg
好游科技2 天前
语聊APP新生态!一站式语聊房语音直播APP源码开发搭建
音视频·webrtc·im即时通讯·社交软件·社交语音视频软件
summerkissyou19872 天前
Android-Audio-为啥不移到packages/module
android·音视频
骄傲的心别枯萎2 天前
RV1126 NO.56:ROCKX+RV1126人脸识别推流项目之VI模块和VENC模块讲解
人工智能·opencv·计算机视觉·音视频·rv1126