ffplay.c源碼閱讀之拉流模塊實現(xiàn)原理(二)

前言

這里所謂的拉流從就是指從本地文件或者遠(yuǎn)程文件不停獲取壓縮的音視頻數(shù)據(jù)包并緩存在本地待解碼的過程,用一張圖形象的畫出來其過程如下:


image.png
  • 拉流模塊

這里要有個拉流線程讓拉流模塊在此線程中不停的工作,它需要滿足忙時工作閑時休眠等待蕉斜,對于拉流模塊心赶,在ffmpeg的世界里也可以通俗的稱為解析器典唇,不同的協(xié)議從其中獲取數(shù)據(jù)的方式也不一樣,在ffmpeg中通過libavformat模塊實現(xiàn)了對各個協(xié)議(file鹦筹、http、https址貌、rtsp铐拐、rtmp徘键、hls)的支持,我們這里只需調(diào)用接口av_read_frame()即可遍蟋,具體的協(xié)議實現(xiàn)細(xì)節(jié)可以暫時不用關(guān)心吹害,ffmpeg已為我們封裝好了,我們只需要在編譯的時候?qū)⑦@些協(xié)議加進(jìn)去即可虚青。

  • 緩沖區(qū)

拉流模塊獲取的壓縮音視頻數(shù)據(jù)需要存放在一個緩沖區(qū)它呀,這個緩沖區(qū)稱之為壓縮數(shù)據(jù)緩沖區(qū)

拋出問題

  • 1、緩沖區(qū)如何實現(xiàn)棒厘?

這里的緩沖區(qū)用于存放從拉流模塊獲取的壓縮音視頻數(shù)據(jù)纵穿,同時它還會提供給解碼模塊使用,所以我覺得這個緩沖區(qū)要滿足如下幾個要求:
1奢人、讀寫線程安全谓媒,寫線程這里就是指拉流模塊所在線程,讀線程就是指解碼模塊所在線程
2何乎、隊列空或者滿時要有讓線程休眠的機制句惯,隊列空代表沒有待解碼的數(shù)據(jù)了,這時解碼線程應(yīng)該暫停解碼支救,也就是去休眠釋放cpu抢野,隊列滿時代表壓縮數(shù)據(jù)緩沖區(qū)數(shù)據(jù)太多,這時拉流模塊應(yīng)該休眠一段時間暫緩繼續(xù)讀取數(shù)據(jù)
3搂妻、高效讀寫蒙保,隊列大小可擴展。要保證讀寫安全欲主,那么首先想到的就是鎖機制邓厕,而鎖又會帶來性能損耗,如何保證高效的讀寫呢扁瓢?

2详恼、拉流模塊如何實現(xiàn)?
首先利用ffmpeg現(xiàn)成的接口av_read_frame()函數(shù)獲取壓縮音視頻以及字幕數(shù)據(jù)包引几,該接口在libavformat模塊實現(xiàn)昧互,支持本地/遠(yuǎn)程MP4文件的讀取,以及RTMP以及RTSP協(xié)議的直播流協(xié)議伟桅,其次也要考慮如下的情況:
1敞掘、由于本地文件讀取數(shù)據(jù)非常快可能出現(xiàn)緩沖區(qū)滿的情況楣铁、遠(yuǎn)程由于網(wǎng)絡(luò)原因讀取很慢會出現(xiàn)緩沖區(qū)空的情況玖雁,那么拉流模塊要處理這兩種極端的情況
2、對回放類型還要支持暫停盖腕,重新播放赫冬,以及拖動播放
3浓镜、多線程下所有跟拉流相關(guān)的線程可能導(dǎo)致數(shù)據(jù)安全問題(其實也就是值野指針問題)

ffplay.c中緩沖區(qū)的實現(xiàn)

ffplay.c中壓縮數(shù)據(jù)緩沖區(qū)是一個用單鏈表實現(xiàn)的隊列

typedef struct MyAVPacketList {
    AVPacket pkt;
    struct MyAVPacketList *next;
    int serial; // 標(biāo)記位,1代表拉流模塊已經(jīng)準(zhǔn)備妥當(dāng)劲厌,該包可用于解碼了
} MyAVPacketList;

