美文网首页三方管理ijkplayer秘籍视频开发
ijkplay播放直播流延时控制小结

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

作者: 暴走大牙 | 来源:发表于2016-09-21 16:52 被阅读12604次

    欢迎加入 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丢帧的处理方案

    相关文章

      网友评论

      • shenmiaotian:请问IJK_AVDISCARD_DEFAULT是什么呢,它的值是?
      • angle_miku:楼主你好,你这些方法和现在的版本方法位置都冲突,已经过时了,ijk-0.8.4
      • PaperFish:我之前的延迟在4秒左右,看完这个文章,完美的解决了我的延迟问题,现在延迟不到一秒。。。十分感谢
        shengshenger:@PaperFish 请问你推流用什么推得?
        PaperFish:@shengshenger 你肯定是推流没有做好,你得把推流和拉流都处理好
        shengshenger:哥们,长时间观察了么?也会出现延迟略大的问题
      • KeyboardLife:视频监控,两个i帧之间有二十九个p帧,请问下如何做丢帧处理呢,有没有啥好的算法,解决花屏问题
        KeyboardLife:@暴走大牙 倍速播放啥意思呢,这个也会引起马赛克??
        暴走大牙:除了丢帧就是倍速播放,视频监控的GOP一般很多,我之前遇到一个硬件摄像头输出的GOP间隔是25秒- -
      • 61d45bd09684:skip_loop_filter这设置有啥影响呢
      • 大美象:请问一下ios怎么获取音频流 保存成mp3
      • 那位小姐:我推流的时候 长期掉数据什么情况
      • 郑大爷:release模式编译,control_queue_duration报错:Static declaration of 'control_queue_duration' follows noo-static declaration
      • 不会算卦的杨大仙:改了这两文件后 打包framework只能选debug模式 选release模式打包加进项目中就会运行报错 为啥啊?
      • S__L:ios按照这个设置完之后播放.m3u8文件忽快忽慢,还停顿,正常么?
      • 815d19942012:编译通过了,谢谢!请问如何实现网络视频秒开呀?我现在设置播放源之后开始播放之前会出现几秒的黑屏再出现画面,请问这个怎么处理修改呢?望回复,谢谢
        30d510f59682:@菜鸟小白 作者不回复
        815d19942012: @cbbs 没呢,🤔
        30d510f59682:请问你这个问题解决没有了?
      • 815d19942012:大神你好,我根据你的文章添加了这些函数 编译通不过了,请问是还需要哪里有配置或者修改吗?本人新手,望指教
        815d19942012: @郑大爷好像 没报错呢!
        郑大爷:请问你那边是static void control_queue_duration这个报错了吗?如果是,方便说下怎么解决的吗?谢谢
      • 不鸣则已:请问关闭缓冲以后,IJKMPMoviePlayerLoadStateDidChangeNotification 这个就不能用来监听缓冲的进度,那么该如何来监听直播是否卡顿了呢?因为以前一直是靠这个监听来监听直播卡顿来转圈圈的。。。求解
        不会算卦的杨大仙:@不鸣则已 我也想解决网络卡顿转圈的问题 请问你有解决这个问题吗
        不鸣则已:@暴走大牙 我试过打开 但是没有设置缓冲 延迟又起来了
        暴走大牙:@不鸣则已 那可以打开 packet-buffering,对于直播,这个缓冲可以设置小一点
      • kkkore:楼主大大,你好。
        1、对您写的iOS方面的option配置,我有点儿没看明白。这块
        if (_isLive) {
        // Param for living
        ……
        } else {
        // Param for playback
        ……
        }
        在创建moviePlayerController时,不是应该要直接带入option参数吗?
        [[IJKFFMoviePlayerController alloc] initWithContentURLString:zhiboURLString withOptions:options];
        那如您所写的,那段条件判断,不是起不到作用吗?

        2、我的播放端所展示的视频与采集端的视频不同步,播放起来较流畅,偶尔卡顿,但是整体是比直播状态慢了10-15s左右。出现这个问题该如何解决?是因为什么原因引起的问题?怎么去赶上直播进度?您有遇到过这样的困扰吗?
        盼回复~~谢谢啦 :pray: :+1:
        kkkore:@暴走大牙 感谢楼主大大的回复。之前没想到点播这块。抱歉=。=
        暴走大牙:@kkkore 传入的url 是直播还是点播这不是提前就能知道吗?
      • 低智商游戏:control_queue_duration函数视频那一段好像永远也执行不到,因为VideoState都是基友视频流也有音频流,所以音频那里执行后就退出了。另外,每一帧都判断损耗太大,请问这方面您有什么思路吗?
        暴走大牙:@低智商游戏 因为存在视频源仅有视频流或者音频流的情况。
        可以增加一些条件,如读入的包为视频包切为关键帧,那么这个判断就是GOP间隔判断一次
        is->max_cached_duration > 0
        && pkt->stream_index == is->video_stream
        && (pkt->pkt.flags & AV_PKT_FLAG_KEY)
      • menttofly:播放rtsp流一快进就无限缓冲是怎么回事 :joy:
        书写传奇:@menttofly 能具体说说吗? 菜鸟一个不是很懂耶
        menttofly:@书写传奇 可以的,编译的时候用那个default的module
        书写传奇: @menttofly 这个能接rtsp流吗?不是不支持rtsp吗?
      • Link913:厉害了我的牙 :smirk:
      • 睡后3k:牙总赶紧把Android也写完啊

      本文标题:ijkplay播放直播流延时控制小结

      本文链接:https://www.haomeiwen.com/subject/oyuaettx.html