ijkplayer解碼流程源碼解讀

ijkplayer是一款基于ffmpeg的在移動(dòng)端比較流行的開源播放器昏滴。FFmpeg是一款用于多媒體處理、音視頻編解碼的自由軟件工程骆捧,采用LGPL或GPL許可證侍筛。

要想理解ijkplayer源碼,首先得知道視頻播放器的基本原理葫督。


播放器原理圖.png

視頻播放器播放一個(gè)互聯(lián)網(wǎng)上的視頻文件竭鞍,需要經(jīng)過(guò)以下幾個(gè)步驟:解協(xié)議,解封裝候衍,音視頻解碼笼蛛,音視頻同步。如果播放的是本地文件則不需要解協(xié)議蛉鹿。

ijkplayer核心源碼都在C文件中滨砍。解碼流程主要涉及到的文件是ijkplayer_jni.c、ijkplayer.c妖异、ff_ffplay.c惋戏。第一個(gè)文件是java與c之間的jni層文件,第二個(gè)文件主要是加了鎖他膳,然后調(diào)用的ff_ffplay.c文件中的代碼响逢。具體核心功能實(shí)現(xiàn)還是在ff_ffplay.c文件中。

1 解封裝

入口函數(shù)為ffp_prepare_async_l棕孙,其中調(diào)用了stream_open方法舔亭。

stream_open()是比較重要的一個(gè)方法些膨,里邊創(chuàng)建了解封裝線程。

static VideoState *stream_open(FFPlayer *ffp, const char *filename, AVInputFormat *iformat)
{
  ...
    is->video_refresh_tid = SDL_CreateThreadEx(&is->_video_refresh_tid, video_refresh_thread, ffp, "ff_vout");
    if (!is->video_refresh_tid) {
        av_freep(&ffp->is);
        return NULL;
    }

    is->initialized_decoder = 0;
    is->read_tid = SDL_CreateThreadEx(&is->_read_tid, read_thread, ffp, "ff_read");
    if (!is->read_tid) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateThread(): %s\n", SDL_GetError());
        goto fail;
    }
  ...
}

VideoState和FFPlayer是2個(gè)非常重要的結(jié)構(gòu)體钦铺,VideoState保存在FFPlayer中订雾,而在FFPlayer在ff_ffplay.c文件中的大部分函數(shù)中都會(huì)傳入其指針,VideoState中保存了播放器的操作狀態(tài)以及其他一些重要信息矛洞。如果需要對(duì)ijkplayer源碼進(jìn)行修改洼哎,一些信息可以保存到FFPlayer或VideoState中。

read_thread()//ret = av_read_frame(ic, pkt); 讀出一個(gè)packet數(shù)據(jù),放入隊(duì)列queue中

static int read_thread(void *arg){
...
//打開輸入源
err = avformat_open_input(&ic, is->filename, is->iformat, &ffp->format_opts);
...
//獲取視頻流信息
err = avformat_find_stream_info(ic, opts);
...
// 根據(jù)音頻/視頻/字幕調(diào)用3次
    /* open the streams */
    if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
        stream_component_open(ffp, st_index[AVMEDIA_TYPE_AUDIO]);
    } else {
        ffp->av_sync_type = AV_SYNC_VIDEO_MASTER;
        is->av_sync_type  = ffp->av_sync_type;
    }

    ret = -1;
    if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
        ret = stream_component_open(ffp, st_index[AVMEDIA_TYPE_VIDEO]);
    }
    if (is->show_mode == SHOW_MODE_NONE)
        is->show_mode = ret >= 0 ? SHOW_MODE_VIDEO : SHOW_MODE_RDFT;

    if (st_index[AVMEDIA_TYPE_SUBTITLE] >= 0) {
        stream_component_open(ffp, st_index[AVMEDIA_TYPE_SUBTITLE]);
    }
...
    for (;;) {  
//開啟循環(huán)沼本,如果用戶進(jìn)行了停止操作噩峦,則返回
        if (is->abort_request)
            break;
...
//執(zhí)行解封裝
        ret = av_read_frame(ic, pkt);
...
//解封裝后將packet保存到VideoState的音頻、視頻抽兆、字幕packet隊(duì)列中
        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);
        } else if (pkt->stream_index == is->subtitle_stream && pkt_in_play_range) {
            packet_queue_put(&is->subtitleq, pkt);
        } 
  }

