暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

安卓客户端关于IJkplayer直播优化

1818

「前提」前一段时间在安卓定制项目中遇到了平板客户端直播延迟达到15s的情况,经过长时间调研现将ijkplayer拉流优化做相关总结

安卓客户端播放器的相关耗时和优化

ijkpler是基于开源框架FFmpeg二次开发的播放框架.

当设置一个数据源给播放器后,播放器需要「open_input」这个数流,设置相关的「options」,与服务端建立长连接,找到对应视频流和音频流。解析视频与音频「Packet」,解码「Packet」 转换成「Frame」.由安卓「nativeWindow」渲染画面「OpenSLES」播放声音.

我们可以按照安卓视频播放的流程进行如下方向优化:

  1. 拉流时网络请求用时
  2. 解复用(avformat_find_stream_info(formatContext, NULL)
    用时
  3. 解码用时
  4. 渲染到安卓设备屏幕用时

数据请求

网络比较畅通的情况视频流能及时发送,不会在缓冲区阻塞,直播画面流畅,网络不好的情况下,要合理设置缓冲区,做好丢帧,降码率方案. 因为音视频同步一般以音频时钟为基准,人们对音频更加敏感,所以我们优先丢掉视频队列的包。无论使用那种播放协议,都主要是基于tcp的,必然遵守tcp协议的特点,通过如下优化。

「下面部分是优化的点:」

static int tcp_read(URLContext *h, uint8_t *buf, int size)
{
    av_log(NULL, AV_LOG_INFO, "tcp_read begin %d\n", size);
    TCPContext *s = h->priv_data;
    int ret;
 
    if (!(h->flags & AVIO_FLAG_NONBLOCK)) {
        ret = ff_network_wait_fd_timeout(s->fd, 0, h->rw_timeout, &h->interrupt_callback);
        if (ret)
            return ret;
    }
    ret = recv(s->fd, buf, size, 0);
    if (ret == 0)
        return AVERROR_EOF;
    //if (ret > 0)
    //    av_application_did_io_tcp_read(s->app_ctx, (void*)h, ret);
    av_log(NULL, AV_LOG_INFO, "tcp_read end %d\n", ret);
    return ret < 0 ? ff_neterrno() : ret;
}

我们可以把上面两行注释掉,因为在ff_network_wait_fd_timeout等回来后,数据可以放到buf中,下面av_application_did_io_tcp_read就没必要去执行了。原来每次ret>0,都会执行av_application_did_io_tcp_read这个函数。

解复用耗时

拿到「AVFormatContext」,分离视频流与音频流时,首先需要匹配对应demuxer,ffmpeg的av_find_input_format和avformat_find_stream_info这两个函数执行花费时间比较长,前者简单理解就是打开某中请求到数据,后者就是探测流的一些信息,做一些样本检测,读取一定长度的码流数据,来分析码流的基本信息,为视频中各个媒体流的 AVStream 结构体填充好相应的数据。这个函数中做了查找合适的解码器、打开解码器、读取一定的音视频帧数据、尝试解码音视频帧等工作,基本上完成了解码的整个流程。该流程比较耗时

这两个函数调用都在ff_ffplay.c的read_thread函数中:

 if (ffp->iformat_name) {
        av_log(ffp, AV_LOG_INFO, "av_find_input_format noraml begin");
        is->iformat = av_find_input_format(ffp->iformat_name);
        av_log(ffp, AV_LOG_INFO, "av_find_input_format normal end");
    }
    else if (av_stristart(is->filename, "rtmp", NULL)) {
        av_log(ffp, AV_LOG_INFO, "av_find_input_format rtmp begin");
        is->iformat = av_find_input_format("flv");
        av_log(ffp, AV_LOG_INFO, "av_find_input_format rtmp end");
        ic->probesize = 4096;
        ic->max_analyze_duration = 2000000;
        ic->flags |= AVFMT_FLAG_NOBUFFER;
    }
    av_log(ffp, AV_LOG_INFO, "avformat_open_input begin");
    err = avformat_open_input(&ic, is->filename, is->iformat, &ffp->format_opts);
    av_log(ffp, AV_LOG_INFO, "avformat_open_input end");
    if (err < 0) {
        print_error(is->filename, err);
        ret = -1;
        goto fail;
    }
    ffp_notify_msg1(ffp, FFP_MSG_OPEN_INPUT);
 
    if (scan_all_pmts_set)
        av_dict_set(&ffp->format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE);
 
    if ((t = av_dict_get(ffp->format_opts, "", NULL, AV_DICT_IGNORE_SUFFIX))) {
        av_log(NULL, AV_LOG_ERROR, "Option %s not found.\n", t->key);
#ifdef FFP_MERGE
        ret = AVERROR_OPTION_NOT_FOUND;
        goto fail;
#endif
    }
    is->ic = ic;
 
    if (ffp->genpts)
        ic->flags |= AVFMT_FLAG_GENPTS;
 
    av_format_inject_global_side_data(ic);
 
    if (ffp->find_stream_info) {
        AVDictionary **opts = setup_find_stream_info_opts(ic, ffp->codec_opts);
        int orig_nb_streams = ic->nb_streams;
 
        do {
            if (av_stristart(is->filename, "data:", NULL) && orig_nb_streams > 0) {
                for (i = 0; i < orig_nb_streams; i++) {
                    if (!ic->streams[i] || !ic->streams[i]->codecpar || ic->streams[i]->codecpar->profile == FF_PROFILE_UNKNOWN) {
                        break;
                    }
                }
 
                if (i == orig_nb_streams) {
                    break;
                }
            }
            ic->probesize=100*1024;
            ic->max_analyze_duration=5*AV_TIME_BASE;
            ic->fps_probe_size=0;
            av_log(ffp, AV_LOG_INFO, "avformat_find_stream_info begin");
            err = avformat_find_stream_info(ic, opts);
            av_log(ffp, AV_LOG_INFO, "avformat_find_stream_info end");
        } while(0);
        ffp_notify_msg1(ffp, FFP_MSG_FIND_STREAM_INFO);

这样,avformat_find_stream_info 的耗时就可以缩减到 100ms 以内。

解码耗时和渲染出图耗时

最后就是解码耗时和渲染出图耗时,这块优化空间很少,大头都在前面。

直播一般都会做首屏秒开处理,当手机信号比较弱,遇到网络波动时,缓冲区没有足够的数据刷到播放队列就会视频就会产生卡顿,ijkplayer也做了比较好的缓冲机制来处理这个问题.

「主要是有几个宏控制:」



把BUFFERING_CHECK_PER_MILLISECONDS设置为50,默认是500:

#define DEFAULT_HIGH_WATER_MARK_IN_BYTES        (30 * 1024)
 
#define DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS     (100)
#define DEFAULT_NEXT_HIGH_WATER_MARK_IN_MS      (1 * 1000)
#define DEFAULT_LAST_HIGH_WATER_MARK_IN_MS      (1 * 1000)
 
#define BUFFERING_CHECK_PER_BYTES               (512)
#define BUFFERING_CHECK_PER_MILLISECONDS        (50)


「修改播放器的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");

文章转载自布尔科技技术团队,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论