我们正在使用FFmpeg库git-ee94362 libavformat v55.2.100。 我们的目的是使用HLS将两个流(视频和音频)复用到M3U8播放列表中。 另外,我们希望每个TS段文件的持续时间恰好为3.0秒(帧速率为25 fps)。
为了达到它,我们尝试设置几个选项和属性,即:
- segment_time
- keyint_min
- scenechange_threshold
- 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秒。 我们的问题:解决问题的最佳方法是什么?
Andrey Mochenov。
答案 0 :(得分:0)
您面临的主要问题可能是您的视频文件在适合位置没有关键帧。如果您只是从输入中复制流,那么这尤其是个问题。
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命令尝试在代码中执行的操作。如果你能以这种方式工作,你至少知道在代码中设置什么参数。如果它使用命令行工具,你知道它必须以某种方式在代码中;)
答案 1 :(得分:0)
您还可以通过修改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草案版建议:
服务器应该尝试在源点划分源媒体 支持单个媒体段的有效解码,例如,在包上 和关键帧边界
我会将其视为推荐而非要求。 ;)
答案 2 :(得分:0)
在没有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的帖子相同。
答案 3 :(得分:0)
使用相机拍摄的图像进行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...
窍门在explained here中的hls_time
和split_by_time
选项中
结果接近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
最后一段在完成之前被打断了。