/** 壓縮音視頻包隊列膛薛,用鏈表實現(xiàn)
 *  疑問:為什么壓縮音視頻包隊列用隊列實現(xiàn),而未壓縮音視頻包隊列FrameQueue卻是用數(shù)組實現(xiàn)的补鼻?
 *  分析:鏈表實現(xiàn)的隊列和數(shù)組實現(xiàn)的隊列區(qū)別就是鏈表方便擴展大小哄啄,壓縮數(shù)據(jù)包比未壓縮的要小很多,所以壓縮隊列不限制大小更適合辽幌。其次考慮到視頻幀過多的情況需要丟幀時增淹,應(yīng)該丟棄未壓縮視頻幀
 *  因為丟棄壓縮視頻中可能導(dǎo)致大量解碼出錯(比如剛好丟棄的是Idr幀)
 */
typedef struct PacketQueue {
    MyAVPacketList *first_pkt, *last_pkt;   //首尾指針
    int nb_packets; //當(dāng)前隊列的數(shù)據(jù)包個數(shù)
    int size;   // 這里是當(dāng)前隊列占用的內(nèi)存大小,并非壓縮視頻幀的大小
    int64_t duration;   // 當(dāng)前隊列所有視頻幀的總時長
    int abort_request;  // 工作結(jié)束的標(biāo)記乌企,為1時代表播放結(jié)束虑润,即將要銷毀等等
    int serial;         // 標(biāo)記位 1代表隊列是否已經(jīng)初始化并且插入了數(shù)據(jù)包,為1時隊列中的數(shù)據(jù)包才可以用于解碼
    SDL_mutex *mutex;   // 用于拉流線程和解碼線程的鎖和條件變量
    SDL_cond *cond;
} PacketQueue;

讀到這里的時候我也有個疑問加酵,為什么壓縮音視頻包隊列用單鏈表實現(xiàn)拳喻,而未壓縮音視頻包隊列FrameQueue卻是用數(shù)組實現(xiàn)的?分析:鏈表實現(xiàn)的隊列和數(shù)組實現(xiàn)的隊列區(qū)別就是鏈表方便擴展大小猪腕,由于壓縮數(shù)據(jù)包比未壓縮的要小很多冗澈,而且每個包的大小不固定,所以應(yīng)該在壓縮隊列中存儲更多的數(shù)據(jù)陋葡,其次考慮到視頻幀過多的情況需要丟幀時亚亲,應(yīng)該丟棄未壓縮視頻幀因為丟棄壓縮視頻中可能導(dǎo)致大量解碼出錯(比如剛好丟棄的是Idr幀),綜合以上兩種因素所以用鏈表來實現(xiàn)

這里主要看兩個函數(shù)腐缤,向隊列插入數(shù)據(jù)和從隊列讀取數(shù)據(jù)

向?qū)α胁迦霐?shù)據(jù)

static int packet_queue_put_private(PacketQueue *q, AVPacket *pkt)
{
    MyAVPacketList *pkt1;

    if (q->abort_request)
       return -1;

    pkt1 = av_malloc(sizeof(MyAVPacketList));
    if (!pkt1)
        return -1;
    pkt1->pkt = *pkt;
    pkt1->next = NULL;
    if (pkt == &flush_pkt)
        q->serial++;
    pkt1->serial = q->serial;

    if (!q->last_pkt)
        q->first_pkt = pkt1;
    else
        q->last_pkt->next = pkt1;
    q->last_pkt = pkt1;
    q->nb_packets++;
    q->size += pkt1->pkt.size + sizeof(*pkt1);
    q->duration += pkt1->pkt.duration;
    /* XXX: should duplicate packet data in DV case */
    SDL_CondSignal(q->cond);
    return 0;
}
static int packet_queue_put(PacketQueue *q, AVPacket *pkt)
{
    int ret;

    SDL_LockMutex(q->mutex);
    ret = packet_queue_put_private(q, pkt);
    SDL_UnlockMutex(q->mutex);

    if (pkt != &flush_pkt && ret < 0)
        av_packet_unref(pkt);

    return ret;
}

從隊列讀取數(shù)據(jù)

/* return < 0 if aborted, 0 if no packet and > 0 if packet.  */
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block, int *serial)
{
    MyAVPacketList *pkt1;
    int ret;

    SDL_LockMutex(q->mutex);

    for (;;) {
        if (q->abort_request) {
            ret = -1;
            break;
        }

        pkt1 = q->first_pkt;
        if (pkt1) {
            q->first_pkt = pkt1->next;
            if (!q->first_pkt)
                q->last_pkt = NULL;
            q->nb_packets--;
            q->size -= pkt1->pkt.size + sizeof(*pkt1);
            q->duration -= pkt1->pkt.duration;
            *pkt = pkt1->pkt;
            if (serial)
                *serial = pkt1->serial;
            av_free(pkt1);
            ret = 1;
            break;
        } else if (!block) {
            ret = 0;
            break;
        } else {
            SDL_CondWait(q->cond, q->mutex);
        }
    }
    SDL_UnlockMutex(q->mutex);
    return ret;
}

