【问题标题】:FFmpeg libraries: Exactly constant segment duration for HLSFFmpeg 库:HLS 的精确恒定段持续时间
【发布时间】:2013-08-20 22:23:51
【问题描述】:

我们正在使用 FFmpeg 库 git-ee94362 libavformat v55.2.100。 我们的目的是使用 HLS 将两个流(视频和音频)混合到 M3U8 播放列表中。 另外,我们希望每个 TS 片段文件的时长正好是 3.0 秒(帧率为 25 fps)。

为了实现它,我们正在尝试设置几个选项和属性,即: - 段时间
- keyint_min - 场景变化阈值 - gop_size - force_key_frames。

我们的代码如下所示:

AVCodecContext *codec_ctx = NULL;
AVFormatContext *ofmt_ctx = NULL;

int ret = 0, gopSize = (int)(3.0 * 25);   // 3 sec * 25 fps

// ofmt_ctx and codec_ctx initialization and filling are OK, but: 
codec_ctx->time_base.num = 1;
codec_ctx->time_base.den = 25 // fps

// It seems, that the following three lines have no effect without explisit setting of the "hls_time" property
codec_ctx->keyint_min = gopSize;       // in FFMpeg application, the corresponding option is "-keyint_min 3"
codec_ctx->scenechange_threshold = 0;  // in FFMpeg application, the corresponding option is "-sc_threshold 0"
codec_ctx->gop_size = gopSize;         // in FFMpeg application, the corresponding option is "-g 3"

ret = av_opt_set_double(ofmt_ctx, "hls_time", 3.0, AV_OPT_SEARCH_CHILDREN);

// Any of the following lines causes "Option not found" error.
ret = av_opt_set(codec_ctx->priv_data, "profile", "main", AV_OPT_SEARCH_CHILDREN);
ret = av_opt_set(codec_ctx->priv_data, "preset", "ultrafast", AV_OPT_SEARCH_CHILDREN);
ret = av_opt_get(ofmt_ctx, "segment_time",  AV_OPT_SEARCH_CHILDREN, &str);
ret = av_opt_set((ofmt_ctx, "segment_time", "3.0", AV_OPT_SEARCH_CHILDREN);

无论如何,TS 文件的持续时间是不同的,(~2-3 秒),而不是正好 3.0 秒。 我们的问题:解决问题的最佳方法是什么?

安德烈·莫切诺夫。

【问题讨论】:

  • 你找到解决这个问题的办法了吗?
  • 你解决了这个问题吗?

标签: c++ c ffmpeg


【解决方案1】:

您面临的主要问题可能是您的视频文件在合适的位置没有关键帧。如果您只是从输入中复制流,这尤其是一个问题。

FFmpeg 取决于关键帧来计算何时“剪切”一个片段。当你考虑它时,这是有道理的。您不能只在两个关键帧之间进行剪辑,因为每个片段都需要自己完全发挥作用。现在,有人可能会争辩说 FFmpeg 应该只自己插入新的关键帧,但是那样使用起来太友好了,不是吗;)

谢天谢地,您可以使用 FFmpeg 强制关键帧。使用参数或在代码中自己设置标志。您说您已经尝试过强制关键帧,但我认为您没有正确执行。

我的这个测试产生了相当好的结果。它只是命令行,抱歉,但您似乎已经知道如何在代码中应用命令行参数,所以您应该没问题。 另请注意,我不使用“hls_XXX”参数,因为a)我真的不信任它们,b)我认为它也应该适用于非 HLS 流。

ffmpeg -i inputFile.mov -force_key_frames "expr:gte(t,n_forced*10)" -strict -2 -c:a aac -c:v libx264 -f segment -segment_list_type m3u8 -segment_list_size 0 -segment_time 10.0 -segment_time_delta 0.1 -segment_list stream/test.m3u8 stream/test%02d.ts 

您可以查看 force_key_frames 命令的具体工作原理here

到目前为止,我在 C++ 中实现了上述命令,并添加了一些内容。但是没有“force_key_frames”,因为我在转码过程中手动设置了关键帧。这是我所做的:

AVDictionary* headerOptions(0);
av_dict_set(&headerOptions, "segment_format", "mpegts", 0);
av_dict_set(&headerOptions, "segment_list_type", "m3u8", 0);
av_dict_set(&headerOptions, "segment_list", _playlistFileName.c_str(), 0);
av_dict_set_int(&headerOptions, "segment_list_size", 0, 0);
av_dict_set(&headerOptions, "segment_time_delta", TO_STRING(1.00).c_str(), 0);
av_dict_set(&headerOptions, "segment_time", TO_STRING(_segmentDuration).c_str(), 0);
av_dict_set_int(&headerOptions, "reference_stream", _videoStream->index, 0);
av_dict_set(&headerOptions, "segment_list_flags", "cache+live", 0);
avformat_write_header(_formatContext, &headerOptions);

这是生成的 m3u8:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:11
#EXTINF:10.083333,
test00.ts
#EXTINF:10.000000,
test01.ts
#EXTINF:10.000000,
test02.ts
#EXTINF:10.000000,
test03.ts
#EXTINF:10.000000,
test04.ts
#EXTINF:10.000000,
test05.ts
#EXTINF:0.083333,
test06.ts
#EXT-X-ENDLIST

它并不完美(第一部分有点偏离),但我相信你不会得到比这更好的结果。

