ijkplayer解码流程源码解读

news2024/9/22 1:02:48

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];
}

 

图中“...”的流程代表省略掉的一些函数调用,可以看出,音频、视频、字幕的解码都是调用的同一个函数。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/447756.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

win10怎么录屏幕视频带声音?有哪些需要注意的地方?

随着科技的不断发展&#xff0c;屏幕录制已成为常见的工具之一。在Win10系统中&#xff0c;录制屏幕视频带声音也变得越来越简单。本文将指导您如何在Win10上实现录制屏幕视频带声音&#xff0c;并介绍一些需要注意的地方。 需要用到的工具和步骤 在Win10系统中&#xff0c;自…

【u盘提示:驱动器未格式化】如何解决?

u盘虽然使用很方便&#xff0c;可随时拷贝资料到任何有电脑的地方&#xff0c;但它的问题也是比较多的&#xff0c;其中u盘提示驱动器未格式化故障最让人心虚&#xff0c;因为已经无法打开u盘了&#xff0c;里面的资料怎么办&#xff0c;很重要的怎么办&#xff0c;所以今天就教…

OAuth2.0 实战总结

title: OAuth2.0 实战总结 date: 2023-01-30 11:23:12 tags: OAuth2.0 categories:开发技术及框架 cover: https://cover.png feature: false 1. 引言 1.1 OAuth 2.0 是什么&#xff1f; 用一句话总结来说&#xff0c;OAuth 2.0 就是一种授权协议。那如何理解这里的“授权”…

前端调试技巧

前端工作中&#xff0c;不仅编码很重要&#xff0c;重现bug&#xff0c;解决bug的能力同样重要。而这些都离不开代码调试。本文就一些调试技巧做出总结&#xff0c;不足之处&#xff0c;欢迎大家多多补充。 PC调试 console.log() 在你觉得有问题的代码处&#xff0c;加上一句 …

基于HFSS软件24GHz雷达天线设计

前言&#xff1a;近年来&#xff0c;我国的汽车保有量不断增加&#xff0c;汽车交通事故引发的财产损失和人 员伤亡也在不断增加。为了有效减少交通事故发生&#xff0c;车载雷达得到了各大汽车厂商和各个研究机构的普遍重视。现在国际上主要国家都把 24GHz 和 77GHz 两个频段分…

解决谷歌翻译不能使用的问题

今天登录国外网站&#xff0c;发现谷歌翻译已无法正常使用&#xff0c;网上最多的方法就是更改host文件&#xff0c;在host内增加ip地址&#xff0c;但是经常失效&#xff0c;经常手动更改增加ip着实烦恼&#xff0c;还有可能有别的错误。 最终解决方式是&#xff1a;登录GitH…

新版本ProPhet时间序列预测1:模型解释和安装和初步使用

ProPhet时间序列预测1安装和初步使用 安装pip安装conda安装 Prophet模型使用读取数据将数据处理为ds和y两列看下数据的分布模型训练预测可视化预测结果可视化趋势、假期、每周、季节性、年度季节性的回归图1.0之前模型保存 安装 安装1.0版本之后: 区别: 1.0版本叫prophet <…

设计模式-行为型模式之策略模式

5. 策略模式 5.1. 模式动机 完成一项任务&#xff0c;往往可以有多种不同的方式&#xff0c;每一种方式称为一个策略&#xff0c;我们可以根据环境或者条件的不同选择不同的策略来完成该项任务。 在软件开发中也常常遇到类似的情况&#xff0c;实现某一个功能有多个途径&#x…

【原理图专题】如何把PCB元件位号重排并反标到原理图

在画原理图时我们有时会复制以前项目或其他项目已经现成的模块进行电路的搭建。但可能会遇到一个问题,就是复制过来后位号也跟着过来了。比如说一个板子可能只有100个元件,但是会出现位号上千的元件。这对于后面我们焊接、维修等都很不利。 那可能有人会问,我直接在画完原理…

Redis持久化机制导致服务自启动后恢复数据过长无法使用以及如何关闭

场景 若依前后端分离版手把手教你本地搭建环境并运行项目&#xff1a; 若依前后端分离版手把手教你本地搭建环境并运行项目_霸道流氓气质的博客-CSDN博客 在上面搭建前后端分离的项目后&#xff0c;如果需要在windows服务上进行部署。 若依前后端分离版本&#xff0c;Windo…