以上鏈表的操作還是比較容易理解的捌归,插入數(shù)據(jù)和讀取數(shù)據(jù)都會枷鎖保證線程安全

疑問:這里的緩沖單鏈表隊列采用鎖機制實現(xiàn)了線程安全,而且是全粒度的上鎖岭粤,每次上鎖均會帶來性能損耗惜索,如果實現(xiàn)一個無鎖鏈表那么效率會更高,會高多少了剃浇?

改良版無鎖隊列實現(xiàn)

待實現(xiàn)

ffplay.c中拉流模塊的實現(xiàn)

1巾兆、在main()函數(shù)啟動時調(diào)用stream_open()函數(shù)

is = stream_open(input_filename, file_iformat);
    if (!is) {
        av_log(NULL, AV_LOG_FATAL, "Failed to initialize VideoState!\n");
        do_exit(NULL);
    }

stream_open()函數(shù)的作用是創(chuàng)建VideoState結(jié)構(gòu)體對象,初始化壓縮音視頻字幕隊列PacketQueue虎囚,未壓縮音視頻音視頻字幕隊列FrameQueue角塑,然后打開拉流線程,打開拉流線程的代碼如下:

static VideoState *stream_open(const char *filename, AVInputFormat *iformat){
    ......省略代碼.....
    // 創(chuàng)建讀取線程用于讀取音視頻和字幕壓縮數(shù)據(jù)
    is->read_tid     = SDL_CreateThread(read_thread, "read_thread", is);
    if (!is->read_tid) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateThread(): %s\n", SDL_GetError());
    .....省略代碼......
}

接下來是拉流模塊的工作代碼

