ijkplayer解码流程源码解读
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)返回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];
}
图中“...”的流程代表省略掉的一些函数调用,可以看出,音频、视频、字幕的解码都是调用的同一个函数。
相关文章:

ijkplayer解码流程源码解读
ijkplayer是一款基于ffmpeg的在移动端比较流行的开源播放器。FFmpeg是一款用于多媒体处理、音视频编解码的自由软件工程,采用LGPL或GPL许可证。 要想理解ijkplayer源码,首先得知道视频播放器的基本原理。 视频播放器播放一个互联网上的视频文件…...

2023年值得关注的3个品牌趋势,帮你弯道超车
2023年,大环境开放,压抑三年的消费蓄势待发,品牌如何唤醒消费者的、热情成了重中之重的大事。 春风和煦,万物生长。又到了各类品牌、各位营销人踌躇满志、斗志昂扬的时候了,浅析一下2023品牌宣传趋势,抓住…...
软考-高级项目管理(二十)
第20章 高级项目管理 (P572考0-2分选择 性价比很低) 在项目集管理中涉及的相关角色主要包括: 项目集发起人、项目集指导委员会、项目集经理、其他影响项目集的干系人 1.项目集发起人 项目集发起人和收益人是负责承诺将组织的资源应用于项目集,并致力于使项目集取得…...
RTMP协议深度解析:从原理到实践,掌握实时流媒体传输技术
目录标题 1. 引言1.1 流媒体传输技术的重要性1.2 为什么选择RTMP协议1.3 RTMP协议的发展与应用 2. RTMP协议基础2.1 RTMP协议简介2.2 RTMP协议与其他流媒体协议的比较2.3 RTMP协议的组成与工作原理 3. RTMP协议详解3.1 RTMP数据单元(Message)3.2 RTMP数据…...
2023mathorcup数学建模ABCD思路分析
更多思路分析,请看文末 A题:量子计算机在信用评分卡组合优化中的应用 题目提到了信用评分卡的组合优化,这是一个经典的优化问题。在这个问题中,需要通过不同的组合方式来选择不同的阈值,以达到最大化贷款利息收入和最…...
普通家庭,千万不要投入大量时间和金钱,让孩子去苦学和培养AI机器人编程了...
普通家庭,千万不要投入大量时间和金钱,让孩子去苦学和培养一些看似高端,实际却用处不大的兴趣爱好课程了,比如学钢琴、学音乐、学AI机器人编程这些兴趣爱好课程。 这些对孩子的成长其实意义并不大,尤其是AI机器人编程。…...
C++学习(day2)
文章目录 四. C中的字符串4.1 C支持两种风格的字符串4.2 string类型的赋值和初始化4.3 C风格和C风格的字符串互换4.4 string类中三个重要成员函数4.5 string类型的比较4.6 string类型的成员访问 at()6.8 string类型数据的输入 五、bool类型六、引用(reference&#…...

软考 - IP地址与网络划分
一.IP组成 1.1 首个八位字节规则 1.2 地址掩码 IP地址掩码 标准地址掩码 A类:255.0.0.0 前1个字节是网络号 后3个字节是主机号 B类:255.255.0.0 前2个字节是网络号 后2个字节是主机号 C类;255.255.255.0 前3个字节是网络号 后1个字节是主机号…...

Apifox软件的基础使用方式
Apifox软件的基础使用方式 简单方便的用途 该工具是接口在线调试工具,这里我给到连接供大家去官网下载,我个人觉得是比较于postman工具好用,提供的语言操作是中文版本的便于操作 下载和安装 https://apifox.com/?utm_sourcebaidu&ut…...
【Tensorflow】模型如何加载HDF文件数据集?
如果每个样本都被保存为一个单独的 HDF5 文件,可以使用 tf.data.Dataset.list_files 函数来创建一个文件名数据集,然后使用 tf.data.Dataset.interleave 函数来并行读取多个文件。 下面的示例展示了如何从多个 HDF5 文件中读取数据并创建一个 tf.data.D…...

校招又临近了,怎么在面试中应对设计模式相关问题呢?
夏天开始了,那么夏天结束时的毕业季也不远了。毕业是个伤感、期待而又略带残酷的时节,就像蜜桃无论成熟与否都会在这个时间被采摘,如果毫无准备就踏入社会,就会……马上变成低级社畜。所以说还是要早点为了毕业找工作做点准备&…...

padans关于数据处理的杂谈
情况:业务数据基本字段会有如下: Index([时间, 地区, 产品, 字段, 数值], dtypeobject)这样就会引发一个经典“三角不可能定理”,如何同时简约展现分时序、分产品、分字段数据。)一般来说, 1、时序为作为单独的分类&…...
神经网络的理解
文章目录 概念得分函数损失函数神经网络结构非线性激活函数神经网络运行过程神经网络能够做的事情概念 人工神经网络(Artificial Neural Networks,简写为ANNs)也简称为神经网络(NNs)或称作连接模型(Connection Model),它是一种模仿动物神经网络行为特征,进行分布式并…...

夏驰和徐策带你从零开始学数据结构——哈希表
哈希表的概念: 哈希表是一种常用的数据结构,它可以在 O(1) 的时间复杂度内执行插入、查找和删除操作。哈希表的核心思想是使用哈希函数将键值对映射到数组中的一个位置上,从而实现快速的访问和修改。 哈希表由两个主要部分组成:…...

