c++ 库文件格式/库文件编解码器:为什么我得到的视频比预期的短很多?

uxh89sit  于 2023-03-05  发布在  其他
关注(0)|答案(2)|浏览(189)

下面的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);
}
hsgswve4

hsgswve41#

看起来带有VP8视频流的MKV强制时基为1/1000
根据following answer1/1000是WebM多路复用器所必需和强制的,不应该进行更改。
(It很难找到那个限制的参考资料-它可能是FFMPEG特定的限制)。
根据定义,MKV容器中的VP8视频编解码器等效于具有VP8视频编解码器的WebM容器。
我假设上述配置使用"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
我不知道这是不是最好的解决办法...

vuktfyat

vuktfyat2#

我自己的备注:Rotem的答案是正确的。根据FFmpeg的文档(强调我):
encoding:可由调用方在avformat_write_header()之前设置,以向多路复用器提供有关所需时基的提示。在avformat_write_header()中,多路复用器将使用实际用于写入文件的时间戳的时基覆盖此字段(根据格式,可能与用户提供的时间戳相关,也可能不相关)。
简而言之,我确实给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;

相关问题