...
}
typedef struct VideoState {
...
PacketQueue audioq;
PacketQueue subtitleq;
PacketQueue videoq;
...
}

typedef struct PacketQueue {
    MyAVPacketList *first_pkt, *last_pkt;
    int nb_packets;
    int size;
    int64_t duration;
    int abort_request;
    int serial;
    SDL_mutex *mutex;
    SDL_cond *cond;
    MyAVPacketList *recycle_pkt;
    int recycle_count;
    int alloc_count;

    int is_buffer_indicator;
} PacketQueue;

C語(yǔ)言中沒(méi)有像C++那樣有容器识补,鏈表、隊(duì)列都需要自己實(shí)現(xiàn)郊丛。

stream_component_open函數(shù)

static int stream_component_open(FFPlayer *ffp, int stream_index)
{
    avctx = avcodec_alloc_context3(NULL);
    ret = avcodec_parameters_to_context(avctx, ic->streams[stream_index]->codecpar);
    codec = avcodec_find_decoder(avctx->codec_id);
    switch (avctx->codec_type) {
    case AVMEDIA_TYPE_AUDIO:
        if ((ret = audio_open(ffp, channel_layout, nb_channels, sample_rate, &is->audio_tgt)) < 0)
goto fail;
        decoder_init(&is->auddec, avctx, &is->audioq, is->continue_read_thread);
        if ((is->ic->iformat->flags & (AVFMT_NOBINSEARCH | AVFMT_NOGENSEARCH | AVFMT_NO_BYTE_SEEK)) && !is->ic->iformat->read_seek) {
            is->auddec.start_pts = is->audio_st->start_time;
            is->auddec.start_pts_tb = is->audio_st->time_base;
        }
    // audio_thread 是音頻解碼線程
        if ((ret = decoder_start(&is->auddec, audio_thread, ffp, "ff_audio_dec")) < 0)
            goto out;
        break;
    case AVMEDIA_TYPE_VIDEO:
            decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread);
// video_thread 是視頻解碼線程
        if ((ret = decoder_start(&is->viddec, video_thread, ffp, "ff_video_dec")) < 0)
            goto out;
        break;
    case AVMEDIA_TYPE_SUBTITLE:
        decoder_init(&is->subdec, avctx, &is->subtitleq, is->continue_read_thread);
        if ((ret = decoder_start(&is->subdec, subtitle_thread, ffp, "ff_subtitle_dec")) < 0)
            goto out;
        break;
  }
}

省略大部分代碼李请,只保留一些關(guān)鍵代碼瞧筛。主要作用就是創(chuàng)建解碼器上下文厉熟,獲取解碼器,打開解碼器等较幌。然后就是根據(jù)音頻揍瑟、視頻、字幕分別調(diào)用decoder_init乍炉、decoder_start函數(shù)绢片。

static void decoder_init(Decoder *d, AVCodecContext *avctx, PacketQueue *queue, SDL_cond *empty_queue_cond) {
    memset(d, 0, sizeof(Decoder));
    d->avctx = avctx;
    d->queue = queue;
    ...
}

在decoder_init函數(shù)中Decoder中的queue指針指向?qū)嶋H的解封裝后的隊(duì)列,后面音視頻解碼時(shí)岛琼,會(huì)從此隊(duì)列中拿出packet進(jìn)行解碼底循。

2 開始視頻解碼

decoder_start()中沒(méi)太多代碼,主要是調(diào)用SDL_CreateThreadEx創(chuàng)建音頻/視頻/字幕解碼線程

我們主要關(guān)注視頻的處理槐瑞,看video_thread函數(shù)熙涤,這個(gè)函數(shù)調(diào)用func_run_sync,然后后面一通沒(méi)太多邏輯的調(diào)用困檩,最終會(huì)執(zhí)行到ffplay_video_thread函數(shù)祠挫。

static int ffplay_video_thread(void *arg)
{
AVFrame *frame = av_frame_alloc();
...
    for (;;) {
        ret = get_video_frame(ffp, frame);
...
            duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
            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);
    }
}