当然,最好的选择是确保您的输入文件在复制流时始终具有正确的关键帧,但有时您无法控制获得的文件。

旁注

当您在代码中使用 FFmpeg 时,请始终先尝试使用 cli ffmpeg 命令在代码中执行的操作。如果你能让它以这种方式工作,你至少知道要在代码中设置哪些参数。如果它使用命令行工具工作,你知道它一定可以在代码中以某种方式实现;)

【讨论】:

    【解决方案2】:

    您也可以尝试通过修改 ffmpeg 来实现 3 秒的片段(大约)持续时间。正如@theSHEEP 指出的那样,ffmpeg 在进行剪辑之前等待 I 帧到达。您可以通过强制它在“您的时间”进行剪切而不是等待 I 帧来更改 ffmpeg 的这种行为。

        ffmpeg/libavformat/segment.c, 
         795 static int seg_write_packet(AVFormatContext *s, AVPacket *pkt)
    
         835     if (pkt->stream_index == seg->reference_stream_index &&
         836         pkt->flags & AV_PKT_FLAG_KEY &&
         837         seg->segment_frame_count > 0 &&
         838         (seg->cut_pending || seg->frame_count >= start_frame ||
         839          (pkt->pts != AV_NOPTS_VALUE &&
         840           av_compare_ts(pkt->pts, st->time_base,
         841                         end_pts-seg->time_delta, AV_TIME_BASE_Q) >= 0))) 
    

    我会将第 835 行更改为 841 以满足我的要求。 (注释行号 836 并尝试,记住 FFMPEG 是 LGPL)

    HLS IETF 草案版本建议:

    服务器应该尝试在以下点划分源媒体 支持对单个媒体段进行有效解码,例如包上 和关键帧边界

    我会将其视为推荐而不是要求。 ;)

    【讨论】:

      【解决方案3】:

      在没有 I 帧的地方强制剪切不是很好,因为如果只需要解码该特定片段中的帧,它们将被灰色框填充。根本没有足够的数据来正确解码一个完整的帧。

      最好的方法是先编码一个序列:

      AVCodecContext *enc_ctx;
      ...
      av_opt_set_int(enc_ctx, "sc_threshold", sc_threshold, 0);
      enc_ctx->gop_size = 3 * 25;
      av_opt_set_int(enc_ctx, "keyint_min", min_keyint, 0);
      

      稍后,一旦您的编码完成,您可以单独为 HLS 混合文件或在编码时执行此操作。在我的特定用例中,我是在整个编码周期完成后才这样做的。 @TheSHEEEP 的代码对此有所帮助,但他使用的选项不是我需要的。

      size_t f = output_filename.find_last_of(".");
      string ofn = output_filename.substr(0, f);
      ofn.append(".m3u8");
      avformat_alloc_output_context2(&ofmt_ctx, NULL, "hls", ofn.c_str());
      
      AVDictionary* headerOptions = NULL;
      av_dict_set(&headerOptions, "hls_segment_type", "mpegts", 0);
      av_dict_set(&headerOptions, "hls_playlist_type", "event", 0);
      av_dict_set_int(&headerOptions, "hls_list_size", 0, 0);
      av_dict_set(&headerOptions, "segment_time_delta", "1.0", 0);
      av_dict_set(&headerOptions, "hls_flags", "append_list", 0);
      
      ret = avformat_write_header(ofmt_ctx, &headerOptions);
      

      ofmt_ctx 是输出 AVFormatContext。输出文件与@TheSHEEEP 的帖子中的相同。

      【讨论】:

        【解决方案4】:

        我在从相机拍摄的图像中制作 HLS 流时遇到同样的问题,对我来说,使用这些选项获得了最佳结果(创建 5 秒的片段):

            AVDictionary *header_op(0);
            // type
            av_dict_set(&header_op, "hls_segment_type", "mpegts", 0);
            // auto delete old segments
            av_dict_set(&header_op, "hls_flags", "delete_segments", 0);
            // list type
            av_dict_set(&header_op, "segment_list_type", "m3u8", 0);
            // playlist size
            av_dict_set_int(&header_op, "hls_list_size", 5, 0);
            // segment time accuracy
            av_dict_set(&header_op, "segment_time_delta", "1.00", 0);
            // ~max time for each segment
            av_dict_set(&header_op, "hls_time", "5.0", 0);
            // enforce hls_time
            av_dict_set(&header_op, "hls_flags", "split_by_time", 0);
        
            av_dict_set_int(&header_op, "reference_stream", m_stream->index, 0);
            av_dict_set(&header_op, "segment_list_flags", "cache+live", 0);
        
            if (avformat_write_header(m_fmt_ctx, &header_op))
                // Error... 
        

        诀窍在于hls_timesplit_by_time 选项中,explained here 也是如此

        结果非常接近 5 秒:

        #EXTM3U
        #EXT-X-VERSION:3
        #EXT-X-TARGETDURATION:5
        #EXT-X-MEDIA-SEQUENCE:4
        #EXTINF:5.033333,
        example4.ts
        #EXTINF:4.966667,
        example5.ts
        #EXTINF:5.000000,
        example6.ts
        #EXTINF:5.100000,
        example7.ts
        #EXTINF:0.200000,
        example8.ts
        #EXT-X-ENDLIST
        

        最后一段在完成之前被中断。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-12-08
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多