前言
有时会碰到这样的需求场景,对一个视频中的某一段感兴趣,想要精确的截取这一段视频以及对应的音频。例如,有一个25fps的MP4的文件,时长20秒,我想要截取从5秒开始到15秒结束的视频以及对应的音频,这里有两点需要说明: 1、对于视频:开始时间5秒,结束时间15秒。只能做到尽量接近,因为源文件25fps,即每一帧的显示间隔为0.04秒,可能5秒附近的视频帧刚好在5.012秒,最大误差一帧时间差就是1/25==0.04秒,所以4.96-5.04秒范围内都可以。 2、对于音频:比如采样率44100,每一个AVPacket包含1024个采样,那么每一帧的显示时间间隔为1/43≈0.025秒,音频再5秒处的最大误差为0.025秒,所以4.975-5.025秒范围内都可以。
这里以MP4文件为例,假设视频的编码方式为H264,音频为aac,其它格式类似。
实现思路分析
1、在ffmpeg解封装之后得到的AVPacket代表着压缩的音/视频数据,该结构体内有一个字段pts,表示了该音/视频的时间,即前面说的5秒就是要参考这个时间。 2、对于音频来说,只需要依次取出5秒(或者最接近5秒)到15秒处的AVPacket,然后调用ffmpeg封装接口依次写入新的MP4文件即可 3、对于视频来说,写入MP4的第一个AVPacket一定要是I帧(对于H264等有IPB帧改变的编码方式来说,其它编码方式则跟音频一样处理),所以视频的处理分两种情况:
-
当5秒(或者最接近5秒)处的AVPacket刚好为I帧,那么只需依次取出直到15秒处的AVpacket,然后依次写入MP4文件即可;
-
当5秒处的AVPacket非I帧,那么就需要往前找出最近一个I帧的AVPacket,然后用这个AVPacket往后依次解码,直到解码出5秒处的AVPacket,然后将解码得到AVFrame重新进行编码为AVPacket(那么5秒处的AVPacket就一定是I帧了)再写入MP4文件,5秒后的视频也按照先解码再编码为AVPacket再写入MP4文件的思路进行
流程图
关键函数
-
int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp, int flags);
调用av_read_frame()函数时,其内部会有一个文件指针(默认情况下指向文件中的首个AVPacket),所以默认时读取的第一个AVPacket就是文件中的首个AVPacket,调用结束后指针再指向下一个AVPacket。此函数的作用就相当于将指针指向指定的key_frame的AVPacket,那么首次调用av_read_frame()函数时将获取的是满足要求的AVPacket,而非文件的第一个AVPacket。
1、stream_index:代表音视频流的索引,如果为-1,那么将对文件的默认流索引进行操作 2、timestamp:移动到指定的时间戳(单位为AVStream.time_base )。如果stream_index为-1,单位为(AV_TIME_BASE) 3、flag:移动参考的方式,取值如下: #define AVSEEK_FLAG_BACKWARD 1 ///< seek backward #define AVSEEK_FLAG_BYTE 2 ///< seeking based on position in bytes #define AVSEEK_FLAG_ANY 4 ///< seek to any frame, even non-keyframes #define AVSEEK_FLAG_FRAME 8 ///< seeking based on frame number
AVSEEK_FLAG_BACKWARD 基于前面指定的时间戳参数查找,如果timestamp处的AVPacket的key_frame非1,那么就往前找,直到找到最近一个key_frame为1的AVPacket
AVSEEK_FLAG_BYTE 基于位置进行查找
AVSEEK_FLAG_ANY 基于前面指定的时间戳参数进行查找,不管AVPacket是否key_frame为1都返回
AVSEEK_FLAG_FRAME 基于帧编号进行查找
实现代码
#include <stdio.h> #include <string> #include <iomanip> #include <chrono> #include "CLog.h" #include <iostream> extern "C" { #include <libavutil/avutil.h> #include <libavcodec/avcodec.h> #include <libavutil/timestamp.h> #include <libavformat/avformat.h> #include <libavutil/opt.h> } using namespace std; // 打开 .hpp line 33 .cpp line 100 class Cut { public: /** 实现时间的精准裁剪 */ void doCut(); private: int64_t start_pos; int64_t video_start_pts; int64_t audio_start_pts; int64_t duration; // 时长 单位秒 int64_t video_next_pts; int64_t audio_next_pts; // 源文件 string srcPath; // 目标文件 string dstPath; // 视频流索引 int video_in_stream_index,video_ou_tream_index; // 音频流索引 int audio_in_stream_index,audio_ou_stream_index; bool has_writer_header; // 用于解封装 AVFormatContext *in_fmtctx; // 用于封装 AVFormatContext *ou_fmtctx; // 视频编码和解码用 AVFrame *video_de_frame; AVFrame *video_en_frame; AVCodecContext *video_de_ctx;