libavformat/libavcodec:为什么我得到的视频比预期的要短得多?

问题描述 投票:0回答:2

下面的 MVE 是一个简单的测试程序,应该对 6 秒(360 帧)的视频进行编码。当我将导出的视频 (

output.mkv
) 导入视频编辑器时,我发现它只有几分之一秒。为什么是这样?我该如何解决?

MVE

#include <cstddef>
#include <cstdio>
#include <iostream>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}

namespace av {
  // ERROR HANDLING
  // ==============

  class av_category_t final : public std::error_category {
  public:
    inline const char* name() const noexcept override { return "av_category"; }

    inline std::string message(int code) const noexcept override {
      thread_local static char msg_buffer[AV_ERROR_MAX_STRING_SIZE];
      av_strerror(code, msg_buffer, sizeof(msg_buffer));
      return std::string(msg_buffer);
    }
  };

  inline const std::error_category& av_category() {
    static av_category_t result;
    return result;
  }

  // helper function to create a std::system_error.
  inline std::system_error av_error(int code) {
    return std::system_error(code, av_category());
  }

  // Allocate or reallocate buffers in a video frame.
  inline void alloc_video_frame(
    AVFrame* frame, int width, int height, AVPixelFormat pix_fmt
  ) {
    int err;
    if (frame->width != width || frame->height != height || 
      frame->format != pix_fmt || !av_frame_is_writable(frame)) {
      // unref the old data if any
      if (frame->buf[0] != nullptr) {
        av_frame_unref(frame);
      }

      // reset parameters
      frame->width  = width;
      frame->height = height;
      frame->format = pix_fmt;

      // reallocate
      if ((err = av_frame_get_buffer(frame, 0)) < 0)
        throw av_error(err);
    }
  }
  inline void alloc_video_frame(AVFrame* frame, const AVCodecContext* ctx) {
    alloc_video_frame(frame, ctx->width, ctx->height, ctx->pix_fmt);
  }
}  // namespace av

void fill_rgb(AVFrame* tmp, AVFrame* dst, uint32_t col) {
  static SwsContext* sws = nullptr;
  int err;
  
  av::alloc_video_frame(tmp, dst->width, dst->height, AV_PIX_FMT_0RGB32);
  for (int i = 0; i < tmp->height; i++) {
    for (int j = 0; j < tmp->width; j++) {
      void* p = tmp->data[0] + (i * tmp->linesize[0]) + (j * 4);
      *((uint32_t*) p) = col;
    }
  }
  
  sws = sws_getCachedContext(sws,
    // src params
    tmp->width, tmp->height, (AVPixelFormat) tmp->format,
    // dst params
    dst->width, dst->height, (AVPixelFormat) dst->format,
    // stuff
    0, nullptr, nullptr, nullptr
  );
  if ((err = sws_scale_frame(sws, dst, tmp)) < 0)
    throw av::av_error(err);
}

int32_t hue_c(int deg) {
  deg %= 360;
  int rem = deg % 60;
  switch (deg / 60) {
  case 0: return 0xFF0000 | ((deg * 256 / 60) << 8);
  case 1: return 0x00FF00 | (((60 - deg) * 256 / 60) << 16);
  case 2: return 0x00FF00 | ((deg * 256 / 60) << 0);
  case 3: return 0x0000FF | (((60 - deg) * 256 / 60) << 8);
  case 4: return 0x00FF00 | ((deg * 256 / 60) << 16);
  case 5: return 0xFF0000 | (((60 - deg) * 256 / 60) << 0);
  }
  return 0xFFFFFF;
}

