ijkplayer基于rtsp直播延时的深度优化

news2025/7/8 8:15:50

现在ijkPlayer是许多播放器、直播平台的首选,相信很多开发者都接触过ijkPlayer,无论是Android工程师还是iOS工程师。本文主要是总结,也是与大家探讨RTSP直播的延时优化。

目录

一、修改编译脚本支持RTSP

二、修改播放器的option参数

三、网络抖动的丢包

四、解码器设为零延时

五、减少FFmpeg拆帧等待延时

1、找到当前帧结束符

2、去掉parse_packet的while循环

3、 修改av_parser_parse2的帧偏移量

4、去掉parser_parse的寻找帧起始码

5、修改parser.c的组帧方法

一、修改编译脚本支持RTSP

ijkPlayer默认是没有把RTSP协议编译进去,所以我们得修改编译脚本,原来的disable改为enable:

export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-protocol=tcp"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-demuxer=rtsp"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-demuxer=sdp"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-demuxer=rtp"

二、修改播放器的option参数

//丢帧阈值
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30);
//视频帧率
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "fps", 30);
//环路滤波
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48);
//设置无packet缓存
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0);
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer");
//不限制拉流缓存大小
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1);
//设置最大缓存数量
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "max-buffer-size", 1024);
//设置最小解码帧数
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min-frames", 3);
//启动预加载
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1);
//设置探测包数量
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probsize", "4096");
//设置分析流时长
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", "2000000");

值得注意的是,ijkPlayer默认使用udp拉流,因为速度比较快。如果需要可靠且减少丢包,可以改为tcp协议:

mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp");

另外,可以这样开启硬解码,如果打开硬解码失败,再自动切换到软解码:

mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0);
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0);
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-resolution-change", 0);

【学习地址】:FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发
【文章福利】:免费领取更多音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击1079654574加群领取哦~

  

三、网络抖动的丢包

在拉流时,音频流、视频流是单独保存到缓冲队列的。如果发生网络抖动,就会引起缓冲抖动(JitBuffer),可以总结为网络卡顿导致音视频缓冲队列增大,从而导致解码滞后、播放滞后。此时,我们需要主动丢包来跟进当前时间戳。因为音视频同步一般以音频时钟为基准,人们对音频更加敏感,所以我们优先丢掉视频队列的包。但是,丢视频数据包时,需要丢掉整个GOP的数据包,因为B帧、P帧依赖I帧来解码,否则会引起花屏。

四、解码器设为零延时

大家应该听过编码器的零延时(zerolatency),但可能没听过解码器零延时。其实解码器内部默认会缓存几帧数据,用于后续关联帧的解码,大概是3-5帧。经过反复测试,发现解码器的缓存帧会带来100多ms延时。也就是说,假如能够去掉缓存帧,就可以减少100多ms的延时。而在avcodec.h文件的AVCodecContext结构体有一个参数(flags)用来设置解码器延时:

typedef struct AVCodecContext {
......
int flags;
......
}

为了去掉解码器缓存帧,我们可以把flags设置为CODEC_FLAG_LOW_DELAY。在初始化解码器时进行设置:

//set decoder as low deday
codec_ctx->flags |= CODEC_FLAG_LOW_DELAY;

五、减少FFmpeg拆帧等待延时

FFmpeg拆帧是根据下一帧的起始码来作为当前帧结束符,起始码一般是:0x00 0x00 0x00 0x01或者0x00 0x00 0x01。这样就会带来一帧的延时,这一帧延时能不能去掉呢?如果有帧结束符,我们以帧结束符来拆帧,这样做就能解决一帧延时。现在,问题变成找到帧结束符,然后替换成下一帧起始码来拆帧。整个调用流程是:read_frame—>read_frame_internal—>parse_packet—>av_parser_parse2—>parser_parse—>ff_combine_frame. 流程图如下:

1、找到当前帧结束符

在rtpdec.c文件的rtp_parse_packet_internal方法里,有获取帧结束符,也就是mark标志位,我们在这里设一个全局变量:

static int rtp_parse_packet_internal(RTPDemuxContext *s, AVPacket *pkt,
                                     const uint8_t *buf, int len)
{
    ......
 
    if (buf[1] & 0x80)
        flags |= RTP_FLAG_MARKER;
    //the end of a frame
    mark_flag = flags;
 
    ......
}

2、去掉parse_packet的while循环

我们在外部调用libavformat模块的utils.c文件的read_frame读取一帧数据,而read_frame调用内部方法read_frame_internal,read_frame_internal接着调用parse_packet方法,在该方法里有一个while循环体。现在把循环体去掉,并且释放申请的内存:

static int parse_packet(AVFormatContext *s, AVPacket *pkt, int stream_index)
{
    ......
 
//    while (size > 0 || (pkt == &flush_pkt && got_output)) {
        int len;
        int64_t next_pts = pkt->pts;
        int64_t next_dts = pkt->dts;
 
        av_init_packet(&out_pkt);
        len = av_parser_parse2(st->parser, st->internal->avctx,
                               &out_pkt.data, &out_pkt.size, data, size,
                               pkt->pts, pkt->dts, pkt->pos);
        pkt->pts = pkt->dts = AV_NOPTS_VALUE;
        pkt->pos = -1;
        /* increment read pointer */
        data += len;
        size -= len;
 
        got_output = !!out_pkt.size;
 
        if (!out_pkt.size){
            av_packet_unref(&out_pkt);//release current packet
            av_packet_unref(pkt);//release current packet
            return 0;
//            continue;
        }
    ......    
   
        ret = add_to_pktbuf(&s->internal->parse_queue, &out_pkt,
                            &s->internal->parse_queue_end, 1);
        av_packet_unref(&out_pkt);
        if (ret < 0)
            goto fail;
//    }
 
    /* end of the stream => close and free the parser */
    if (pkt == &flush_pkt) {
        av_parser_close(st->parser);
        st->parser = NULL;
    }
 
fail:
    av_packet_unref(pkt);
    return ret;
}

3、 修改av_parser_parse2的帧偏移量 在libavcodec模块的parser.c文件中,parse_packet调用到av_parser_parse2来解释数据包,该方法内部有记录帧偏移量。原先是等待下一帧的起始码,现在改为当前帧结束符,所以要把下一帧起始码这个偏移量长度去掉:

int av_parser_parse2(AVCodecParserContext *s, AVCodecContext *avctx,
                     uint8_t **poutbuf, int *poutbuf_size,
                     const uint8_t *buf, int buf_size,
                     int64_t pts, int64_t dts, int64_t pos)
{
    ......
 
    /* WARNING: the returned index can be negative */
    index = s->parser->parser_parse(s, avctx, (const uint8_t **) poutbuf,
                                    poutbuf_size, buf, buf_size);
    av_assert0(index > -0x20000000); // The API does not allow returning AVERROR codes
#define FILL(name) if(s->name > 0 && avctx->name <= 0) avctx->name = s->name
    if (avctx->codec_type == AVMEDIA_TYPE_VIDEO) {
        FILL(field_order);
    }
 
    /* update the file pointer */
    if (*poutbuf_size) {
        /* fill the data for the current frame */
        s->frame_offset = s->next_frame_offset;
 
        /* offset of the next frame */
//        s->next_frame_offset = s->cur_offset + index;
        //video frame don't plus index
        if (avctx->codec_type == AVMEDIA_TYPE_VIDEO) {
            s->next_frame_offset = s->cur_offset;
        }else{
            s->next_frame_offset = s->cur_offset + index;
        }
        s->fetch_timestamp   = 1;
    }
    if (index < 0)
        index = 0;
    s->cur_offset += index;
    return index;
}

4、去掉parser_parse的寻找帧起始码

av_parser_parse2调用到parser_parse方法,而我们这里使用的是h264解码,所以在libavcodec模块的h264_parser.c有一个结构体ff_h264_parser,把h264_parse赋值给parser_parse:

AVCodecParser ff_h264_parser = {
    .codec_ids      = { AV_CODEC_ID_H264 },
    .priv_data_size = sizeof(H264ParseContext),
    .parser_init    = init,
    .parser_parse   = h264_parse,
    .parser_close   = h264_close,
    .split          = h264_split,
};

现在我们需要h264_parser.c文件的h264_parse方法,去掉寻找下一帧起始码作为当前帧结束符的过程:

static int h264_parse(AVCodecParserContext *s,
                      AVCodecContext *avctx,
                      const uint8_t **poutbuf, int *poutbuf_size,
                      const uint8_t *buf, int buf_size)
{
    ......
 
    if (s->flags & PARSER_FLAG_COMPLETE_FRAMES) {
        next = buf_size;
    } else {
//TODO:don't use next frame start code, modify by xufulong
//        next = h264_find_frame_end(p, buf, buf_size, avctx);
 
        if (ff_combine_frame(pc, next, &buf, &buf_size) < 0) {
            *poutbuf      = NULL;
            *poutbuf_size = 0;
            return buf_size;
        }
 
/*        if (next < 0 && next != END_NOT_FOUND) {
            av_assert1(pc->last_index + next >= 0);
            h264_find_frame_end(p, &pc->buffer[pc->last_index + next], -next, avctx); // update state
        }*/
    }
 
    ......
}

