ijkplay播放直播流延时控制小结

欢迎加入 ijkplay 播放器学习群:490805051
本文讨论ijkplay播放直播流延时现象产生的原因和解决方法。

原因

1,网络抖动

a),推流端因为网络变差,buffer queue 会越来越大,等网络恢复正常时,再推流出去。当然,推流端大家估计有不同的控制策略。
b),CDN 源节点到边缘节点转发网络抖动
c),播放器端拉流因为网络变差,读取不到数据,等网络恢复正常时,会把之前的数据读回来(CDN服务器缓存多少秒?),导致buffer queue变大.
设置player.shouldShowHudView=YES; 可以实时观察音视频缓冲区的大小,对于is中的videoq和audioq,存的是未解码前数据包。

2,待补充

解决方法

推荐使用 cutv web播放端 播放直播流,或者使用 ffplay -fflags nobuffer -i 播放地址。
针对播放器端因网络抖动引起缓冲区变大,该怎么处理呢?

1,倍速播放

ijkplay虽然提供倍速播放的接口,但是需要Android 6.0以上,我这边测试过效果并不好,没有使用,但是这个倍速播放方法用户体验会更好。
倍速播放,同时也需要解码性能跟得上,当然也可以只解码I帧。

2,丢包

丢解码前数据包还是解码后的数据帧呢?
为了简单处理,这边采用丢解码前的数据包,策略如下:
a),有音频流和视频流,或者只有音频流情况下,当audioq达到一定的duration,就丢掉前面一部分数据包,因为默认是AV_SYNC_AUDIO_MASTER,视频会追上来。
b),只有视频流情况,当videoq达到一定的duration,就丢掉前面一部分数据包。

下面是代码,ijkplay版本是 0.5.1
ff_ffplay_def.h中添加最大缓存时长

typedef struct VideoState {
    ...
    // Add by ljsdaya
    // for low delay time with live play(realtime), control videoq/audioq duration < max_cached_duration
    // realtime set to 0, max_cached_duration = 0 means is playback
    int max_cached_duration;
} VideoState;

ff_ffplay.c 添加控制缓存队列的方法

static void drop_queue_until_pts(PacketQueue *q, int64_t drop_to_pts) {
    MyAVPacketList *pkt1 = NULL;
    int del_nb_packets = 0;
    for (;;) {
        pkt1 = q->first_pkt;
        if (!pkt1) {
            break;
        }
        // video need key frame? 这里如果不判断是否是关键帧会导致视频画面花屏。但是这样会导致全部清空的可能也会出现花屏
        // 所以这里推流端设置好 GOP 的大小,如果 max_cached_duration > 2 * GOP,可以尽可能规避全部清空
        // 也可以在调用control_queue_duration之前判断新进来的视频pkt是否是关键帧,这样即使全部清空了也不会花屏
        if ((pkt1->pkt.flags & AV_PKT_FLAG_KEY) && pkt1->pkt.pts >= drop_to_pts) {
//        if (pkt1->pkt.pts >= drop_to_pts) {
            break;
        }
        q->first_pkt = pkt1->next;
        if (!q->first_pkt)
            q->last_pkt = NULL;
        q->nb_packets--;
        ++del_nb_packets;
        q->size -= pkt1->pkt.size + sizeof(*pkt1);
        if (pkt1->pkt.duration > 0)
            q->duration -= pkt1->pkt.duration;
        av_free_packet(&pkt1->pkt);
#ifdef FFP_MERGE
        av_free(pkt1);
#else
        pkt1->next = q->recycle_pkt;
        q->recycle_pkt = pkt1;
#endif
    }
    av_log(NULL, AV_LOG_INFO, "233 del_nb_packets = %d.\n", del_nb_packets);
}

static void control_video_queue_duration(FFPlayer *ffp, VideoState *is) {
    int time_base_valid = 0;
    int64_t cached_duration = -1;
    int nb_packets = 0;
    int64_t duration = 0;
    int64_t drop_to_pts = 0;
    
    //Lock
    SDL_LockMutex(is->videoq.mutex);
    
    time_base_valid = is->video_st->time_base.den > 0 && is->video_st->time_base.num > 0;
    nb_packets = is->videoq.nb_packets;
    
    // TOFIX: if time_base_valid false, calc duration with nb_packets and framerate
    // 为什么不用 videoq.duration?因为遇到过videoq.duration 一直为0,audioq也一样
    if (time_base_valid) {
        if (is->videoq.first_pkt && is->videoq.last_pkt) {
            duration = is->videoq.last_pkt->pkt.pts - is->videoq.first_pkt->pkt.pts;
            cached_duration = duration * av_q2d(is->video_st->time_base) * 1000;
        }
    }
    
    if (cached_duration > is->max_cached_duration) {
        // drop
        av_log(NULL, AV_LOG_INFO, "233 video cached_duration = %lld, nb_packets = %d.\n", cached_duration, nb_packets);
        drop_to_pts = is->videoq.last_pkt->pkt.pts - (duration / 2);  // 这里删掉一半,你也可以自己修改,依据设置进来的max_cached_duration大小
        drop_queue_until_pts(&is->videoq, drop_to_pts);
    }
    
    //Unlock
    SDL_UnlockMutex(is->videoq.mutex);
}