static int read_thread(void *arg)
{
    VideoState *is = arg;
    AVFormatContext *ic = NULL;
    int err, i, ret;
    int st_index[AVMEDIA_TYPE_NB];
    AVPacket pkt1, *pkt = &pkt1;
    int64_t stream_start_time;
    int pkt_in_play_range = 0;
    AVDictionaryEntry *t;
    SDL_mutex *wait_mutex = SDL_CreateMutex();
    int scan_all_pmts_set = 0;
    int64_t pkt_ts;

    ......... 省略拉流相關(guān)初始化代碼.......

        /** 學(xué)習(xí):拉流線程中當(dāng)所有的音視頻字幕隊列占用內(nèi)存超過指定MAX_QUEUE_SIZE(15M)后通過條件變量和互斥鎖等待10ms
         *  對于實時流(rtsp等等)不限制最大占用的內(nèi)存淘讥,因為畢竟是網(wǎng)絡(luò)圃伶,下載速度肯定跟不上解碼以及渲染速度,所以不可能出現(xiàn)內(nèi)存暴漲的情況适揉,但對于本地文件來說就有可能出現(xiàn)了留攒,
         *  所以這里的邏輯時針對本地文件處理的
         * infinite_buffer = 1 時代表實時流,實時流不做這樣的限制
         */
        /* if the queue are full, no need to read more */
        if (infinite_buffer<1 &&
              (is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE
            || (stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq) &&
                stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq) &&
                stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq)))) {
            /* wait 10 ms */
            SDL_LockMutex(wait_mutex);
            SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);  // 當(dāng)超標(biāo)后等待10ms嫉嘀,如果10ms內(nèi)隊列中數(shù)據(jù)處理完了這里又會被喚醒炼邀,然后繼續(xù)拉流
            SDL_UnlockMutex(wait_mutex);
            continue;
        }
        if (!is->paused &&
            (!is->audio_st || (is->auddec.finished == is->audioq.serial && frame_queue_nb_remaining(&is->sampq) == 0)) &&
            (!is->video_st || (is->viddec.finished == is->videoq.serial && frame_queue_nb_remaining(&is->pictq) == 0))) {
            if (loop != 1 && (!loop || --loop)) {
                stream_seek(is, start_time != AV_NOPTS_VALUE ? start_time : 0, 0, 0);
            } else if (autoexit) {
                ret = AVERROR_EOF;
                goto fail;
            }
        }
        ret = av_read_frame(ic, pkt);
        if (ret < 0) {
            /** 學(xué)習(xí):當(dāng)讀取到文件末尾時的處理邏輯
             *  分析:當(dāng)檢測到達(dá)文件末尾后,這里依然是通過條件變量加互斥鎖的方式讓線程休眠10ms剪侮,為什么這么做拭宁?因為播放結(jié)束后整個程序還在,這個拉流線程也沒有銷毀瓣俯,讓其休眠就不至于線程在那空轉(zhuǎn)
             */
            if ((ret == AVERROR_EOF || avio_feof(ic->pb)) && !is->eof) {
                if (is->video_stream >= 0)
                    packet_queue_put_nullpacket(&is->videoq, is->video_stream);
                if (is->audio_stream >= 0)
                    packet_queue_put_nullpacket(&is->audioq, is->audio_stream);
                if (is->subtitle_stream >= 0)
                    packet_queue_put_nullpacket(&is->subtitleq, is->subtitle_stream);
                is->eof = 1;
            }
            if (ic->pb && ic->pb->error)
                break;
            SDL_LockMutex(wait_mutex);
            SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
            SDL_UnlockMutex(wait_mutex);
            continue;
        } else {
            is->eof = 0;
        }
        
        /** 學(xué)習(xí):拖動和(首次播放指定其實播放時間時)杰标,讀取的壓縮音視頻數(shù)據(jù)邏輯處理
         *  分析:當(dāng)首次播放指定了播放開始時間或者用戶拖動時,有可能讀取的數(shù)據(jù)包是這個起始時間之前的數(shù)據(jù)彩匕,這時候要丟棄
         *  每一個音視頻包的pts和dts都有一個起始時間參考值腔剂,這個值就是保存在AVStream的start_time變量中(這是一個固定的值,音視頻流的值一般都不一樣驼仪,這樣是為了保證音視頻的對齊)掸犬,
         *  舉例,一個視頻包的pts為10绪爸,對應(yīng)的start_time為1湾碎,那么其相對于播放時間軸的pts就為10-1 = 9,所以如果播放起始時間是2s或者用戶拖動到了2s處奠货,那么就要將9換算成時間軸在與2s比較大小
         *  如果在2s之前則將此包丟棄介褥;pkt_in_play_range就代表了這個計算過程
         */
        /* check if packet is in play range specified by user, then queue, otherwise discard */
        stream_start_time = ic->streams[pkt->stream_index]->start_time;
        pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;
        pkt_in_play_range = duration == AV_NOPTS_VALUE ||
                (pkt_ts - (stream_start_time != AV_NOPTS_VALUE ? stream_start_time : 0)) *
                av_q2d(ic->streams[pkt->stream_index]->time_base) -
                (double)(start_time != AV_NOPTS_VALUE ? start_time : 0) / 1000000
                <= ((double)duration / 1000000);
        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->disposition & AV_DISPOSITION_ATTACHED_PIC)) {
            packet_queue_put(&is->videoq, pkt);
        } else if (pkt->stream_index == is->subtitle_stream && pkt_in_play_range) {
            packet_queue_put(&is->subtitleq, pkt);
        } else {
            av_packet_unref(pkt);
        }
    }

    ret = 0;
 fail:
    if (ic && !is->ic)
        avformat_close_input(&ic);

    if (ret != 0) {
        SDL_Event event;

        event.type = FF_QUIT_EVENT;
        event.user.data1 = is;
        SDL_PushEvent(&event);
    }
    SDL_DestroyMutex(wait_mutex);
    return 0;
}