ffplay_video_thread 會(huì)調(diào)用get_video_frame獲得解碼后的數(shù)據(jù)幀。然后通過(guò)queue_picture函數(shù)將解碼后數(shù)據(jù)幀塞到隊(duì)列中保存下來(lái)悼沿,以便渲染時(shí)去拿數(shù)據(jù)渲染等舔。

get_video_frame會(huì)調(diào)用decoder_decode_frame函數(shù),真正執(zhí)行音視頻的解碼糟趾。

decoder_decode_frame 函數(shù)

static int decoder_decode_frame(FFPlayer *ffp, Decoder *d, AVFrame *frame, AVSubtitle *sub) {
...
        if (d->queue->serial == d->pkt_serial) {
            do {
                if (d->queue->abort_request)
                    return -1;

                switch (d->avctx->codec_type) {
                    case AVMEDIA_TYPE_VIDEO:
                      // 從解碼器中獲得一陣解碼后的視頻幀  frame里面有長(zhǎng)/寬數(shù)據(jù)
                        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 {
  //從Decoder中保存的解封裝隊(duì)列(queue)里拿出一個(gè)packet慌植,保存到pkt中
                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);
...
            } else {
// 將pkt發(fā)送給解碼器進(jìn)行解碼
                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);
                }
            }
}

decoder_decode_frame函數(shù)會(huì)調(diào)用ffmpeg的avcodec_send_packet函數(shù)將解封裝后的數(shù)據(jù)塞給解碼器甚牲,并調(diào)用 avcodec_receive_frame函數(shù)從解碼器總獲得解碼后的音視頻數(shù)據(jù)幀。調(diào)試時(shí)發(fā)現(xiàn)剛開始播放時(shí)視頻解碼得到的frame里面的數(shù)據(jù)可能為空蝶柿,包括width鳖藕、height、linesize都為空只锭。所以如果要改用解碼后的視頻幀數(shù)據(jù)著恩,要先判斷下里面是否有數(shù)據(jù)。

3 解碼后視頻幀保存

視頻解碼完成了蜻展,需要保存解碼后的數(shù)據(jù)喉誊,以便渲染線程來(lái)拿數(shù)據(jù)渲染。視頻幀解碼后數(shù)據(jù)保存主要看queue_picture函數(shù)

static int queue_picture(FFPlayer *ffp, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial)
{
...
    if (!(vp = frame_queue_peek_writable(&is->pictq)))
        return -1;
...
      alloc_picture(ffp, src_frame->format);
...
    //將解碼后視頻幀保存到隊(duì)列中  
    frame_queue_push(&is->pictq);
...
}

queue_picture及alloc_picture中纵顾,以及還有幾個(gè)跟解碼后數(shù)據(jù)幀拷貝相關(guān)的函數(shù)伍茄,這塊還沒(méi)完全理清。除了解碼后YUV數(shù)據(jù)拷貝施逾,還涉及到一些色彩空間轉(zhuǎn)換敷矫。

再看frame_queue_push函數(shù)

static void frame_queue_push(FrameQueue *f)
{
    if (++f->windex == f->max_size)
        f->windex = 0;
    SDL_LockMutex(f->mutex);
    f->size++;
    SDL_CondSignal(f->cond);
    SDL_UnlockMutex(f->mutex);
}

typedef struct FrameQueue {
    Frame queue[FRAME_QUEUE_SIZE];
    int rindex;
    int windex;
    int size;
    int max_size;
    int keep_last;
    int rindex_shown;
    SDL_mutex *mutex;
    SDL_cond *cond;
    PacketQueue *pktq;
} FrameQueue;

這個(gè)函數(shù)很簡(jiǎn)單,就是更新一些索引及隊(duì)列大小汉额。隊(duì)列是循環(huán)重用的曹仗,隊(duì)列中的rindex表示數(shù)據(jù)開頭的index,也是讀取數(shù)據(jù)的index蠕搜,即read index怎茫,windex表示空數(shù)據(jù)開頭的index,是寫入數(shù)據(jù)的index妓灌,即write index轨蛤。

4 音頻解碼及數(shù)據(jù)保存

從前面可知stream_component_open中會(huì)調(diào)用decode_start函數(shù)創(chuàng)建音頻解碼線程audio_thread。

