什么星网站做调查问卷的,世界足球排名前十名,汕头公关公司,济南百搜科技ijkplayer是一款基于ffmpeg的在移动端比较流行的开源播放器。FFmpeg是一款用于多媒体处理、音视频编解码的自由软件工程#xff0c;采用LGPL或GPL许可证。
要想理解ijkplayer源码#xff0c;首先得知道视频播放器的基本原理。 视频播放器播放一个互联网上的视频文件#xf…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 indexwindex表示空数据开头的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)返回Frameif (!(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];
} 图中“...”的流程代表省略掉的一些函数调用可以看出音频、视频、字幕的解码都是调用的同一个函数。