5、修改parser.c的组帧方法

h264_parse又调用parser.c的ff_combine_frame组帧方法,我们在这里把mark替换起始码作为帧结束符:

external int mark_flag;//引用全局变量
 
int ff_combine_frame(ParseContext *pc, int next,const uint8_t **buf, int *buf_size)
{
    ......
 
    /* copy into buffer end return */
//    if (next == END_NOT_FOUND) {
        void *new_buffer = av_fast_realloc(pc->buffer, &pc->buffer_size,
                                           *buf_size + pc->index +
                                           AV_INPUT_BUFFER_PADDING_SIZE);
 
        if (!new_buffer) {
      
            pc->index = 0;
            return AVERROR(ENOMEM);
        }
        pc->buffer = new_buffer;
        memcpy(&pc->buffer[pc->index], *buf, *buf_size);
        pc->index += *buf_size;
//        return -1;
          if(!mark_flag)
            return -1;
        next = 0;
//    }
 
    ......
 
}

经过以上修改,局域网用电脑推送1080P、30fps的视频流,Android设备拉流解码播放,整体延时可优化至130ms左右。而手机推流,延时可达到86ms。

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

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

相关文章

WEB基础

互联网简介 互联网是世界上最大的计算机网络 互联网被称为网络的网络 万维网是互联网中的一个子网 WWW包含分散在世界范围内的众多Web 服务器&#xff08;World Wide Web&#xff09;WEB web即全球广域网World Wide Web&#xff0c;也称万维网&#xff0c;是一种基于超文本和HT…

基于ssm+mysql+jsp作业管理(在线学习)系统

基于ssmmysqljsp作业管理&#xff08;在线学习&#xff09;系统一、系统介绍二、功能展示1.用户登陆2.用户注册3.在线学习&#xff08;评论&#xff09;--学生4.任务列表--学生5.我的作业--学生6.个人中心7.发布课程--老师8.发布任务--老师9.评阅作业10.后台管理--管理员一、系…

RabbitMQ初步到精通-第五章-RabbitMQ之消息防丢失

目录 第五章-RabbitMQ之消息防丢失 1.消息是如何丢的 ​编辑 2.如何控制消息丢失 2.1 生产者发送消息到Broker过程 2.2 Broker内部过程 2.2.1 Exchange发送至queue过程-Return机制 2.2.2 queue存储过程 2.3 消费者消费过程-消费端确认 3.最佳实践 第五章-RabbitMQ之消息…

养老服务系统设计与实现-计算机毕业设计源码+LW文档

基于SSM的养老服务系统设计与实现 摘 要 本养老服务系统就是建立在充分利用现在完善科技技术这个理念基础之上&#xff0c;并使用IT技术进行对养老服务的管理&#xff0c;从而保证系统得到充分利用&#xff0c;可以实现养老服务的在线管理&#xff0c;这样保证了资源共享效率的…

牛客刷题记录(常见笔试题)

目录 一、Map的应用篇 乒 乓球筐 简单的错误记录 二、动态规划篇 计算字符串的编辑距离 年终奖 最长不含重复字符的子字符串 合唱团 三、数组篇 顺时针打印矩阵 一、Map的应用篇 乒 乓球筐 题目地址&#xff1a;乒乓球筐 小白代码 import java.util.*;// 注意类名必…

一次就能释放大量Mac内存空间的方法,你用过哪种?

清理Mac内存空间对Mac的运行速度有着非常大的好处&#xff0c;所以合理释放Mac内存空间是广大用户常做的一件事。那么小编整理了一些能够一次性大量释放Mac内存空间的方法&#xff0c;大家常用的是哪一种呢&#xff1f;欢迎一起交流哦~以下&#xff1a; 一、清理MAC缓存&#x…

微信小程序|从零动手实现俄罗斯方块

&#x1f4cc;个人主页&#xff1a;个人主页 ​&#x1f9c0; 推荐专栏&#xff1a;小程序开发成神之路 --【这是一个为想要入门和进阶小程序开发专门开启的精品专栏&#xff01;从个人到商业的全套开发教程&#xff0c;实打实的干货分享&#xff0c;确定不来看看&#xff1f; …

[Linux]----进程间通信之管道通信

文章目录前言一、进程间通信目的二、进程间通信发展三、进程间通信分类四、管道1. 匿名管道2. 管道内核代码3. 站在文件描述符角度-深度理解管道4. 站在内核角度-管道本质5. 管道的特征总结五、命名管道1. 创建命名管道总结前言 首先我基于通信背景来带大家了解进程间通讯&…