read_thread就是拉流模塊的所有代碼了,它其實是基于libavformat實現(xiàn)的拉流功能递惋,這塊代碼在ffmpeg官網(wǎng)的示例中也可以找到柔滔,這里就不多解釋了。這里只看它的一些核心代碼點丹墨。1廊遍、它是如何控制當(dāng)壓縮緩沖區(qū)超過一定大小后的處理的?2贩挣、對于本地文件讀取到文件末尾時的處理邏輯 3喉前、拖動時的處理邏輯

  • 1、是如何控制當(dāng)壓縮緩沖區(qū)超過一定大小后的處理的王财?
    學(xué)習(xí):拉流線程中當(dāng)所有的音視頻字幕隊列占用內(nèi)存超過指定MAX_QUEUE_SIZE(15M)后通過條件變量和互斥鎖等待10ms卵迂,對于實時流(rtsp等等)不限制最大占用的內(nèi)存,因為畢竟是網(wǎng)絡(luò)绒净,下載速度肯定跟不上解碼以及渲染速度见咒,所以不可能出現(xiàn)內(nèi)存暴漲的情況,但對于本地文件來說就有可能出現(xiàn)了挂疆,所以這里的邏輯時針對本地文件處理的nfinite_buffer = 1 時代表實時流改览,實時流不做這樣的限制

  • 2下翎、讀取到文件末尾時的處理邏輯
    當(dāng)檢測到達(dá)文件末尾后,這里依然是通過條件變量加互斥鎖的方式讓線程休眠10ms宝当,為什么這么做视事?因為播放結(jié)束后整個程序還在,這個拉流線程也沒有銷毀庆揩,讓其休眠就不至于線程在那空轉(zhuǎn)

  • 3俐东、拖動和(首次播放指定其實播放時間時),讀取的壓縮音視頻數(shù)據(jù)邏輯處理
    當(dāng)首次播放指定了播放開始時間或者用戶拖動時订晌,有可能讀取的數(shù)據(jù)包是這個起始時間之前的數(shù)據(jù)虏辫,這時候要丟棄
    每一個音視頻包的pts和dts都有一個起始時間參考值,這個值就是保存在AVStream的start_time變量中(這是一個固定的值锈拨,音視頻流的值一般都不一樣砌庄,這樣是為了保證音視頻的對齊),舉例奕枢,一個視頻包的pts為10鹤耍,對應(yīng)的start_time為1,那么其相對于播放時間軸的pts就為10-1 = 9验辞,所以如果播放起始時間是2s或者用戶拖動到了2s處稿黄,那么就要將9換算成時間軸在與2s比較大小,如果在2s之前則將此包丟棄跌造;pkt_in_play_range就代表了這個計算過程

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末杆怕,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子壳贪,更是在濱河造成了極大的恐慌陵珍,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件违施,死亡現(xiàn)場離奇詭異互纯,居然都是意外死亡,警方通過查閱死者的電腦和手機磕蒲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進(jìn)店門留潦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人辣往,你說我怎么就攤上這事兔院。” “怎么了站削?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵坊萝,是天一觀的道長。 經(jīng)常有香客問我,道長十偶,這世上最難降的妖魔是什么菩鲜? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮惦积,結(jié)果婚禮上睦袖,老公的妹妹穿的比我還像新娘。我一直安慰自己荣刑,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布伦乔。 她就那樣靜靜地躺著厉亏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪烈和。 梳的紋絲不亂的頭發(fā)上爱只,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天,我揣著相機與錄音招刹,去河邊找鬼恬试。 笑死,一個胖子當(dāng)著我的面吹牛疯暑,可吹牛的內(nèi)容都是我干的训柴。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼妇拯,長吁一口氣:“原來是場噩夢啊……” “哼幻馁!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起越锈,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤仗嗦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后甘凭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體稀拐,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年丹弱,在試婚紗的時候發(fā)現(xiàn)自己被綠了德撬。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡躲胳,死狀恐怖砰逻,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情泛鸟,我是刑警寧澤蝠咆,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響刚操,放射性物質(zhì)發(fā)生泄漏闸翅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一菊霜、第九天 我趴在偏房一處隱蔽的房頂上張望坚冀。 院中可真熱鬧,春花似錦鉴逞、人聲如沸记某。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽液南。三九已至,卻和暖如春勾徽,著一層夾襖步出監(jiān)牢的瞬間滑凉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工喘帚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留畅姊,地道東北人。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓吹由,卻偏偏與公主長得像若未,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子倾鲫,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,486評論 2 348

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

  • 前言 之前陸續(xù)學(xué)習(xí)了視頻渲染相關(guān)技術(shù)opengl es,視頻編解碼相關(guān)技術(shù)(基于ffmpeg封裝接口的使用)玫荣,雖然...
    仙人掌__閱讀 952評論 0 0
  • 對于實時音視頻應(yīng)用來講甚淡,媒體數(shù)據(jù)從采集到渲染,在數(shù)據(jù)流水線上依次完成一系列處理捅厂。流水線由不同的功能模塊組成贯卦,彼此分...
    weizhenwei閱讀 11,035評論 4 29
  • 一、總體說明 1.打開ijkplayer焙贷,可看到其主要目錄結(jié)構(gòu)如下: tool - 初始化項目工程腳本 confi...
    laixh閱讀 2,125評論 0 0
  • 本文基于 ExoPlayer 2.13.2 版撵割。 本文將從 HLS 入手快速的分析一下 ExoPlayer 各個組...
    iBOBO閱讀 5,807評論 0 2
  • 16宿命:用概率思維提高你的勝算 以前的我是風(fēng)險厭惡者,不喜歡去冒險辙芍,但是人生放棄了冒險啡彬,也就放棄了無數(shù)的可能羹与。 ...
    yichen大刀閱讀 6,041評論 0 4