ijkplayer是一款基于ffmpeg的在移動端比較流行的開源播放器。FFmpeg是一款用于多媒體處理、音視頻編解碼的自由軟件工程,采用LGPL或GPL許可證。
要想理解ijkplayer源碼,首先得知道視頻播放器的基本原理。
視頻播放器播放一個互聯網上的視頻文件,需要經過以下幾個步驟:解協議,解封裝,音視頻解碼,音視頻同步。如果播放的是本地文件則不需要解協議。
ijkplayer核心源碼都在C文件中。解碼流程主要涉及到的文件是ijkplayer_jni.c、ijkplayer.c、ff_ffplay.c。第一個文件是java與c之間的jni層文件,第二個文件主要是加了鎖,然后調用的ff_ffplay.c文件中的代碼。具體核心功能實現還是在ff_ffplay.c文件中。
1 解封裝
入口函數為ffp_prepare_async_l,其中調用了stream_open方法。
stream_open()是比較重要的一個方法,里邊創建了解封裝線程。
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個非常重要的結構體,VideoState保存在FFPlayer中,而在FFPlayer在ff_ffplay.c文件中的大部分函數中都會傳入其指針,VideoState中保存了播放器的操作狀態以及其他一些重要信息。如果需要對ijkplayer源碼進行修改,一些信息可以保存到FFPlayer或VideoState中。
read_thread()//ret = av_read_frame(ic, pkt); 讀出一個packet數據,放入隊列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);
...
// 根據音頻/視頻/字幕調用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 (;;) {
//開啟循環,如果用戶進行了停止操作,則返回
if (is->abort_request)
break;
...
//執行解封裝
ret = av_read_frame(ic, pkt);
...
//解封裝后將packet保存到VideoState的音頻、視頻、字幕packet隊列中
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語言中沒有像C++那樣有容器,鏈表、隊列都需要自己實現。
stream_component_open函數
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;
}
}
省略大部分代碼,只保留一些關鍵代碼。主要作用就是創建解碼器上下文,獲取解碼器,打開解碼器等。然后就是根據音頻、視頻、字幕分別調用decoder_init、decoder_start函數。
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函數中Decoder中的queue指針指向實際的解封裝后的隊列,后面音視頻解碼時,會從此隊列中拿出packet進行解碼。
2 開始視頻解碼
decoder_start()中沒太多代碼,主要是調用SDL_CreateThreadEx創建音頻/視頻/字幕解碼線程
我們主要關注視頻的處理,看video_thread函數,這個函數調用func_run_sync,然后后面一通沒太多邏輯的調用,最終會執行到ffplay_video_thread函數。
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 會調用get_video_frame獲得解碼后的數據幀。然后通過queue_picture函數將解碼后數據幀塞到隊列中保存下來,以便渲染時去拿數據渲染。
get_video_frame會調用decoder_decode_frame函數,真正執行音視頻的解碼。
decoder_decode_frame 函數
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里面有長/寬數據
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中保存的解封裝隊列(queue)里拿出一個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發送給解碼器進行解碼
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函數會調用ffmpeg的avcodec_send_packet函數將解封裝后的數據塞給解碼器,并調用 avcodec_receive_frame函數從解碼器總獲得解碼后的音視頻數據幀。調試時發現剛開始播放時視頻解碼得到的frame里面的數據可能為空,包括width、height、linesize都為空。所以如果要改用解碼后的視頻幀數據,要先判斷下里面是否有數據。
3 解碼后視頻幀保存
視頻解碼完成了,需要保存解碼后的數據,以便渲染線程來拿數據渲染。視頻幀解碼后數據保存主要看queue_picture函數
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);
...
//將解碼后視頻幀保存到隊列中
frame_queue_push(&is->pictq);
...
}
queue_picture及alloc_picture中,以及還有幾個跟解碼后數據幀拷貝相關的函數,這塊還沒完全理清。除了解碼后YUV數據拷貝,還涉及到一些色彩空間轉換。
再看frame_queue_push函數
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;
這個函數很簡單,就是更新一些索引及隊列大小。隊列是循環重用的,隊列中的rindex表示數據開頭的index,也是讀取數據的index,即read index,windex表示空數據開頭的index,是寫入數據的index,即write index。
4 音頻解碼及數據保存
從前面可知stream_component_open中會調用decode_start函數創建音頻解碼線程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;
...
// 獲取隊列中可用于寫入寫入數據的隊列索引(windex),根據(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中音頻解碼流程比視頻流程更少一點,直接調用decoder_decode_frame獲得解碼后數據幀frame,通過frame_queue_peek_writable函數獲取到隊列中下一個可用于音頻幀數據保存的位置(windex),返回Frame用于解碼后音頻數據及相關信息保存。通過ffmpeg的av_frame_move_ref函數完成數據的拷貝,然后調用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];
}
整體流程圖下圖所示:
圖中“...”的流程代表省略掉的一些函數調用,可以看出,音頻、視頻、字幕的解碼都是調用的同一個函數。