ijkplayer播放flv延时优化0延时20200825
ijkplayer播放flv延时优化0延时20200825
ios和android的直播参数调整的完全一致,但是安卓就是有2s的flv直播延时无法消除. 经过分析发现音频有2s的pts延缓了,所以大胆猜测ios和web的ijkplayer做了音频pts不准确的矫正.
-
ijkplyaer/ff_player.c中 视频同步到音频会有一个delay的判断.
-
修改
ijkmedia>ijkplayer>ff_ffplay.c#1290
static double vp_duration(VideoState *is, Frame *vp, Frame *nextvp) {
if(vp->serial == nextvp->serial) {
doubleduration = nextvp->pts - vp->pts;
if(isnan(duration) || duration <=0|| duration > is->max_frame_duration)
return vp->duration;
else
return duration;
}else{
return 0.0;
}
}
- 修改为:
static double vp_duration(VideoState*is,Frame*vp,Frame*nextvp) {
return vp->duration;
}
- 修改
ijkmedia>ijkplayer>ff_ffplay.c#ffplay_video_thread#2187
static int ffplay_video_thread(void*arg){
FFPlayer*ffp = arg;
VideoState*is = ffp->is;
AVFrame*frame =av_frame_alloc();
doublepts;
doubleduration;
intret;
AVRationaltb = is->video_st->time_base;
//注释如下一行代码
//AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);
//......省略部分代码
//注释如下一行代码
//duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational) {frame_rate.den, frame_rate.num}) : 0);
//直接这里写出
duration=0.01;
//........
}
- 重新编译ijkplayer(这里只有ijk的代码修改了,不需要重新编译ffmpeg和openssl)
~/develop/caixingming/ijkplayer-android/android$ ./compile-ijk.sh all
- 打包
zip -r ijkplayer0825.zip ijkplayer/
- 拷贝zip文件到自己本地然后替换so文件即可.
这么改后发现,音视频无法同步了 音频延迟了2s
那么为什么这么改呢,让我们一步一步分析
视频渲染缓存逻辑分析
- 初始化
ijkmedia/ijkplayer_jni.c#IjkMediaPlayer_native_setup
static void
IjkMediaPlayer_native_setup(JNIEnv *env, jobject thiz, jobject weak_this)
{
MPTRACE("%s\n", __func__);
IjkMediaPlayer *mp = ijkmp_android_create(message_loop);
JNI_CHECK_GOTO(mp, env, "java/lang/OutOfMemoryError", "mpjni: native_setup: ijkmp_create() failed", LABEL_RETURN);
jni_set_media_player(env, thiz, mp);
ijkmp_set_weak_thiz(mp, (*env)->NewGlobalRef(env, weak_this));
ijkmp_set_inject_opaque(mp, ijkmp_get_weak_thiz(mp));
ijkmp_set_ijkio_inject_opaque(mp, ijkmp_get_weak_thiz(mp));
ijkmp_android_set_mediacodec_select_callback(mp, mediacodec_select_callback, ijkmp_get_weak_thiz(mp));
LABEL_RETURN:
ijkmp_dec_ref_p(&mp);
}
- 注意
ijkmp_android_create(message_loop)
ijkmedia/ijkplayer/android/ijkplayer_android.c#34
IjkMediaPlayer *ijkmp_android_create(int(*msg_loop)(void*))
{
IjkMediaPlayer *mp = ijkmp_create(msg_loop);
if (!mp)
goto fail;
mp->ffplayer->vout = SDL_VoutAndroid_CreateForAndroidSurface();
if (!mp->ffplayer->vout)
goto fail;
mp->ffplayer->pipeline = ffpipeline_create_from_android(mp->ffplayer);
if (!mp->ffplayer->pipeline)
goto fail;
ffpipeline_set_vout(mp->ffplayer->pipeline, mp->ffplayer->vout);
return mp;
fail:
ijkmp_dec_ref_p(&mp);
return NULL;
}
- 创建管道
ffpipeline_create_from_android(mp->ffplayer);
ijkmedia/ijkplayer/android/pipline/ffpipeline_android.c#134
IJKFF_Pipeline *ffpipeline_create_from_android(FFPlayer *ffp)
{
ALOGD("ffpipeline_create_from_android()\n");
IJKFF_Pipeline *pipeline = ffpipeline_alloc(&g_pipeline_class, sizeof(IJKFF_Pipeline_Opaque));
if (!pipeline)
return pipeline;
IJKFF_Pipeline_Opaque *opaque = pipeline->opaque;
opaque->ffp = ffp;
opaque->surface_mutex = SDL_CreateMutex();
opaque->left_volume = 1.0f;
opaque->right_volume = 1.0f;
if (!opaque->surface_mutex) {
ALOGE("ffpipeline-android:create SDL_CreateMutex failed\n");
goto fail;
}
pipeline->func_destroy = func_destroy;
pipeline->func_open_video_decoder = func_open_video_decoder;
pipeline->func_open_audio_output = func_open_audio_output;
pipeline->func_init_video_decoder = func_init_video_decoder;
pipeline->func_config_video_decoder = func_config_video_decoder;
return pipeline;
fail:
ffpipeline_free_p(&pipeline);
return NULL;
}
- 注意
pipeline->func_open_video_decoder = func_open_video_decoder;
ijkmedia/ijkplayer/android/pipline/ffpipeline_android.c#38
、
/**
* 打开视频解码通道.
*/
static IJKFF_Pipenode *func_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp)
{
IJKFF_Pipenode* node = NULL;
IJKFF_Pipeline_Opaque *opaque = pipeline->opaque;
if (ffp->videotoolbox) {
node = ffpipenode_create_video_decoder_from_ios_videotoolbox(ffp);
if (!node)
ALOGE("vtb fail!!! switch to ffmpeg decode!!!! \n");
}
if (node == NULL) {
node = ffpipenode_create_video_decoder_from_ffplay(ffp);
ffp->stat.vdec_type = FFP_PROPV_DECODER_AVCODEC;
opaque->is_videotoolbox_open = false;
} else {
ffp->stat.vdec_type = FFP_PROPV_DECODER_VIDEOTOOLBOX;
opaque->is_videotoolbox_open = true;
}
ffp_notify_msg2(ffp, FFP_MSG_VIDEO_DECODER_OPEN, opaque->is_videotoolbox_open);
return node;
}
- 注意这一行
node = ffpipenode_create_video_decoder_from_ffplay(ffp);
ijkmedia/ijkplayer/android/pipline/ffpipeline_ffplay_vdec.c
/**
* 创建解码线程.
*/
IJKFF_Pipenode *ffpipenode_create_video_decoder_from_ffplay(FFPlayer *ffp)
{
IJKFF_Pipenode *node = ffpipenode_alloc(sizeof(IJKFF_Pipenode_Opaque));
if (!node)
return node;
IJKFF_Pipenode_Opaque *opaque = node->opaque;
opaque->ffp = ffp;
node->func_destroy = func_destroy;
node->func_run_sync = func_run_sync;
ffp_set_video_codec_info(ffp, AVCODEC_MODULE_NAME, avcodec_get_name(ffp->is->viddec.avctx->codec_id));
ffp->stat.vdec_type = FFP_PROPV_DECODER_AVCODEC;
return node;
}
- 下面调用了`func_run_sync
static int func_run_sync(IJKFF_Pipenode *node) { IJKFF_Pipenode_Opaque *opaque = node->opaque; return ffp_video_thread(opaque->ffp); }
- 让我来看看
ijkmedia/ijkplayer/ff_ffplay.c#ffp_video_thread(FFPlayer *ffp)
int ffp_video_thread(FFPlayer *ffp)
{
return ffplay_video_thread(ffp);
}
/**
* 视频解码线程 .
*/
static int ffplay_video_thread(void *arg)
{
FFPlayer *ffp = arg;
VideoState *is = ffp->is;
AVFrame *frame = av_frame_alloc();
double pts;
double duration;
int ret;
AVRational tb = is->video_st->time_base;
//AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);
int64_t dst_pts = -1;
int64_t last_dst_pts = -1;
int retry_convert_image = 0;
int convert_frame_count = 0;
#if CONFIG_AVFILTER
AVFilterGraph *graph = avfilter_graph_alloc();
AVFilterContext *filt_out = NULL, *filt_in = NULL;
int last_w = 0;
int last_h = 0;
enum AVPixelFormat last_format = -2;
int last_serial = -1;
int last_vfilter_idx = 0;
if (!graph) {
av_frame_free(&frame);
return AVERROR(ENOMEM);
}
#else
ffp_notify_msg2(ffp, FFP_MSG_VIDEO_ROTATION_CHANGED, ffp_get_video_rotate_degrees(ffp));
#endif
if (!frame) {
#if CONFIG_AVFILTER
avfilter_graph_free(&graph);
#endif
return AVERROR(ENOMEM);
}
for (;;) {
ret = get_video_frame(ffp, frame);
if (ret < 0)
goto the_end;
if (!ret)
continue;
if (ffp->get_frame_mode) {
if (!ffp->get_img_info || ffp->get_img_info->count <= 0) {
av_frame_unref(frame);
continue;
}
last_dst_pts = dst_pts;
if (dst_pts < 0) {
dst_pts = ffp->get_img_info->start_time;
} else {
dst_pts += (ffp->get_img_info->end_time - ffp->get_img_info->start_time) / (ffp->get_img_info->num - 1);
}
pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
pts = pts * 1000;
if (pts >= dst_pts) {
while (retry_convert_image <= MAX_RETRY_CONVERT_IMAGE) {
ret = convert_image(ffp, frame, (int64_t)pts, frame->width, frame->height);
if (!ret) {
convert_frame_count++;
break;
}
retry_convert_image++;
av_log(NULL, AV_LOG_ERROR, "convert image error retry_convert_image = %d\n", retry_convert_image);
}
retry_convert_image = 0;
if (ret || ffp->get_img_info->count <= 0) {
if (ret) {
av_log(NULL, AV_LOG_ERROR, "convert image abort ret = %d\n", ret);
ffp_notify_msg3(ffp, FFP_MSG_GET_IMG_STATE, 0, ret);
} else {
av_log(NULL, AV_LOG_INFO, "convert image complete convert_frame_count = %d\n", convert_frame_count);
}
goto the_end;
}
} else {
dst_pts = last_dst_pts;
}
av_frame_unref(frame);
continue;
}
#if CONFIG_AVFILTER
if ( last_w != frame->width
|| last_h != frame->height
|| last_format != frame->format
|| last_serial != is->viddec.pkt_serial
|| ffp->vf_changed
|| last_vfilter_idx != is->vfilter_idx) {
SDL_LockMutex(ffp->vf_mutex);
ffp->vf_changed = 0;
av_log(NULL, AV_LOG_DEBUG,
"Video frame changed from size:%dx%d format:%s serial:%d to size:%dx%d format:%s serial:%d\n",
last_w, last_h,
(const char *)av_x_if_null(av_get_pix_fmt_name(last_format), "none"), last_serial,
frame->width, frame->height,
(const char *)av_x_if_null(av_get_pix_fmt_name(frame->format), "none"), is->viddec.pkt_serial);
avfilter_graph_free(&graph);
graph = avfilter_graph_alloc();
if ((ret = configure_video_filters(ffp, graph, is, ffp->vfilters_list ? ffp->vfilters_list[is->vfilter_idx] : NULL, frame)) < 0) {
// FIXME: post error
SDL_UnlockMutex(ffp->vf_mutex);
goto the_end;
}
filt_in = is->in_video_filter;
filt_out = is->out_video_filter;
last_w = frame->width;
last_h = frame->height;
last_format = frame->format;
last_serial = is->viddec.pkt_serial;
last_vfilter_idx = is->vfilter_idx;
//frame_rate = av_buffersink_get_frame_rate(filt_out);
SDL_UnlockMutex(ffp->vf_mutex);
}
ret = av_buffersrc_add_frame(filt_in, frame);
if (ret < 0)
goto the_end;
while (ret >= 0) {
is->frame_last_returned_time = av_gettime_relative() / 1000000.0;
ret = av_buffersink_get_frame_flags(filt_out, frame, 0);
if (ret < 0) {
if (ret == AVERROR_EOF)
is->viddec.finished = is->viddec.pkt_serial;
ret = 0;
break;
}
is->frame_last_filter_delay = av_gettime_relative() / 1000000.0 - is->frame_last_returned_time;
if (fabs(is->frame_last_filter_delay) > AV_NOSYNC_THRESHOLD / 10.0)
is->frame_last_filter_delay = 0;
tb = av_buffersink_get_time_base(filt_out);
#endif
//duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
//直接这里写出
duration=0.01;
pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
ret = queue_picture(ffp, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
av_frame_unref(frame);
#if CONFIG_AVFILTER
}
#endif
if (ret < 0)
goto the_end;
}
the_end:
#if CONFIG_AVFILTER
avfilter_graph_free(&graph);
#endif
av_log(NULL, AV_LOG_INFO, "convert image convert_frame_count = %d\n", convert_frame_count);
av_frame_free(&frame);
return 0;
}
ret = get_video_frame(ffp, frame);
static int get_video_frame(VideoState *is, AVFrame *frame)
{
int got_picture;
if ((got_picture = decoder_decode_frame(&is->viddec, frame, NULL)) < 0)
return -1;
if (got_picture) {
frame->sample_aspect_ratio = av_guess_sample_aspect_ratio(is->ic, is->video_st, frame);
//……
}
return got_picture;
}
decoder_decode_frame
static int decoder_decode_frame(FFPlayer *ffp, Decoder *d, AVFrame *frame, AVSubtitle *sub) {
int ret = AVERROR(EAGAIN);
for (;;) {
AVPacket pkt;
if (d->queue->serial == d->pkt_serial) {
do {
if (d->queue->abort_request)
return -1;
switch (d->avctx->codec_type) {
case AVMEDIA_TYPE_VIDEO:
ret = avcodec_receive_frame(d->avctx, frame);
if (ret >= 0) {
ffp->stat.vdps = SDL_SpeedSamplerAdd(&ffp->vdps_sampler, FFP_SHOW_VDPS_AVCODEC, "vdps[avcodec]");
if (ffp->decoder_reorder_pts == -1) {
frame->pts = frame->best_effort_timestamp;
} else if (!ffp->decoder_reorder_pts) {
frame->pts = frame->pkt_dts;
}
}
break;
case AVMEDIA_TYPE_AUDIO:
ret = avcodec_receive_frame(d->avctx, frame);
if (ret >= 0) {
AVRational tb = (AVRational){1, frame->sample_rate};
if (frame->pts != AV_NOPTS_VALUE)
frame->pts = av_rescale_q(frame->pts, av_codec_get_pkt_timebase(d->avctx), tb);
else if (d->next_pts != AV_NOPTS_VALUE)
frame->pts = av_rescale_q(d->next_pts, d->next_pts_tb, tb);
if (frame->pts != AV_NOPTS_VALUE) {
d->next_pts = frame->pts + frame->nb_samples;
d->next_pts_tb = tb;
}
}
break;
default:
break;
}
if (ret == AVERROR_EOF) {
d->finished = d->pkt_serial;
avcodec_flush_buffers(d->avctx);
return 0;
}
if (ret >= 0)
return 1;
} while (ret != AVERROR(EAGAIN));
}
do {
if (d->queue->nb_packets == 0)
SDL_CondSignal(d->empty_queue_cond);
if (d->packet_pending) {
av_packet_move_ref(&pkt, &d->pkt);
d->packet_pending = 0;
} else {
if (packet_queue_get_or_buffering(ffp, d->queue, &pkt, &d->pkt_serial, &d->finished) < 0)
return -1;
}
} while (d->queue->serial != d->pkt_serial);
if (pkt.data == flush_pkt.data) {
avcodec_flush_buffers(d->avctx);
d->finished = 0;
d->next_pts = d->start_pts;
d->next_pts_tb = d->start_pts_tb;
} else {
if (d->avctx->codec_type == AVMEDIA_TYPE_SUBTITLE) {
int got_frame = 0;
ret = avcodec_decode_subtitle2(d->avctx, sub, &got_frame, &pkt);
if (ret < 0) {
ret = AVERROR(EAGAIN);
} else {
if (got_frame && !pkt.data) {
d->packet_pending = 1;
av_packet_move_ref(&d->pkt, &pkt);
}
ret = got_frame ? 0 : (pkt.data ? AVERROR(EAGAIN) : AVERROR_EOF);
}
} else {
if (avcodec_send_packet(d->avctx, &pkt) == AVERROR(EAGAIN)) {
av_log(d->avctx, AV_LOG_ERROR, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");
d->packet_pending = 1;
av_packet_move_ref(&d->pkt, &pkt);
}
}
av_packet_unref(&pkt);
}
}
}
继续分析vcache的获取路径
- 才过了一周我就发现这个方法也不靠谱了,有几个流还是延迟2s左右,
IJkMediaplayer.java
public long getVideoCachedDuration() {
return this._getPropertyLong(20005, 0L);
}
- 继续跟进到
ff_ffplay.c
int64_t ffp_get_property_int64(FFPlayer *ffp, int id, int64_t default_value)
{
switch (id) {
case FFP_PROP_INT64_SELECTED_VIDEO_STREAM:
if (!ffp || !ffp->is)
return default_value;
return ffp->is->video_stream;
case FFP_PROP_INT64_SELECTED_AUDIO_STREAM:
if (!ffp || !ffp->is)
return default_value;
return ffp->is->audio_stream;
case FFP_PROP_INT64_SELECTED_TIMEDTEXT_STREAM:
if (!ffp || !ffp->is)
return default_value;
return ffp->is->subtitle_stream;
case FFP_PROP_INT64_VIDEO_DECODER:
if (!ffp)
return default_value;
return ffp->stat.vdec_type;
case FFP_PROP_INT64_AUDIO_DECODER:
return FFP_PROPV_DECODER_AVCODEC;
case FFP_PROP_INT64_VIDEO_CACHED_DURATION:
if (!ffp)
return default_value;
return ffp->stat.video_cache.duration;
...
}
- 让我们来看下``
typedef struct FFPlayer {
//静态存放视频缓存信息.
FFStatistic stat;
}
typedef struct FFStatistic
{
int64_t vdec_type;
float vfps;
float vdps;
float avdelay;
float avdiff;
int64_t bit_rate;
FFTrackCacheStatistic video_cache;
FFTrackCacheStatistic audio_cache;
int64_t buf_backwards;
int64_t buf_forwards;
int64_t buf_capacity;
SDL_SpeedSampler2 tcp_read_sampler;
int64_t latest_seek_load_duration;
int64_t byte_count;
int64_t cache_physical_pos;
int64_t cache_file_forwards;
int64_t cache_file_pos;
int64_t cache_count_bytes;
int64_t logical_file_size;
int drop_frame_count;
int decode_frame_count;
float drop_frame_rate;
} FFStatistic;
/*****************************************************************************
* end at line 330 in ffplay.c
* near packet_queue_put
****************************************************************************/
typedef struct FFTrackCacheStatistic
{
int64_t duration;
int64_t bytes;
int64_t packets;
} FFTrackCacheStatistic;
计算音视频同步delay的地方ff_ffpaly.c#1250
static double compute_target_delay(FFPlayer *ffp, double delay, VideoState *is)
{
double sync_threshold, diff = 0;
/* update delay to follow master synchronisation source */
if (get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER) {
/* if video is slave, we try to correct big delays by
duplicating or deleting a frame */
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;
}
}
if (ffp) {
ffp->stat.avdelay = delay;
ffp->stat.avdiff = diff;
}
#ifdef FFP_SHOW_AUDIO_DELAY
av_log(NULL, AV_LOG_TRACE, "video: delay=%0.3f A-V=%f\n",
delay, -diff);
#endif
return delay;
}
ff_ffplay.c#4646
void ffp_track_statistic_l(FFPlayer *ffp, AVStream *st, PacketQueue *q, FFTrackCacheStatistic *cache)
{
assert(cache);
if (q) {
cache->bytes = q->size;
cache->packets = q->nb_packets;
}
if (q && st && st->time_base.den > 0 && st->time_base.num > 0) {
cache->duration = q->duration * av_q2d(st->time_base) * 1000;
}
}
-
由上面的分析可知道,主要的延迟还是来自
PacketQueue * q*
- 所以延迟来自
PacketQueue
ff_ffplay.c#143#packet_queue_put_private
/**
*缓存一帧数据的封装packet.
**/
static int packet_queue_put_private(PacketQueue *q, AVPacket *pkt)
{
MyAVPacketList *pkt1;
if (q->abort_request)
return -1;
#ifdef FFP_MERGE
pkt1 = av_malloc(sizeof(MyAVPacketList));
#else
pkt1 = q->recycle_pkt;
if (pkt1) {
q->recycle_pkt = pkt1->next;
q->recycle_count++;
} else {
q->alloc_count++;
pkt1 = av_malloc(sizeof(MyAVPacketList));
}
#ifdef FFP_SHOW_PKT_RECYCLE
int total_count = q->recycle_count + q->alloc_count;
if (!(total_count % 50)) {
av_log(ffp, AV_LOG_DEBUG, "pkt-recycle \t%d + \t%d = \t%d\n", q->recycle_count, q->alloc_count, total_count);
}
#endif
#endif
if (!pkt1)
return -1;
pkt1->pkt = *pkt;
pkt1->next = NULL;
//全局刷新packet.
if (pkt == &flush_pkt)
q->serial++;
pkt1->serial = q->serial;
//后面插满了,则插在开头.
if (!q->last_pkt)
q->first_pkt = pkt1;
else
q->last_pkt->next = pkt1;
//强制下一个帧为pkt1.
q->last_pkt = pkt1;
q->nb_packets++;
q->size += pkt1->pkt.size + sizeof(*pkt1);
//计算一帧的duration最少的时间. 单位ms毫秒.
q->duration += FFMAX(pkt1->pkt.duration, MIN_PKT_DURATION);
/* XXX: should duplicate packet data in DV case */
SDL_CondSignal(q->cond);
return 0;
}
- 参考:ijkplayer在rtsp协议下使用经验 - 简书
- 关闭队列满载或者空时等待
ff_ffaplay.c#3508
/* this thread gets the stream from the disk or the network */
static int read_thread(void *arg)
{
/* wait 10 ms */
SDL_LockMutex(wait_mutex);
//SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
SDL_UnlockMutex(wait_mutex);
continue;
}
关闭队列满载或者空时暂停/启动切换
ff_ffplay.c#336
另外,还得吧ffp_toggle_buffering(ffp, 1) 的地方干掉, 不干掉的话,会发现ffp_toggle_buffering(ffp, 0) 执行时,我们没有动态码率的需求,VideoToolBox会重新创建, 没有必要浪费这性能(何况我们推流的SDP报文sps还必须重启服务器才会替换
static int packet_queue_get_or_buffering(FFPlayer *ffp, PacketQueue *q, AVPacket *pkt, int *serial, int *finished)
{
......
//ffp_toggle_buffering(ffp, 1);
.......
}
写在最后,上面的分析没有错误,v-cache确实代表是屁你的缓冲队列的大小,但是这里的延迟并不是因为读取数据的问题,关键还是音视频同步的处理。我们的O4球机延时通过PPlayer
的日志打印信息发现音频信息在前两秒audio_clock会比video_clock慢2s,因为ijkplayer默认是视频同步到音频,所以总会延迟2s也就可以理解了
解决方案:参见下一篇文章ijkplayer开启视频同步 打开视频同步即可.