linux实现网络程序
1️⃣ 在linux下,通过套接字实现服务器和客户端的通信。 2️⃣ 实现单线程、多线程通信。或者实现线程池来通信。 3️⃣ 优化通信,增加守护进程。 有情提醒,类里面默认的函数是内联。内联函数在调用的地方展开,没有函数地址&…...

FreeRTOS 队列(二)
文章目录 一、向队列发送消息1. 函数原型(1)函数 xQueueOverwrite()(2)函数 xQueueGenericSend()(3)函数 xQueueSendFromISR()、xQueueSendToBackFromISR()、xQueueSendToFrontFromISR()(4&…...
用python获取当前目录下的创建时间超过3天的所有python文件
直接上代码: import os import datetime print(os.getcwd()) # 获取当前目录下所有的html文件 html_files [] for filename in os.listdir(): if filename.endswith(.py): html_files.append(os.path.join(., filename)) now date…...
第五章 Linux实际操作——用户管理
第五章 Linux实际操作——用户管理 5.1 基本介绍5.2 添加用户5.3 指定、修改密码5.4 删除用户5.5 查询用户信息指令5.6 切换用户5.7 查看当前用户、登录用户5.8 用户组5.9 用户和组相关文件8.9.1/etc/passwd 文件8.9.2/etc/shadow文件8.9.3/etc/group文件 5.1 基本介绍 Linux系…...
悲观锁和乐观锁详细
悲观锁和乐观锁详细 悲观锁 悲观锁就是悲观的思想,他认为数据每一次被访问的时候都会被上锁,所以每次获得锁的时候都会上锁,这样其他线程想要获取这个锁的时候就会被堵塞,要等待上一个线程锁的释放。也就是说这个线程只一次只…...

三谈ChatGPT(ChatGPT可以解决问题的90%)
这是我第三次谈ChatGPT,前两篇主要谈了ChatGPT的概念,之所以火的原因和对人们的影响,以及ChatGPT可能存在的安全风险和将面临的监管问题。这一篇主要讲讲ChatGPT的场景和处理问题的逻辑。 这一次我特意使用了ChatGPT中文网页版体验了一番。并…...
基于算法竞赛的c++编程(28)结构体的进阶应用
结构体的嵌套与复杂数据组织 在C中,结构体可以嵌套使用,形成更复杂的数据结构。例如,可以通过嵌套结构体描述多层级数据关系: struct Address {string city;string street;int zipCode; };struct Employee {string name;int id;…...
vscode里如何用git
打开vs终端执行如下: 1 初始化 Git 仓库(如果尚未初始化) git init 2 添加文件到 Git 仓库 git add . 3 使用 git commit 命令来提交你的更改。确保在提交时加上一个有用的消息。 git commit -m "备注信息" 4 …...
java 实现excel文件转pdf | 无水印 | 无限制
文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...
Leetcode 3577. Count the Number of Computer Unlocking Permutations
Leetcode 3577. Count the Number of Computer Unlocking Permutations 1. 解题思路2. 代码实现 题目链接:3577. Count the Number of Computer Unlocking Permutations 1. 解题思路 这一题其实就是一个脑筋急转弯,要想要能够将所有的电脑解锁&#x…...
React Native在HarmonyOS 5.0阅读类应用开发中的实践
一、技术选型背景 随着HarmonyOS 5.0对Web兼容层的增强,React Native作为跨平台框架可通过重新编译ArkTS组件实现85%以上的代码复用率。阅读类应用具有UI复杂度低、数据流清晰的特点。 二、核心实现方案 1. 环境配置 (1)使用React Native…...
五年级数学知识边界总结思考-下册
目录 一、背景二、过程1.观察物体小学五年级下册“观察物体”知识点详解:由来、作用与意义**一、知识点核心内容****二、知识点的由来:从生活实践到数学抽象****三、知识的作用:解决实际问题的工具****四、学习的意义:培养核心素养…...

PL0语法,分析器实现!
简介 PL/0 是一种简单的编程语言,通常用于教学编译原理。它的语法结构清晰,功能包括常量定义、变量声明、过程(子程序)定义以及基本的控制结构(如条件语句和循环语句)。 PL/0 语法规范 PL/0 是一种教学用的小型编程语言,由 Niklaus Wirth 设计,用于展示编译原理的核…...
根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:
根据万维钢精英日课6的内容,使用AI(2025)可以参考以下方法: 四个洞见 模型已经比人聪明:以ChatGPT o3为代表的AI非常强大,能运用高级理论解释道理、引用最新学术论文,生成对顶尖科学家都有用的…...
大数据学习(132)-HIve数据分析
🍋🍋大数据学习🍋🍋 🔥系列专栏: 👑哲学语录: 用力所能及,改变世界。 💖如果觉得博主的文章还不错的话,请点赞👍收藏⭐️留言Ǵ…...

Kafka入门-生产者
生产者 生产者发送流程: 延迟时间为0ms时,也就意味着每当有数据就会直接发送 异步发送API 异步发送和同步发送的不同在于:异步发送不需要等待结果,同步发送必须等待结果才能进行下一步发送。 普通异步发送 首先导入所需的k…...