准备工作
ijkplayer的编译环境
一个局域网网络的rtsp实时流(比如wifi摄像头)
使用软解码,ff_play.c有完整控制软解码流程
ffmpeg使用ijk默认的就行,需要配置rtsp支持
注意事项
- 网络本身有问题的情况下,此处理办法会导致频繁丢帧卡顿,不适用于真实互联网络的流。建议调试之前先用其他播放器看一下流是否有问题
- 对于终端本身编码有延迟递增的情况下,此方案并不会降低延迟
场景
可以确保网络没有问题,并且频段也不会有影响。使用vlc播放器发现不会出现以下两个问题:
- rtsp实时流播放随着时间变长,延时越来越高。
- 某些设备开屏花屏。
原因
延时变高
- 播放器会进行基准同步(包括视频,音频和系统时钟3个维度)。如果外部设置取消视频音频同步,那还会用系统时钟。
花屏
- 某些设备开屏时流编码没有处理好,ijk这边通过配置进行丢掉指定数量的帧数再进行解码
修改方案
- 对于实时预览,取消任何时间同步来应对此问题
修改函数
新增 previewMode, 新增preview_drop_first , 新增 low_pic
关于ijk配置修改
//java调用设置, 开屏丢弃30帧
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER,"preview_drop_firstframe",30);
//降低分辨率处理降低解码压力延迟
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER,"low_pic",1);//0:不降分辨率,正常解码。默认为0
// 1:分辨率降低到原始的 1/2。
// 2:分辨率降低到原始的 1/4。
// 3:分辨率降低到原始的 1/8。
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER,"previewMode",1);//直播模式 ,1开启 0关闭 回放视频使用0
c++位置涉及文件:ff_play_option.h,ff_play_def.h
//在ff_play_def.h中新加入
typedef struct FFPlayer {
//其他
int lowres;
int previewMode;
int preview_drop_first;
} FFPlayer;
//在ff_play.def.h里的关闭播放器时的伴生函数里重置启状态
inline static void ffp_reset_internal(FFPlayer *ffp)
{
//其他
//新增以下
ffp->lowres = 0;
ffp->previewMode = 0;
ffp->preview_drop_first =0;
}
//ff_play_option.h中新增暴露配置值
static const AVOption ffp_context_options[] = {
OPTION_OFFSET(infinite_buffer), OPTION_INT(0, 0, 1) },
{ "low_pic", "lowres",
{ "previewMode", "previewMode",
OPTION_OFFSET(previewMode), OPTION_INT(0, 0, 1) },
{"preview_drop_firstframe", "preview_drop_firstframe",
OPTION_OFFSET(preview_drop_first), OPTION_INT(0,0,INT_MAX)},
}
以上配置完成。可以在java层进行使用了。
配置在native层进行应用
- 打开ff_play.c
修改函数stream_component_open
在获取解码配置时加入
if (!av_dict_get(opts, "threads", NULL, 0))
if (ffp->previewMode==1)
{
av_log(NULL,AV_LOG_VERBOSE,"实时解码降低开销,使用线程数%d",2);
av_dict_set(&opts, "threads", "2", 0);
}else{
av_dict_set(&opts, "threads", "auto", 0);
}
if (ffp->lowres>0)
av_log(NULL,AV_LOG_VERBOSE,"实时解码降低开销,使用分辨率%d",ffp->lowres);
av_dict_set_int(&opts, "lowres", ffp->lowres, 0);
修改函数get_video_frame,获取视频帧时,加入判断跳过为pts准备的时间偏移逻辑
double diff = dpts;
if (ffp->previewMode==0)
{
diff=dpts - get_master_clock(is);
}else{
av_log(NULL,AV_LOG_VERBOSE,"当前为直播模式,不进行%s","时间同步,完全根据pts渲染");
}
修改函数video_refresh,每一帧获取时,进行跳帧处理
//帧间隔时间处理
if (lastvp->serial != vp->serial){
if (ffp->previewMode==1)
{
is->frame_timer = 0;
}
else{
is->frame_timer = av_gettime_relative() / 1000000.0;
}
}
//跳帧处理
if (opaque->preview_drop_first > 0) {
av_log(NULL,AV_LOG_VERBOSE,"drop first income frame count down %d",opaque->preview_drop_first);
opaque->preview_drop_first -= 1;
frame_queue_next(&is->pictq);
goto retry;
}
修改函数compute_target_delay,获取下一帧处理要延时多久处理时,进行延时归0处理,仅仅依赖系统时钟
if (ffp->previewMode==0)
{
diff = get_clock(&is->vidclk) - get_master_clock(is);
/* skip or repeat frame. We take into account the
delay to compute the threshold. I still don't know
if it is the best guess */
sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
/* -- by bbcallen: replace is->max_frame_duration with AV_NOSYNC_THRESHOLD */
if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD) {
if (diff <= -sync_threshold)
delay = FFMAX(0, delay + diff);
else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)
delay = delay + diff;
else if (diff >= sync_threshold)
delay = 2 * delay;
}
}else{
av_log(NULL,AV_LOG_VERBOSE,"当前为直播模式,不延迟计算同步%s","直播模式1");
}
修改函数stream_open,加入如果是直播模式则不初始化音频解码和字幕解码的判断
if (frame_queue_init(&is->pictq, &is->videoq, ffp->pictq_size, 1) < 0)
goto fail;
if (ffp->previewMode==0)
{
if (frame_queue_init(&is->subpq, &is->subtitleq, SUBPICTURE_QUEUE_SIZE, 0) < 0)
goto fail;
if (frame_queue_init(&is->sampq, &is->audioq, SAMPLE_QUEUE_SIZE, 1) < 0)
goto fail;
}else{
av_log(NULL,AV_LOG_VERBOSE,"当前为直播模式禁用音视频解码队列初始化:%s","模式为1");
}
修改函数stream_component_open,将解码线程默认定死,这个值外部也能自行修改,也应用lowres设置分辨率
if (ffp->previewMode==1)
{
av_log(NULL,AV_LOG_VERBOSE,"实时解码降低开销,使用线程数%d",2);
av_dict_set(&opts, "threads", "2", 0);
}else{
av_dict_set(&opts, "threads", "auto", 0);
}
if (ffp->lowres>0)
av_log(NULL,AV_LOG_VERBOSE,"实时解码降低开销,使用分辨率%d",ffp->lowres);
av_dict_set_int(&opts, "lowres", ffp->lowres, 0);
参照文件修改,对照复制修改
观察设备master平台,延迟300-400ms 持续1小时,分辨率720p
观察设备羚羊平台,延迟300-400ms 持续1小时,分辨率640p
软解码
ijk总体配置
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-hevc", 0);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0);//软解码
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 3000000);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "fast", 1); // 开启快速解码模式
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "max_delay", 0); // 禁用缓冲延迟
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER,"previewMode",1);//直播模式
/* mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "video-pictq-size", 3);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "video-pictq-size-min", 1);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "video-pictq-size-max", 1);//编码关键帧间隔不大时,可以使用此配置,有效降低延迟,网络波动必卡*/
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "video-pictq-size", 9);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "video-pictq-size-min", 3);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "video-pictq-size-max", 6);
//丢首次开屏帧
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER,"preview_drop_firstframe",30);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT,"dns_cache_clear",1);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT,"find_stream_info",0);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", rtspTransport);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100L);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1L);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 1);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "max-buffer-size", 2097152);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER,"overlay-format","fcc-_es2");
//此配置可以有效控制所有延迟,不能设置为0,单位为ms
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 10); //300
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer");
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER,"keyframe_start",1);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 1048576);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "http-detect-range-support", 0);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48);
//mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_frame", 5);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 60);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min-frames", 30);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER,"low_pic",0);//0:不降分辨率,正常解码。
// 1:分辨率降低到原始的 1/2。
// 2:分辨率降低到原始的 1/4。
// 3:分辨率降低到原始的 1/8。
// mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_fps", 30);
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "an", 1);//静音
总结
一次简单的对着问题调整的方案,没有什么技术含量。
后续补充
- 在函数video_refresh里,有一个控制帧数渲染间隔的位置。那里可以更加直观的在延迟和播放之间进行平衡
- 在对delay赋值的时候,获取maxfps来确定帧数间隔
- 测试在delay为0的时候,局域网wifi摄像头可以达到200ms左右延迟。已经很低了,如果再继续降低,需要从网络链路下手了,缺点是网络波动时,会有卡顿掉帧很明显。
- 测试在delay根据maxfps计算的间隔时,局域网wifi摄像头可以达到延迟在350ms左右,但是播放平稳,除了受到干扰(系统资源不够,或者网络波动)的情况下,也会明显的出现卡顿掉帧。
//在java层可以通过设置最大帧数来减少间隔
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_fps", 30);
//在native层里进行获取
time= av_gettime_relative()/1000000.0;
if (ffp->previewMode==1)
{
double fps = ffp->max_fps; // 目标帧率(例如 30 FPS)
delay = 1.0 / fps; // 每帧的延迟时间(秒)
av_log(NULL,AV_LOG_VERBOSE,"直播模式,非系统控制,当前帧数间隔,%0.6f",delay);
//delay可以根据帧数来确定间隔,忽略网络延迟等其他因素,在播放平稳和延迟之间取得平衡。
//delay如果设置为0,系统会根据自己的情况来调度,会优先保证平稳播放,延迟就会递增。
}