static void control_audio_queue_duration(FFPlayer *ffp, VideoState *is) {
    int time_base_valid = 0;
    int64_t cached_duration = -1;
    int nb_packets = 0;
    int64_t duration = 0;
    int64_t drop_to_pts = 0;
    
    //Lock
    SDL_LockMutex(is->audioq.mutex);
    
    time_base_valid = is->audio_st->time_base.den > 0 && is->audio_st->time_base.num > 0;
    nb_packets = is->audioq.nb_packets;
    
    // TOFIX: if time_base_valid false, calc duration with nb_packets and samplerate
    if (time_base_valid) {
        if (is->audioq.first_pkt && is->audioq.last_pkt) {
            duration = is->audioq.last_pkt->pkt.pts - is->audioq.first_pkt->pkt.pts;
            cached_duration = duration * av_q2d(is->audio_st->time_base) * 1000;
        }
    }
    
    if (cached_duration > is->max_cached_duration) {
        // drop
        av_log(NULL, AV_LOG_INFO, "233 audio cached_duration = %lld, nb_packets = %d.\n", cached_duration, nb_packets);
        drop_to_pts = is->audioq.last_pkt->pkt.pts - (duration / 2);
        drop_queue_until_pts(&is->audioq, drop_to_pts);
    }
    
    //Unlock
    SDL_UnlockMutex(is->audioq.mutex);
}

static void control_queue_duration(FFPlayer *ffp, VideoState *is) {
    if (is->max_cached_duration <= 0) {
        return;
    }
    
    if (is->audio_st) {
        return control_audio_queue_duration(ffp, is);
    }
    if (is->video_st) {
        return control_video_queue_duration(ffp, is);
    }
    
}

ff_ffplay.c read_thread 线程中,在每次 av_read_frame后去判断缓存队列有没有达到最大时长。这里需要把原来的realtime设置为0

...
// 把原来的realtime设置为0,并从外部设置获取max_cached_duration的值
//    is->realtime = is_realtime(ic);
    is->realtime = 0;
    AVDictionaryEntry *e = av_dict_get(ffp->player_opts, "max_cached_duration", NULL, 0);
    if (e) {
        int max_cached_duration = atoi(e->value);
        if (max_cached_duration <= 0) {
            is->max_cached_duration = 0;
        } else {
            is->max_cached_duration = max_cached_duration;
        }
    } else {
        is->max_cached_duration = 0;
    }

    if (true || ffp->show_status)
        av_dump_format(ic, 0, is->filename, 0);
...
...
        // 每次读取一个pkt,都去判断处理
        // TODO:优化,不用每次都调用
        if (is->max_cached_duration > 0) {
            control_queue_duration(ffp, is);
        }
        if (pkt->stream_index == is->audio_stream && pkt_in_play_range) {
            packet_queue_put(&is->audioq, pkt);
        } else if (pkt->stream_index == is->video_stream && pkt_in_play_range
                   && !(is->video_st && (is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC))) {
            packet_queue_put(&is->videoq, pkt);
#ifdef FFP_MERGE
        } else if (pkt->stream_index == is->subtitle_stream && pkt_in_play_range) {
            packet_queue_put(&is->subtitleq, pkt);
#endif
        } else {
            av_packet_unref(pkt);
        }
...
...

iOS端使用实例代码

    IJKFFOptions *options = [IJKFFOptions optionsByDefault];
    // Set param
    [options setFormatOptionIntValue:1024 * 16 forKey:@"probsize"];
    [options setFormatOptionIntValue:50000 forKey:@"analyzeduration"];
    [options setPlayerOptionIntValue:0 forKey:@"videotoolbox"];
    [options setCodecOptionIntValue:IJK_AVDISCARD_DEFAULT forKey:@"skip_loop_filter"];
    [options setCodecOptionIntValue:IJK_AVDISCARD_DEFAULT forKey:@"skip_frame"];
    if (_isLive) {
        // Param for living
        [options setPlayerOptionIntValue:3000 forKey:@"max_cached_duration"];   // 最大缓存大小是3秒,可以依据自己的需求修改
        [options setPlayerOptionIntValue:1 forKey:@"infbuf"];  // 无限读
        [options setPlayerOptionIntValue:0 forKey:@"packet-buffering"];  //  关闭播放器缓冲
    } else {
        // Param for playback
        [options setPlayerOptionIntValue:0 forKey:@"max_cached_duration"];
        [options setPlayerOptionIntValue:0 forKey:@"infbuf"];
        [options setPlayerOptionIntValue:1 forKey:@"packet-buffering"];
    }

Android端使用实例代码

ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 1024 * 16);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 50000);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 0);
ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_frame", 0);
if (mIsLive) {
    // Param for living
    ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 3000);
    ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1);
    ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0);
} else {
    // Param for playback
    ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 0);
    ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 0);
    ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 1);    
}

@不鸣则已 提到的问题请参考 @Gongjia 的文章 ijkplayer丢帧的处理方案

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • 现今移动直播技术上的挑战要远远难于传统设备或电脑直播,其完整的处理环节包括但不限于:音视频采集、美颜/滤镜/特效处...
    大荣纸阅读 8,080评论 5 22
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,520评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,029评论 4 62
  • OWASP中国:http://www.owasp.org.cn/ ESAPI (The OWASP Enterpr...
    helphi阅读 885评论 0 0
  • 团队在实施敏捷开发的过程中经常会遇到这样的选择:看板还是Scrum?看板和Scrum都是一种提供团队效率的一种工具...
    木叶丸阅读 1,196评论 0 50