58 openEuler搭建Mariadb数据库服务器-管理数据库

文章目录 58 openEuler搭建Mariadb数据库服务器-管理数据库58.1 创建数据库58.2 查看数据库58.3 选择数据库58.4 删除数据库58.5 备份数据库58.6 恢复数据库 58 openEuler搭建Mariadb数据库服务器-管理数据库 58.1 创建数据库 可以使用CREATE DATABASE语句来创建数据库。 CR…

Jetson Nano (4GB)烧写jetbot Image

一、Image下载 1、官方下载&#xff0c;浏览器访问Using SD Card Image - JetBot&#xff0c;下载jetbot-043_nano-4gb-jp45.zip。 注意&#xff1a;通过此方式下载需要电脑能访问外网。 2、天翼网盘下载&#xff0c;https://cloud.189.cn/t/q2mUJv7jaIna &#xff08;访问码…

【三十天精通Vue 3】 第十五天 Vue 3的异步组件和代码拆分

✅创作者&#xff1a;陈书予 &#x1f389;个人主页&#xff1a;陈书予的个人主页 &#x1f341;陈书予的个人社区&#xff0c;欢迎你的加入: 陈书予的社区 &#x1f31f;专栏地址: 三十天精通 Vue 3 文章目录 引言一、Vue 3 中的异步组件1.1 异步组件的概念1.2 Vue 3 中的异步…

Python机器学习、深度学习技术提升气象、海洋、水文领域应用

Python是功能强大、免费、开源&#xff0c;实现面向对象的编程语言&#xff0c;能够在不同操作系统和平台使用&#xff0c;简洁的语法和解释性语言使其成为理想的脚本语言。除了标准库&#xff0c;还有丰富的第三方库&#xff0c;Python在数据处理、科学计算、数学建模、数据挖…

设计模式--原型模式

目录 基本介绍 传统方式克隆 原型模式改进 浅拷贝和深拷贝 浅拷贝的介绍 深拷贝的介绍 原型模式的注意事项和细节 基本介绍 (1) 原型模式(prototype模式): 用原型实例指定创建对象的种类 并且通过拷贝这些原型 创建新的对象 (2) 原型模式是一种创建型设计模式 允许一个…

妙用Java 8中的 Function接口,消灭if...else

在开发过程中经常会使用if...else...进行判断抛出异常、分支处理等操作。这些if...else...充斥在代码中严重影响了代码代码的美观&#xff0c;这时我们可以利用Java 8的Function接口来消灭if...else...。 if (...){throw new RuntimeException("出现异常了")&#x…

全网详细解决1093 - You can‘t specify target table ‘xxx‘ for update in FROM clause的错误

文章目录 1. 复现错误2. 分析错误3. 解决错误 1. 复现错误 今天在工作时&#xff0c;接到一个新需求&#xff0c;就是将app_page_button表中的label_code字段修改为edit&#xff0c;条件如下&#xff1a; 只更新值为null的label_code 且以/edit/${id}结尾的option_value 首先…

基于微信小程序+爬虫制作一个表情包小程序

跟朋友聊天斗图失败气急败坏的我选择直接制作一个爬虫表情包小程序,从源头解决问题,从此再也不用担心在斗图中落入下风 精彩专栏持续更新↓↓↓ 微信小程序实战开发专栏 一、API1.1 项目创建1.2 图片爬虫帮助类1.3 测试窗体1.4 接口封装二、小程序2.1 项目创建2.2 页面设计2.…

css 实现太极效果

目录 一、简述二、太极效果制作 一、简述 本次主要介绍::after&#xff0c;::before&#xff0c;box-shadow这三个属性。 ::after&#xff0c;::before这两个是伪类选择器&#xff0c;box-shaow是用来设置元素的阴影效果 before:向选定的元素前插入内容 after:向选定的元素后插…

关于在线CAD编辑的解决方案思考

基于ODA的VisualizeJs库的能力 ODA官网的OpenCloud提供了在线浏览CAD图纸的能力(ODA自身也不支持直接在线解析Dwg&#xff0c;而是通过ODA提供的WebTools里面的FileConverter的能力将dwg图纸转换为VSF格式(CAD的一种二三维格式&#xff0c;在AutoCAD中也有一种类似的ForgeView…