HTTP协议详细总结

目录 1.HTTP协议是什么? 2.什么叫做应用层协议 3.HTTP协议的工作流程 4.HTTP报文格式 请求报文: 响应报文: 5.URL 6.方法的认识 1.GET 2.POST 3.GET和POST的区别 4.其他方法 7.报头的认识 用户登陆过程: 8.状态码的认识 9.HTTPS 9.1HTTPS是什么? 9.2HTTPS的…

现代c++中实现精确延时方法总结

程序中实现延时有很多种办法&#xff0c;但是有些不建议用。比如还在用sleep()或者空转计数的方式延时&#xff1f;要么移植性不好&#xff0c;要么不够精确且效率太低。这里总结下现代c中推荐的一种实现精确延时的方法。 之前的一些用法 粗暴空转 long wait 0; while(wait…

十二、Mysql的索引

Mysql的索引十二、Mysql的索引一、什么是索引二、常见索引的种类(算法)三、B树 基于不同的查找算法分类介绍1、B树结构2、B-树四、索引的功能性分类1、辅助索引(S)及构建B树结构2、聚集索引(C)及构建B树结构3、聚集索引和辅助索引构成区别4、关于索引树的高度受什么影响五、索引…

Vue快速入门一:官网、下载、定义变量

Vue官网&#xff1a;Vue.js - 渐进式 JavaScript 框架 | Vue.js Vue2中文文档&#xff1a;Vue.js介绍 — Vue.jsVue.js Vue3中文文档&#xff1a;快速上手 | Vue.js Vue下载&#xff1a; Vue2下载&#xff1a; 引入Vue2版本&#xff1a;打开上面的中文文档&#xff0c;找到这…

Flink窗口及其分类-详细说明

文章目录&#x1f48e;Flink窗口的概念⚽窗口的分类&#x1faa9;窗口 API 概览⚾窗口分配器&#xff08;Window Assigners&#xff09;&#x1f603;&#x1f603;&#x1f603;&#x1f603;&#x1f603; 更多资源链接&#xff0c;欢迎访问作者gitee仓库&#xff1a;https:/…

Hive:BUG记录,错误使用动态分区导致的插入失败

1.场景 在Hive中&#xff0c;插入数据时可以指定动态分区&#xff0c;如果通过partition(day_partition)指定动态分区&#xff0c;而实际的select语句是直接把这个属性值写死了&#xff08;如‘2022-10-13’&#xff09;&#xff0c;就可以不需要指定hive变量set hive.exec.dy…

【牛客】四选一多路器

描述 制作一个四选一的多路选择器&#xff0c;要求输出定义上为线网类型 状态转换&#xff1a; d0 11 d1 10 d2 01 d3 00 信号示意图&#xff1a; 波形示意图&#xff1a; 输入描述&#xff1a; 输入信号 d1,d2,d3,d4 sel 类型 wire 输出描述&#xff1a; 输出信…

【信号处理】扩展卡尔曼滤波EKF(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜…

学习 MongoDB5 这一篇就够了

文章目录一、相关概念1.1、业务场景1.2、简介1.3、体系结构1.4、数据模型二、安装三、基本常用命令3.1、数据库操作3.2、集合操作3.3、文档基本CRUD1、插入2、查询3、更新4、删除3.4、分页查询3.5、更多查询3.6、小结四、索引4.1、概述4.2、类型4.3、管理操作4.4、索引的使用4.…

四十五、壁面函数理论及y+的确定

0. 前言 什么叫做壁面函数&#xff0c;为什么引入壁面函数的概念?? 因为流体无论流动&#xff0c;还是传热、传质都存在边界层。而之所以有壁面函数这个东西&#xff0c;根源就在于边界层理论。 1. 边界层理论 大家都知道什么是边界层理论&#xff0c;我们想要理解壁面函数…

美国这几年的人口死亡数据

2015年&#xff1a;总死亡271.20万&#xff0c;平均死亡年龄78.8 2016年&#xff1a;总死亡274.40万&#xff0c;平均死亡年龄78.6 2017年&#xff1a;总死亡281.35万&#xff0c;平均死亡年龄78.6 2018年&#xff1a;总死亡283.90万&#xff0c;平均死亡年龄78.7 2019年&#…

Java中的方法是什么?(Java系列2)

目录 前言&#xff1a; 1.什么是方法 2.方法的定义 3.方法调用的执行过程 4.实参和形参的关系 5.方法重载 6.方法签名 7.递归 8.关于“调用栈” 结束语&#xff1a; 前言&#xff1a; 在上一次博客中小编主要和大家分享了Java中的一些基础知识&#xff0c;与小编之前…