ijkplay播放直播流延時(shí)控制小結(jié)

歡迎加入 ijkplay 播放器學(xué)習(xí)群:490805051
本文討論ijkplay播放直播流延時(shí)現(xiàn)象產(chǎn)生的原因和解決方法。

原因

1,網(wǎng)絡(luò)抖動(dòng)

a),推流端因?yàn)榫W(wǎng)絡(luò)變差,buffer queue 會(huì)越來(lái)越大,等網(wǎng)絡(luò)恢復(fù)正常時(shí),再推流出去。當(dāng)然,推流端大家估計(jì)有不同的控制策略。
b),CDN 源節(jié)點(diǎn)到邊緣節(jié)點(diǎn)轉(zhuǎn)發(fā)網(wǎng)絡(luò)抖動(dòng)
c),播放器端拉流因?yàn)榫W(wǎng)絡(luò)變差,讀取不到數(shù)據(jù),等網(wǎng)絡(luò)恢復(fù)正常時(shí),會(huì)把之前的數(shù)據(jù)讀回來(lái)(CDN服務(wù)器緩存多少秒?),導(dǎo)致buffer queue變大.
設(shè)置player.shouldShowHudView=YES; 可以實(shí)時(shí)觀察音視頻緩沖區(qū)的大小,對(duì)于is中的videoq和audioq,存的是未解碼前數(shù)據(jù)包。

2,待補(bǔ)充

解決方法

推薦使用 cutv web播放端 播放直播流,或者使用 ffplay -fflags nobuffer -i 播放地址。
針對(duì)播放器端因網(wǎng)絡(luò)抖動(dòng)引起緩沖區(qū)變大,該怎么處理呢?

1,倍速播放

ijkplay雖然提供倍速播放的接口,但是需要Android 6.0以上,我這邊測(cè)試過(guò)效果并不好,沒(méi)有使用,但是這個(gè)倍速播放方法用戶體驗(yàn)會(huì)更好。
倍速播放,同時(shí)也需要解碼性能跟得上,當(dāng)然也可以只解碼I幀。

2,丟包

丟解碼前數(shù)據(jù)包還是解碼后的數(shù)據(jù)幀呢?
為了簡(jiǎn)單處理,這邊采用丟解碼前的數(shù)據(jù)包,策略如下:
a),有音頻流和視頻流,或者只有音頻流情況下,當(dāng)audioq達(dá)到一定的duration,就丟掉前面一部分?jǐn)?shù)據(jù)包,因?yàn)槟J(rèn)是AV_SYNC_AUDIO_MASTER,視頻會(huì)追上來(lái)。
b),只有視頻流情況,當(dāng)videoq達(dá)到一定的duration,就丟掉前面一部分?jǐn)?shù)據(jù)包。

下面是代碼,ijkplay版本是 0.5.1
ff_ffplay_def.h中添加最大緩存時(shí)長(zhǎng)

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 添加控制緩存隊(duì)列的方法

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? 這里如果不判斷是否是關(guān)鍵幀會(huì)導(dǎo)致視頻畫面花屏。但是這樣會(huì)導(dǎo)致全部清空的可能也會(huì)出現(xiàn)花屏
        // 所以這里推流端設(shè)置好 GOP 的大小,如果 max_cached_duration > 2 * GOP,可以盡可能規(guī)避全部清空
        // 也可以在調(diào)用control_queue_duration之前判斷新進(jìn)來(lái)的視頻pkt是否是關(guān)鍵幀,這樣即使全部清空了也不會(huì)花屏
        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?因?yàn)橛龅竭^(guò)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);  // 這里刪掉一半,你也可以自己修改,依據(jù)設(shè)置進(jìn)來(lái)的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后去判斷緩存隊(duì)列有沒(méi)有達(dá)到最大時(shí)長(zhǎng)。這里需要把原來(lái)的realtime設(shè)置為0

...
// 把原來(lái)的realtime設(shè)置為0,并從外部設(shè)置獲取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);
...
...
        // 每次讀取一個(gè)pkt,都去判斷處理
        // TODO:優(yōu)化,不用每次都調(diào)用
        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端使用實(shí)例代碼

    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秒,可以依據(jù)自己的需求修改
        [options setPlayerOptionIntValue:1 forKey:@"infbuf"];  // 無(wú)限讀
        [options setPlayerOptionIntValue:0 forKey:@"packet-buffering"];  //  關(guān)閉播放器緩沖
    } else {
        // Param for playback
        [options setPlayerOptionIntValue:0 forKey:@"max_cached_duration"];
        [options setPlayerOptionIntValue:0 forKey:@"infbuf"];
        [options setPlayerOptionIntValue:1 forKey:@"packet-buffering"];
    }

Android端使用實(shí)例代碼

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);    
}

@不鳴則已 提到的問(wèn)題請(qǐng)參考 @Gongjia 的文章 ijkplayer丟幀的處理方案

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,055評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,346評(píng)論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,889評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評(píng)論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,118評(píng)論 0 286
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,637評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,558評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,739評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評(píng)論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,980評(píng)論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評(píng)論 1 280
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,347評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,702評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容

  • 現(xiàn)今移動(dòng)直播技術(shù)上的挑戰(zhàn)要遠(yuǎn)遠(yuǎn)難于傳統(tǒng)設(shè)備或電腦直播,其完整的處理環(huán)節(jié)包括但不限于:音視頻采集、美顏/濾鏡/特效處...
    大榮紙閱讀 8,164評(píng)論 5 22
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,591評(píng)論 25 707
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,147評(píng)論 4 61
  • OWASP中國(guó):http://www.owasp.org.cn/ ESAPI (The OWASP Enterpr...
    helphi閱讀 890評(píng)論 0 0
  • 團(tuán)隊(duì)在實(shí)施敏捷開發(fā)的過(guò)程中經(jīng)常會(huì)遇到這樣的選擇:看板還是Scrum?看板和Scrum都是一種提供團(tuán)隊(duì)效率的一種工具...
    木葉丸閱讀 1,207評(píng)論 0 50