
FFMPEG 缓冲设置
问题描述
某项目的视频说明书APP开发过程中遇到一个问题,网络带宽远低于视频码率时使用IjkPlayer播放会频繁缓冲再播放,影响使用体验。
问题复现
先是想办法复现问题,为了可以控制网络速度,使用了Throttly - Network Tools这个软件来通过本地VPN限速的方式来模拟低带宽环境。视频源可以直接使用ijkPlayer自带demo里的sample。
实际试下来会发生缓冲-播放频繁切换的现象,较为影响使用体验。
问题解决
视频码率和网络环境没有改善可能的情况下,只能从缓冲策略下手,来改善使用体验。 网上搜索了一下,其他的很多都是加快起播速度(所谓秒开)的设置方案,无帮助,最终在ijkPlayer的GitHub里找到了有人提出的类似问题,但最终没有答案。
但题主提到的几个option还是提供了很重要的线索,顺藤摸瓜找到了这篇日志,日志详细解释了ijkPlayer的缓冲策略和可以调整的几个option。大致摘抄如下:
buffering机制描述 在read_thread的for(;;)循环中首帧未播放时每50ms/首帧播放后每500ms进行检查是否可以恢复播放; 当检查到能满足播放时,就升级时间梯度,进行更严格的检查,让队列中缓存尽可能多的数据,以避免卡顿;同样,当触发卡顿时,也必须得等到满足时间梯度才能进入播放。 在解码前取包时,若发现取不到包了,则暂停播放,触发缓冲,并置is->paused为1;此时依然要等循环调用ffp_check_buffering_l,填满队列,满足播放条件后才能恢复播放状态;是以牺牲延迟来保障流畅。 满足播放条件:能播放时长已经足够播放hwm_in_ms了 或者 能播放的数据量已经大于256K了 (这里我看实际代码是不准确的,对应看下面代码块第60行,我在ijkPlayer的源码里没找到,估计后来改了?) 时间梯度默认初始值
high_water_mark_in_bytes = DEFAULT_HIGH_WATER_MARK_IN_BYTES; 256K,始终不变
first_high_water_mark_in_ms = DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS; 100
next_high_water_mark_in_ms = DEFAULT_NEXT_HIGH_WATER_MARK_IN_MS; 1000
last_high_water_mark_in_ms = DEFAULT_LAST_HIGH_WATER_MARK_IN_MS; 5000
current_high_water_mark_in_ms = DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS; 100
buffer缓冲时长 100ms -> 1s -> 2s -> 4s -> 5s,最大递增到5s;但满足256K即可放行; 代码如下:
static int read_thread(void *arg) {
for (;;) {
/*
* 开启缓冲机制
* 在for循环中,每读到一个包,都会检查是否进行缓冲
*/
if (ffp->packet_buffering) {
io_tick_counter = SDL_GetTickHR();
if ((!ffp->first_video_frame_rendered && is->video_st) ||
(!ffp->first_audio_frame_rendered && is->audio_st)) {
// 首帧未显示前,50ms检测一次
if (abs((int) (io_tick_counter - prev_io_tick_counter)) > FAST_BUFFERING_CHECK_PER_MILLISECONDS) {
prev_io_tick_counter = io_tick_counter;
ffp->dcc.current_high_water_mark_in_ms = ffp->dcc.first_high_water_mark_in_ms;
ffp_check_buffering_l(ffp);
}
} else {
if (abs((int) (io_tick_counter - prev_io_tick_counter)) > BUFFERING_CHECK_PER_MILLISECONDS) {
// 首帧显示后,500ms检测一次
prev_io_tick_counter = io_tick_counter;
ffp_check_buffering_l(ffp);
}
}
}
}
}
/*
* 循环检查是否缓冲够了,够了就去播放吧,取消缓冲状态,取消暂停,恢复播放
*/
void ffp_check_buffering_l(FFPlayer *ffp) {
// 阶梯递增,最大DEFAULT_LAST_HIGH_WATER_MARK_IN_MS,5s
int hwm_in_ms = ffp->dcc.current_high_water_mark_in_ms;
int hwm_in_bytes = ffp->dcc.high_water_mark_in_bytes;
// 队列里缓存的能播放的音视频播放时长
int64_t audio_cached_duration = ffp->stat.audio_cache.duration;
int64_t video_cached_duration = ffp->stat.video_cache.duration;
int cached_duration_in_ms = min((video_cached_duration, audio_cached_duration);
/*
* 计算当前能播放的时长超过了多少hwm_in_ms
* 我理解这块是个四舍五入,然后放大一百倍,cached_duration_in_ms * 100.5 / hwm_in_ms
* 后面的算法,实际上表示cached_duration_in_ms>hwm_in_ms即可进行播放了
*/
int buf_time_percent = (int) av_rescale(cached_duration_in_ms, 1005, hwm_in_ms * 10);
// 队列里缓存的音视频总大小
int cached_size = is->audioq.size + is->videoq.size;
// 计算缓存数据大小超过了多少hwm_in_bytes
int buf_size_percent = (int) av_rescale(cached_size, 1005, hwm_in_bytes * 10);
/*
* 能播放时长已经足够播放hwm_in_ms了 ||
* 能播放的数据量已经足够播放hwm_in_bytes了,
* 就解除缓冲状态/暂停状态,设置为播放状态
*/
int need_start_buffering = 0;
if (buf_time_percent >= 100 || buf_size_percent >= 100) {
need_start_buffering = 1;
}
if (need_start_buffering) {
if (hwm_in_ms < ffp->dcc.next_high_water_mark_in_ms) {
hwm_in_ms = ffp->dcc.next_high_water_mark_in_ms;
} else {
hwm_in_ms *= 2;
}
if (hwm_in_ms > ffp->dcc.last_high_water_mark_in_ms)
hwm_in_ms = ffp->dcc.last_high_water_mark_in_ms;
ffp->dcc.current_high_water_mark_in_ms = hwm_in_ms;
if (is->buffer_indicator_queue && is->buffer_indicator_queue->nb_packets > 0) {
if ((is->audioq.nb_packets >= MIN_MIN_FRAMES || is->audio_stream < 0 || is->audioq.abort_request)
&& (is->videoq.nb_packets >= MIN_MIN_FRAMES || is->video_stream < 0 || is->videoq.abort_request)) {
// 音视频队列的缓冲区差不多了,> MIN_MIN,则去除暂停状态,
ffp_toggle_buffering(ffp, 0);
}
}
}
}
void ffp_toggle_buffering(FFPlayer *ffp, int start_buffering) {
SDL_LockMutex(ffp->is->play_mutex);
ffp_toggle_buffering_l(ffp, start_buffering);
SDL_UnlockMutex(ffp->is->play_mutex);
}
void ffp_toggle_buffering_l(FFPlayer *ffp, int buffering_on) {
if (!ffp->packet_buffering) {
// 缓存机制是否开启
return;
}
VideoState *is = ffp->is;
if (buffering_on && !is->buffering_on) {
// 当前没buffering, 要去buffering,FFP_MSG_BUFFERING_START
is->buffering_on = 1;
stream_update_pause_l(ffp); // 暂停
} else if (!buffering_on && is->buffering_on) {
// 当前buffering,取消buffering,FFP_MSG_BUFFERING_END
is->buffering_on = 0;
stream_update_pause_l(ffp); // 取消暂停
}
}
/*
* 软件音频、软硬解视频时,阻塞等待直到退出或者有AVPacket数据
* 在开启缓冲机制的情况下,会暂停进行缓冲,等待check buffering修复到播放状态
*/
static int packet_queue_get_or_buffering(FFPlayer *ffp, PacketQueue *q, AVPacket *pkt, int *serial,
int *finished) {
assert(finished);
if (!ffp->packet_buffering){
// 未开启缓冲机制
return packet_queue_get(q, pkt, 1, serial); // queue为空时会阻塞等待
}
while (1) {
int new_packet = packet_queue_get(q, pkt, 0, serial);
if (new_packet < 0) {
return -1;
} else if (new_packet == 0) {
if (q->is_buffer_indicator && !*finished) {
ffp_toggle_buffering(ffp, 1); // 暂停当前,去缓冲,设置为1
}
new_packet = packet_queue_get(q, pkt, 1, serial); // 缓冲了,再拿一次
if (new_packet < 0) {
return -1;
}
}
if (*finished == *serial) {
av_packet_unref(pkt);
continue;
} else {
break;
}
}
return 1;
}
实际我在ijkPlayer的代码里看到对于缓冲时长和缓冲数据量的处理是这样的,如果缓冲时长设置有效,缓冲时长的percent优先,否则以数据量缓冲进度为准:
于是我在demo里添加了如下的修改
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "next-high-water-mark-ms", 10 * 1000L);
但无效,并且log显示设置option失败
Error setting option next-high-water-mark-ms to value 10000.
又进行了一番搜索之后,看到这么一篇博客,解释了ijkPlayer设置option时一些限制:
{ "max-buffer-size", "max buffer size should be pre-read",
OPTION_OFFSET(dcc.max_buffer_size), OPTION_INT(MAX_QUEUE_SIZE, 0, MAX_QUEUE_SIZE) }
配置一个选项需要填写4个信息: max-buffer-size:选项的名称; max buffer size should be pre-read:选项的描述; OPTION_OFFSET(dcc.max_buffer_size):选项对应的系统变量; OPTION_INT(MAX_QUEUE_SIZE, 0, MAX_QUEUE_SIZE):选项的取值范围; 下面解释OPTION_OFFSET、OPTION_INT函数: OPTION_OFFSET:
#define OPTION_OFFSET(x) offsetof(FFPlayer, x)
系统最终回调用offsetof函数,这个是gcc中的函数,网上搜了下,其作用为:访问返回结构体FFPlayer中成员变量x相对于结构体首地址的偏移量,转换为Java的思维就是返回FFPlayer类中成员变量x. OPTION_INT:
#define OPTION_INT(default__, min__, max__) \
.type = AV_OPT_TYPE_INT, \
{ .i64 = default__ }, \
.min = min__, \
.max = max__, \
.flags = AV_OPT_FLAG_DECODING_PARAM
其第一个参数为选项的默认值,第二个参数为选项的最小值,第三个参数为选项的最大值
再看我们设置的option相关代码,发现它的最大最小值是写死的 DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS 到 DEFAULT_LAST_HIGH_WATER_MARK_IN_MS 之间
{ "first-high-water-mark-ms", "first chance to wakeup read_thread",
OPTION_OFFSET(dcc.first_high_water_mark_in_ms),
OPTION_INT(DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS,
DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS,
DEFAULT_LAST_HIGH_WATER_MARK_IN_MS) },
{ "next-high-water-mark-ms", "second chance to wakeup read_thread",
OPTION_OFFSET(dcc.next_high_water_mark_in_ms),
OPTION_INT(DEFAULT_NEXT_HIGH_WATER_MARK_IN_MS,
DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS,
DEFAULT_LAST_HIGH_WATER_MARK_IN_MS) },
{ "last-high-water-mark-ms", "last chance to wakeup read_thread",
OPTION_OFFSET(dcc.last_high_water_mark_in_ms),
OPTION_INT(DEFAULT_LAST_HIGH_WATER_MARK_IN_MS,
DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS,
DEFAULT_LAST_HIGH_WATER_MARK_IN_MS) },
DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS = 100ms DEFAULT_LAST_HIGH_WATER_MARK_IN_MS = 5000ms 我设置的是10000ms自然会报错,改为5000ms后不再报错且生效。 为什么会有上述限制还要进一步的寻找答案,目前猜和代码的逻辑有关系。