FFmpeg学习笔记 - 裁剪视频

391 阅读5分钟

概述

本篇博客主要记录如何使用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(&params, "preset", "slow", 0);
  av_dict_set(&params, "tune", "zerolatency", 0);

  err = avcodec_open2(codecCtx, h264Codec, &params);
  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格式,指定宽高,格式等信息,设置presetslow,保证压缩质量。

打开文件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文件就会被生成出来了。