static int audio_thread(void *arg){
    AVFrame *frame = av_frame_alloc();
    Frame *af;
...
    //  音頻解碼
        if ((got_frame = decoder_decode_frame(ffp, &is->auddec, frame, NULL)) < 0)
            goto the_end;
...
              // 獲取隊(duì)列中可用于寫入寫入數(shù)據(jù)的隊(duì)列索引(windex)虫埂,根據(jù)(windex)返回Frame
                if (!(af = frame_queue_peek_writable(&is->sampq)))
                    goto the_end;

                af->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
                af->pos = frame->pkt_pos;
                af->serial = is->auddec.pkt_serial;
                af->duration = av_q2d((AVRational){frame->nb_samples, frame->sample_rate});

                av_frame_move_ref(af->frame, frame);
                frame_queue_push(&is->sampq);
...
}

可以看出audio_thread中音頻解碼流程比視頻流程更少一點(diǎn)祥山,直接調(diào)用decoder_decode_frame獲得解碼后數(shù)據(jù)幀frame,通過(guò)frame_queue_peek_writable函數(shù)獲取到隊(duì)列中下一個(gè)可用于音頻幀數(shù)據(jù)保存的位置(windex)掉伏,返回Frame用于解碼后音頻數(shù)據(jù)及相關(guān)信息保存缝呕。通過(guò)ffmpeg的av_frame_move_ref函數(shù)完成數(shù)據(jù)的拷貝,然后調(diào)用frame_queue_push更新windex岖免。

static Frame *frame_queue_peek_writable(FrameQueue *f)
{
    /* wait until we have space to put a new frame */
    SDL_LockMutex(f->mutex);
    while (f->size >= f->max_size &&
           !f->pktq->abort_request) {
        SDL_CondWait(f->cond, f->mutex);
    }
    SDL_UnlockMutex(f->mutex);

    if (f->pktq->abort_request)
        return NULL;

    return &f->queue[f->windex];
}

整體流程圖下圖所示:


ijkplayer解碼流程.png

圖中“...”的流程代表省略掉的一些函數(shù)調(diào)用岳颇,可以看出,音頻颅湘、視頻话侧、字幕的解碼都是調(diào)用的同一個(gè)函數(shù)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末闯参,一起剝皮案震驚了整個(gè)濱河市瞻鹏,隨后出現(xiàn)的幾起案子悲立,更是在濱河造成了極大的恐慌,老刑警劉巖新博,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件薪夕,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡赫悄,警方通過(guò)查閱死者的電腦和手機(jī)原献,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)埂淮,“玉大人姑隅,你說(shuō)我怎么就攤上這事【笞玻” “怎么了讲仰?”我有些...
    開封第一講書人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)痪蝇。 經(jīng)常有香客問(wèn)我鄙陡,道長(zhǎng),這世上最難降的妖魔是什么躏啰? 我笑而不...
    開封第一講書人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任趁矾,我火速辦了婚禮,結(jié)果婚禮上丙唧,老公的妹妹穿的比我還像新娘肩民。我一直安慰自己僵驰,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開白布怀估。 她就那樣靜靜地躺著溪厘,像睡著了一般胡本。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上畸悬,一...
    開封第一講書人閱讀 51,718評(píng)論 1 305
  • 那天侧甫,我揣著相機(jī)與錄音,去河邊找鬼蹋宦。 笑死披粟,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的冷冗。 我是一名探鬼主播守屉,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼蒿辙!你這毒婦竟也來(lái)了拇泛?” 一聲冷哼從身側(cè)響起滨巴,我...
    開封第一講書人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎俺叭,沒(méi)想到半個(gè)月后恭取,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡熄守,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年蜈垮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片裕照。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡窃款,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出牍氛,到底是詐尸還是另有隱情晨继,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布搬俊,位于F島的核電站紊扬,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏唉擂。R本人自食惡果不足惜餐屎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望玩祟。 院中可真熱鬧腹缩,春花似錦、人聲如沸空扎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)转锈。三九已至盘寡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間撮慨,已是汗流浹背竿痰。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留砌溺,地道東北人影涉。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像规伐,于是被迫代替她去往敵國(guó)和親蟹倾。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355

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