概述
本篇博客主要记录如何使用ffmpeg对视频进行裁剪,主要包括两个部分:
- Seek到开始裁剪的位置,解析原始视频文件
- 将视频帧和音频帧通过ffmpeg api写入输出文件
代码示例
例子的地址
例子为CMake项目,目前只在MacOS上运行。运行项目中的Target MediaFileClip
,可查看效果
读取原始数据
读取的方法参考 基于FFmpeg和SDL2的简易播放器 ,代码在 MediaFileReader
中。 相对于上一篇,做了一些改动和优化
- 支持了seek
- pullFrame会从decoder中读取缓存的数据,直到没有读完位置
关于seek
通过av_seek_frame
可以seek到指定时间戳,可以通过2种方式指定时间戳
- 指定具体的流索引,此时的时间戳是基于这条流的timebase
- 不指定具体的流索引,此时的时间戳是基于AV_TIME_BASE
比如想要索引到第3秒,2种写法分别是
// 通过音频流索引,audioStreamIdx是音频流索引值,timeInSecs是要seek到的秒数
auto audioTimeBase = formatCtx->streams[audioStreamIdx]->time_base;
int64_t timestamp = (int64_t)(timeInSecs * audioTimeBase.den / audioTimeBase.num);
int ret = av_seek_frame(formatCtx, audioStreamIdx, timestamp, AVSEEK_FLAG_BACKWARD);
// 不指定流索引,timeInSecs是要seek到的秒数
int64_t timestamp = (int64_t)(timeInSecs * AV_TIME_BASE);
int ret = av_seek_frame(formatCtx, -1, timestamp, AVSEEK_FLAG_BACKWARD);
你可能注意到了,我使用了AVSEEK_FLAG_BACKWARD
,意思是seek到指定秒数前的一个关键帧。当我们不指定任何flag时,seek并不会那么精准,为了保证解码正常,ffmpeg只会seek到关键帧,这就导致我想从3秒截取,可能ffmpeg会seek到5s。为了解决这个问题,使用AVSEEK_FLAG_BACKWARD
,seek到指定秒数前的一个关键帧,然后顺序向后读取Packet,直到开始裁剪的时间点。
关于decoder缓存
在做裁剪的过程中,发现结尾总是会有几帧画面丢失,最后发现是decoder异步导致的。需要在av_read_frame
读不到packet之后,给decoder
传入空的packet,让它把缓存的数据吐出来
AVPacket *emptyPkt = av_packet_alloc();
emptyPkt->data = nullptr;
emptyPkt->size = 0;
AVFrame *rawVideoFrame = av_frame_alloc();
avcodec_decode_video2(formatCtx->streams[videoStreamIdx]->codec, rawVideoFrame, &gotVideoFrame, emptyPkt);
AVFrame *rawAudioFrame = av_frame_alloc();
avcodec_decode_audio4(formatCtx->streams[audioStreamIdx]->codec, rawAudioFrame, &gotAudioFrame, emptyPkt);
视频和音频都可以做这种处理
写入输出文件
输出相关的代码都在MediaFileWriter
中
创建输出上下文
输出上下文依旧是AVFormatContext
,通过avformat_alloc_output_context2
创建
avformat_alloc_output_context2(&formatCtx, NULL, "flv", "output");
这里创建一个flv格式的输出容器
添加流和配置编码器
分别配置视频和音频的编码器以及流信息
// 音频
AVCodec *aacEncoder = avcodec_find_encoder(::AV_CODEC_ID_AAC);
if (!aacEncoder) {
std::cout << "aacEncoder find failed" << std::endl;
return freeAll();
}
audioCodecCtx = avcodec_alloc_context3(aacEncoder);
audioCodecCtx->codec_type = AVMEDIA_TYPE_AUDIO;
audioCodecCtx->sample_rate = audioInfo.sampleRate;
audioCodecCtx->sample_fmt = audioInfo.sampleFormat;
audioCodecCtx->channel_layout = audioInfo.channels == 1 ? AV_CH_LAYOUT_MONO : AV_CH_LAYOUT_STEREO;
audioCodecCtx->time_base.num = 1;
audioCodecCtx->time_base.den = audioInfo.sampleRate;
audioCodecCtx->frame_size = 1024;
audioCodecCtx->channels = av_get_channel_layout_nb_channels(audioCodecCtx->channel_layout);
// 有些格式需要全局header
if (formatCtx->oformat->flags & AVFMT_GLOBALHEADER) {
audioCodecCtx->flags |= CODEC_FLAG_GLOBAL_HEADER;
}
int err = avcodec_open2(audioCodecCtx, aacEncoder, NULL);
if (err < 0) {
std::cout << "avcodec_open2 audio failed" << std::endl;
return freeAll();
}
AVStream *audioSt;
audioSt = avformat_new_stream(formatCtx, aacEncoder);
audioSt->id = 0;
audioSt->codec = audioCodecCtx;
audioStIdx = 0;
这里的音频编码器采用AAC格式,然后指定采样率,数据格式,通道数等信息,接着使用编码器创建AVStream
。
AVCodec *h264Codec = avcodec_find_encoder(::AV_CODEC_ID_H264);
if (!h264Codec) {
std::cout << "h264Codec find failed" << std::endl;
return freeAll();
}
codecCtx = avcodec_alloc_context3(h264Codec);
codecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
codecCtx->width = videoInfo.width;
codecCtx->height = videoInfo.height;
// codec的timebase一般为1/framerate
codecCtx->time_base.num = 1000;
codecCtx->time_base.den = videoInfo.frameRate * 1000;
codecCtx->bit_rate = 1800000;
codecCtx->gop_size = 5;
// 有些格式需要全局header
if (formatCtx->oformat->flags & AVFMT_GLOBALHEADER) {
codecCtx->flags |= CODEC_FLAG_GLOBAL_HEADER;
}
// h264 params
// codecCtx->qmin = 20;
// codecCtx->qmax = 30;
codecCtx->max_b_frames = 0;
AVDictionary *params = NULL;
av_dict_set(¶ms, "preset", "slow", 0);
av_dict_set(¶ms, "tune", "zerolatency", 0);
err = avcodec_open2(codecCtx, h264Codec, ¶ms);
if (err < 0) {
std::cout << "avcodec_open2 faild" << std::endl;
return freeAll();
}
// video stream
AVStream *videoSt;
videoSt = avformat_new_stream(formatCtx, h264Codec);
if (!videoSt) {
std::cout << "create stream failed" << std::endl;
}
videoSt->id = 1;
videoSt->codec = codecCtx;
videoStIdx = 1;
这里的视频编码器采用H264格式,指定宽高,格式等信息,设置preset
为slow
,保证压缩质量。
打开文件IO
通过avio_open
打开指定路径文件的IO
err = avio_open(&formatCtx->pb, filePath.c_str(), AVIO_FLAG_READ_WRITE);
写入文件头
avformat_write_header(formatCtx,NULL);
写入帧数据
写入视频数据,视频数据的写入比较简单,主要就是pts的计算,这里使用当前写入的帧数除以帧率,就是该帧PTS对应的秒数,最后再转成对应流timebase的表示即可
AVPacket *encPacket = av_packet_alloc();
av_init_packet(encPacket);
int encOK;
int err = avcodec_encode_video2(codecCtx, encPacket, frame, &encOK);
if (err >= 0 && encOK) {
// std::cout << "avcodec_encode_video2 OK" << std::endl;
} else {
return;
}
AVRational outputTimeBase = formatCtx->streams[videoStIdx]->time_base;
AVRational avtimebase = {1, AV_TIME_BASE};
double ptsInAVTimeBase = this->videoFrameCount * 1.0 / videoInfo.frameRate * AV_TIME_BASE;
double durationInAVTimeBase = 1.0 / videoInfo.frameRate * AV_TIME_BASE;
encPacket->pts = av_rescale_q(ptsInAVTimeBase, avtimebase, outputTimeBase);
encPacket->dts = encPacket->pts;
encPacket->duration = av_rescale_q(durationInAVTimeBase, avtimebase, outputTimeBase);
encPacket->pos = -1;
encPacket->stream_index = videoStIdx;
std::cout << "Video Write pts: " << encPacket->pts << std::endl;
av_interleaved_write_frame(formatCtx, encPacket);
this->videoFrameCount++;
写入音频数据,音频数据的重点也是PTS的计算,这里通过已经写入的sample数除以采样率,可以得到对应的PTS秒数,最后再根据流的timebase进行转换
AVPacket *encPacket = av_packet_alloc();
av_init_packet(encPacket);
int encOK;
int err = avcodec_encode_audio2(audioCodecCtx, encPacket, frame, &encOK);
if (err >= 0 && encOK) {
// std::cout << "avcodec_encode_audio2 OK" << std::endl;
} else {
return;
}
AVRational outputTimeBase = formatCtx->streams[audioStIdx]->time_base;
AVRational avtimebase = {1, AV_TIME_BASE};
double ptsInAVTimeBase = this->audioSampleCount / (double)audioInfo.sampleRate * AV_TIME_BASE;
double durationInAVTimeBase = frame->nb_samples / (double)audioInfo.sampleRate * AV_TIME_BASE;
encPacket->pts = av_rescale_q(ptsInAVTimeBase, avtimebase, outputTimeBase);
encPacket->dts = encPacket->pts;
encPacket->duration = av_rescale_q(durationInAVTimeBase, avtimebase, outputTimeBase);
encPacket->pos = -1;
encPacket->stream_index = audioStIdx;
std::cout << "Audio Write pts: " << encPacket->pts << " samples: " << frame->nb_samples << std::endl;
av_interleaved_write_frame(formatCtx, encPacket);
this->audioSampleCount += frame->nb_samples;
清空encoder缓存中的数据
当输入数据全部处理完,在编码器中可能还残留部分未未处理的数据,我们可以通过codec的capabilities来判断有没有
formatCtx->streams[videoStIdx]->codec->codec->capabilities & AV_CODEC_CAP_DELAY;
如果某条流的上述判断为true,表示他的encoder很可能还有未处理的数据,可以通过传入空的AVFrame把残留数据给编码出来
AVRational outputTimeBase = formatCtx->streams[videoStIdx]->time_base;
AVRational avtimebase = {1, AV_TIME_BASE};
encPacket->pts = av_rescale_q(this->videoFrameCount * frameDurationInAVTimeBase, avtimebase, outputTimeBase);
encPacket->dts = encPacket->pts;
encPacket->duration = av_rescale_q(frameDurationInAVTimeBase, avtimebase, outputTimeBase);
encPacket->pos = -1;
encPacket->stream_index = videoStIdx;
std::cout << "Write FLV Video: " << encPacket->pts << std::endl;
av_interleaved_write_frame(formatCtx, encPacket);
PTS依旧使用上面的方式计算,音频也是类似的处理方式
AVPacket *encPacket = av_packet_alloc();
av_init_packet(encPacket);
int encOK;
int err = avcodec_encode_audio2(audioCodecCtx, encPacket, NULL, &encOK);
if (err >= 0 && encOK) {
std::cout << "avcodec_encode_audio2 OK" << " samples: " << encPacket->duration << std::endl;
} else {
break;
}
double durationInSec = encPacket->duration / audioInfo.sampleRate;
AVRational outputTimeBase = formatCtx->streams[audioStIdx]->time_base;
AVRational srcAudioTimeBase = {1, AV_TIME_BASE};
double ptsInAVTimeBase = this->audioSampleCount / (double)audioInfo.sampleRate * AV_TIME_BASE;
encPacket->pts = av_rescale_q(ptsInAVTimeBase, srcAudioTimeBase, outputTimeBase);
encPacket->dts = encPacket->pts;
encPacket->duration = av_rescale_q(durationInSec * AV_TIME_BASE, srcAudioTimeBase, outputTimeBase);
encPacket->pos = -1;
encPacket->stream_index = audioStIdx;
av_interleaved_write_frame(formatCtx, encPacket);
this->audioSampleCount += encPacket->duration;
这里需要循环取,直到取不出来为止
写入文件尾
当所有数据全部写入完毕,就可以写入文件尾部了
av_write_trailer(formatCtx);
到这里一个完整的FLV文件就会被生成出来了。