int main() {
  int res;

  AVFormatContext* fmt_ctx;
  AVStream* vstream;
  const AVCodec* vcodec;
  AVCodecContext* vcodec_ctx;

  AVFrame* vframe;
  AVFrame* vframe2;
  AVPacket* vpacket;

  int64_t pts = 0;

  // init frame
  res = avformat_alloc_output_context2(&fmt_ctx, NULL, NULL, "output.mkv");
  if (res < 0)
    return 1;

  // get encoder
  vcodec = avcodec_find_encoder(AV_CODEC_ID_VP8);

  // allocate everything else
  vstream    = avformat_new_stream(fmt_ctx, vcodec);
  vcodec_ctx = avcodec_alloc_context3(vcodec);
  vframe     = av_frame_alloc();
  vframe2    = av_frame_alloc();
  vpacket    = av_packet_alloc();
  if (!vstream || !vcodec_ctx || !vframe || !vpacket) {
    return 1;
  }

  // set PTS counter
  pts = 0;

  // codec: size parameters
  vcodec_ctx->width               = 640;
  vcodec_ctx->height              = 480;
  vcodec_ctx->sample_aspect_ratio = {1, 1};

  // codec: pixel formats
  vcodec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;

  // codec: bit rate et al.
  vcodec_ctx->bit_rate       = 2e6;
  vcodec_ctx->rc_buffer_size = 4e6;
  vcodec_ctx->rc_max_rate    = 2e6;
  vcodec_ctx->rc_min_rate    = 2.5e6;

  // codec: frame rate
  vcodec_ctx->time_base   = {1, 60};
  vstream->time_base      = {1, 60};
  vstream->avg_frame_rate = {60, 1};

  // open codec
  res = avcodec_open2(vcodec_ctx, vcodec, NULL);
  if (res < 0)
    throw av::av_error(res);
  res = avcodec_parameters_from_context(vstream->codecpar, vcodec_ctx);
  if (res < 0)
    throw av::av_error(res);
  
  // open file
  if (!(fmt_ctx->oformat->flags & AVFMT_NOFILE)) {
    res = avio_open(&fmt_ctx->pb, "output.mkv", AVIO_FLAG_WRITE);
    if (res < 0)
      throw av::av_error(res);
  }
  
  // write format header
  res = avformat_write_header(fmt_ctx, NULL);
  if (res < 0)
    throw av::av_error(res);

  // encode loop
  for (int i = 0; i < 360; i++) {
    av::alloc_video_frame(vframe, vcodec_ctx);

    // gen data and store to vframe
    fill_rgb(vframe2, vframe, hue_c(i));
    
    // set timing info
    vframe->time_base = vcodec_ctx->time_base;
    vframe->pts       = i;

    // send to encoder
    avcodec_send_frame(vcodec_ctx, vframe);
    while ((res = avcodec_receive_packet(vcodec_ctx, vpacket)) == 0) {
      printf("DTS: %ld, PTS: %ld\n", vpacket->dts, vpacket->pts);
      
      if ((res = av_interleaved_write_frame(fmt_ctx, vpacket)) < 0)
        throw av::av_error(res);
      
    }
    if (res != AVERROR_EOF && res != AVERROR(EAGAIN))
      return -1;
  }
  
  if ((res = av_write_trailer(fmt_ctx)) < 0)
    throw av::av_error(res);
  
  av_frame_free(&vframe);
  av_frame_free(&vframe2);
  av_packet_free(&vpacket);
  avcodec_free_context(&vcodec_ctx);
  
  avformat_free_context(fmt_ctx);
}
c++ ffmpeg libavcodec
2个回答
0
投票

看起来像带有 VP8 视频流的 MKV 强制时基为

1/1000
.

根据以下答案

1/1000
是 WebM muxer 所必需和强制执行的,不应该更改。
(很难找到该限制的参考 - 它可能是 FFmpeg 特定限制)。

根据定义,MKV容器中的VP8视频编解码器相当于WebM容器中的VP8视频编解码器。
我假设上述配置使用“WebM muxer”并且具有与 WebM 容器中的 VP9 编解码器相同的

1/1000
时基限制。

注:
我不知道在上面的配置中是否有办法强制使用不同的时基。

获得 60fps 的简单解决方案是将

pts
设置为
i*16
.

vframe->pts = (int64_t)i*16;  //16 = 1000/60 (approximately).

执行

ffprobe -show_packets output.mkv
总是输出:

time_base=1/1000


当设置

pts
i*16
时,FFprobe报告:

Stream #0:0: Video: vp8, yuv420p(progressive), 640x480, 60 fps, 60 tbr, 1k tbn, 1k tbc

MediaInfo 工具还报告

Frame rate : 60.000 FPS
.


我不知道这是不是最好的解决方案...


0
投票

我自己的注意事项:Rotem 的回答是正确的。根据 FFmpeg 的文档(强调我的):

encoding:可以由调用者在 avformat_write_header() 之前设置,以向 muxer 提供有关所需时基的提示。在 avformat_write_header() 中,muxer 将使用实际用于写入文件的时间戳的时基覆盖此字段(这可能与用户提供的相关,也可能不相关,具体取决于格式)。

简而言之,我确实为 FFmpeg 设置了一个提示,以使用

1/60
作为流时间基准。但是,MKV 格式将时基限制为
1/1000
,因此忽略了我的提示。

要解决时序问题,您需要使用

av_packet_rescale_ts
将时间戳从 codec 的时基转换为 muxer 的时基。

    // send to encoder
    avcodec_send_frame(vcodec_ctx, vframe);
    while ((res = avcodec_receive_packet(vcodec_ctx, vpacket)) == 0) {
      printf("DTS: %ld, PTS: %ld\n", vpacket->dts, vpacket->pts);
      // change to stream time base
      av_packet_rescale_ts(vpacket, vcodec_ctx->time_base, vstream->time_base);
      if ((res = av_interleaved_write_frame(fmt_ctx, vpacket)) < 0)
        throw av::av_error(res);
      
    }
    if (res != AVERROR_EOF && res != AVERROR(EAGAIN))
      return -1;
© www.soinside.com 2019 - 2024. All rights reserved.