iOS ijkplayer 硬解H265(hevc)4k视频问题解决

Posted 安卓开发-顺

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS ijkplayer 硬解H265(hevc)4k视频问题解决相关的知识,希望对你有一定的参考价值。

目录

一、mp2 音频格式下无声音问题的解决

二、视频卡顿、音视频不同步问题(硬解失败导致的)解决

1、创建播放器

2、创建解码器

3、确定问题及解决


接上一篇:Android、iOS ijkplayer编译步骤及相关问题解决

上一篇基于B站开源项目:https://github.com/bilibili/ijkplayer

编译成功ios版本的ijkplayer以后,进行了h265并且是4K(3840x2160)码流的播放测试,发现不管怎么尝试上层暴露的软硬解、丢帧等参数的配置

- (void)viewWillAppear:(BOOL)animated 
    ...

    // 尝试硬解 0 是软解 1是硬解
    // 这里提前剧透一下:这里的硬解设置没有生效,下面会详细解释
    IJKFFOptions *options = [IJKFFOptions optionsByDefault];
    [options setPlayerOptionIntValue:1 forKey:@"videotoolbox"];

    // 尝试通过增加丢帧数来让视频尽快追上音频
    [options setPlayerOptionIntValue:5 forKey:@"framedrop"];

    printf("---- zs log ----IJKMoviePlayerViewController prepareToPlay \\n");
    [self.player prepareToPlay];


都无法正常播放,具体表现为:

  • aac 格式音频播放正常
  • mp2 格式音频 无声音
  • 视频播放非常卡顿
  • 在aac 音频格式下,音频正常速度播放,视频由于卡顿,播放越来越慢从而音视频不同步现象随播放时间的增加而越来越明显

因此不得不从源码入手来分析解决问题:

一、mp2 音频格式下无声音问题的解决

这个问题相对比较好解决,因为源码是支持此格式的,只需要在配置中加入即可:

我这里用的配置文件是module-lite.sh,如果你使用的是module-lite-hevc.sh 或其他就改对应的配置文件就可以了。

第一步:打开ijkplayer-ios/config/module-lite.sh

第二步:在 # ./configure --list-decoders 这组下面添加:

export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=mpga"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=mp2"

第三步:在 # ./configure --list-muxers 这组下面添加:

export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-muxer=mpga"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-muxer=mp2"

第四步:在 # ./configure --list-demuxers 这组下面添加:

export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-demuxer=mpga"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-demuxer=mp2"

第五步:在 # ./configure --list-parsers 这组下面添加:

export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-parser=mpga"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-parser=mp2"

最后贴一张截图:

第六步:到config路径下删除module.sh ,然后执行:

ln -s module-lite.sh module.sh

     或者 到 根目录下执行:

./init-config.sh(此步骤会自动拷贝module-lite.sh文件内容到module.sh)

说白了你也可以不删除module.sh直接手动复制module-lite.sh里面的所有内容覆盖到module.sh里面

第七步:clean之后重新编译即可

./compile-ffmpeg.sh clean
./compile-ffmpeg.sh all

二、视频卡顿、音视频不同步问题(硬解失败导致的)解决

注意:如果是iPhone 7以下的手机 或者是 iOS版本低于11.0的,本身就不支持硬解。

这是本篇文章分析的重头戏,我们在梳理播放器核心流程的过程中一步步定位并解决问题:

1、创建播放器

从上层调用 IJKFFMoviePlayerControll 的 initWithContentURL方法开始,传入播放地址:

- (id)initWithContentURL:(NSURL *)aUrl withOptions:(IJKFFOptions *)options

...
    NSString *aUrlString = [aUrl isFileURL] ? [aUrl path] : [aUrl absoluteString];
    return [self initWithContentURLString:aUrlString withOptions:options];

继续调用到:initWithContentURLString方法

- (id)initWithContentURLString:(NSString *)aUrlString
                   withOptions:(IJKFFOptions *)options

    if (aUrlString == nil)
        return nil;

    self = [super init];
    if (self) 
        ijkmp_global_init();
        ijkmp_global_set_inject_callback(ijkff_inject_callback);

        [IJKFFMoviePlayerController checkIfFFmpegVersionMatch:NO];

        if (options == nil)
            options = [IJKFFOptions optionsByDefault];

        ...
        // init player --- 关键方法 初始化播放器
        _mediaPlayer = ijkmp_ios_create(media_player_msg_loop);
        ...

进行播放器的初始化:    _mediaPlayer = ijkmp_ios_create(media_player_msg_loop);

这行代码会调用到 ijkplayer_ios.m文件中

IjkMediaPlayer *ijkmp_ios_create(int (*msg_loop)(void*))

    //这是我们要关注的核心方法1
    IjkMediaPlayer *mp = ijkmp_create(msg_loop);
    if (!mp)
        goto fail;

    mp->ffplayer->vout = SDL_VoutIos_CreateForGLES2();
    if (!mp->ffplayer->vout)
        goto fail;

    //这是我们要关注的核心方法2
    mp->ffplayer->pipeline = ffpipeline_create_from_ios(mp->ffplayer);
    if (!mp->ffplayer->pipeline)
        goto fail;

    return mp;

fail:
    ijkmp_dec_ref_p(&mp);
    return NULL;

ijkmp_ios_create方法主要干了两件事:

  • 通过ijkmp_create方法创建了IjkMediaPlayer 播放器
  • 通过ffpipeline_create_from_ios 创建了ffpipeline(可以理解为解码器和音频输出的提供者)

创建过程到此结束。

2、创建解码器

还是从上层的调用入手:

IJKFFMoviePlayerController.m  --- >  prepareToPlay

- (void)prepareToPlay

    //设置播放url
    ijkmp_set_data_source(_mediaPlayer, [_urlString UTF8String]);
    ...    
    //异步准备
    ijkmp_prepare_async(_mediaPlayer);

跟进到:ijkplayer.c --- > ijkmp_prepare_async

int ijkmp_prepare_async(IjkMediaPlayer *mp)

    assert(mp);
    MPTRACE("ijkmp_prepare_async()\\n");
    pthread_mutex_lock(&mp->mutex);
    //关键流程方法
    int retval = ijkmp_prepare_async_l(mp);
    pthread_mutex_unlock(&mp->mutex);
    MPTRACE("ijkmp_prepare_async()=%d\\n", retval);
    return retval;

继续看 ijkplayer.c --- > ijkmp_prepare_async_l 方法

static int ijkmp_prepare_async_l(IjkMediaPlayer *mp)

...
    // released in msg_loop
    ijkmp_inc_ref(mp);
    ...
    //关键方法
    int retval = ffp_prepare_async_l(mp->ffplayer, mp->data_source);
    ...
    return 0;

跟进到:ff_ffplay.c ---> ffp_prepare_async_l 方法

int ffp_prepare_async_l(FFPlayer *ffp, const char *file_name)

    ...
    //关键方法
    VideoState *is = stream_open(ffp, file_name, NULL);

进入:ff_ffplay.c ---> stream_open方法

static VideoState *stream_open(FFPlayer *ffp, const char *filename, AVInputFormat *iformat)

    ...
    //ZS:启动read_thread 线程,在线程中会根据读取的文件或者流的信息去判断是否存在音频流和视频流,然后通过 stream_component_open 方法找到对应的解码器,启动解码线程。
    is->read_tid = SDL_CreateThreadEx(&is->_read_tid, read_thread, ffp, "ff_read");
...

进入到 ff_ffplay.c ---> read thread 看下:

/* this thread gets the stream from the disk or the network */
//ZS:读取文件或网络流的信息
static int read_thread(void *arg)

    ...
    /* open the streams */
    if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) 
        /* ZS:音频存在 去查找对应的解码器 启动解码线程*/
        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) 
        /* ZS:视频存在 去查找对应的解码器 启动解码线程*/
        ret = stream_component_open(ffp, st_index[AVMEDIA_TYPE_VIDEO]);
    
    ...

这里我们只关注视频,进入到 ff_ffplay.c ---> stream_component_open方法

/* open a given stream. Return 0 if OK */
static int stream_component_open(FFPlayer *ffp, int stream_index)

...
    case AVMEDIA_TYPE_VIDEO:
        
        if (ffp->async_init_decoder) 
            while (!is->initialized_decoder) 
                SDL_Delay(5);
            
            if (ffp->node_vdec) 
                is->viddec.avctx = avctx;
                ret = ffpipeline_config_video_decoder(ffp->pipeline, ffp);
            
            if (ret || !ffp->node_vdec) 
                decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread);
                ffp->node_vdec = ffpipeline_open_video_decoder(ffp->pipeline, ffp);
                if (!ffp->node_vdec)
                    goto fail;
            
         else 
            decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread);
            //打开视频解码器,我们这里是同步的情况
            ffp->node_vdec = ffpipeline_open_video_decoder(ffp->pipeline, ffp);
            if (!ffp->node_vdec)
                goto fail;
        
        ...

跟进到ff_ffpipeline.c --->ffpipeline_open_video_decoder

IJKFF_Pipenode* ffpipeline_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp)

    return pipeline->func_open_video_decoder(pipeline, ffp);

继续跟进,进入到具体实现:ffpipeline_ioc.c ---- >ffpipeline_create_from_ios

 看下 ffpipeline_ioc.c ---- >func_open_video_decoder的具体实现:

static IJKFF_Pipenode *func_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp)

    IJKFF_Pipenode* node = NULL;
    IJKFF_Pipeline_Opaque *opaque = pipeline->opaque;
    if (ffp->videotoolbox) 
        //如果配置了硬解 即 ffp->videotoolbox 是 1 就走这里
        node = ffpipenode_create_video_decoder_from_ios_videotoolbox(ffp);
        if (!node)
            ALOGE("vtb fail!!! switch to ffmpeg decode!!!! \\n");
    
    if (node == NULL) 
        //没配置硬解 或者不支持硬解 默认使用软解
        node = ffpipenode_create_video_decoder_from_ffplay(ffp);
        ffp->stat.vdec_type = FFP_PROPV_DECODER_AVCODEC;
        opaque->is_videotoolbox_open = false;
     else 
        //配置了硬解 并且支持硬解 就走硬解流程
        ffp->stat.vdec_type = FFP_PROPV_DECODER_VIDEOTOOLBOX;
        opaque->is_videotoolbox_open = true;
    
    ffp_notify_msg2(ffp, FFP_MSG_VIDEO_DECODER_OPEN, opaque->is_videotoolbox_open);
    return node;

由于我们配置了硬解,所以跟进到到 ffpipenode_ios_videotoolbox_vdec.m 文件的ffpipenode_create_video_decoder_from_ios_videotoolbox 方法来看下;

IJKFF_Pipenode *ffpipenode_create_video_decoder_from_ios_videotoolbox(FFPlayer *ffp)

   ...
    switch (opaque->avctx->codec_id) 
    case AV_CODEC_ID_H264:
    case AV_CODEC_ID_HEVC:
        if (ffp->vtb_async)
            opaque->context = Ijk_VideoToolbox_Async_Create(ffp, opaque->avctx);
         else 
            //这里走同步方式
            opaque->context = Ijk_VideoToolbox_Sync_Create(ffp, opaque->avctx);
        
        break;
        ...

跟进到:IJKVideoToolBox.m ----> Ijk_VideoToolbox_Sync_Create

 跟进到:IJKVideoToolBoxSync.m ----> videotoolbox_sync_create

Ijk_VideoToolBox_Opaque* videotoolbox_sync_create(FFPlayer* ffp, AVCodecContext* avctx)

    ...
    //问题出在这里面
    ret = vtbformat_init(&context_vtb->fmt_desc, context_vtb->codecpar);
    ...

跟进到:IJKVideoToolBoxSync.m ----> vtbformat_init

static int vtbformat_init(VTBFormatDesc *fmt_desc, AVCodecParameters *codecpar)

    ...
    switch (codec) 
        case AV_CODEC_ID_HEVC:
            format_id = kCMVideoCodecType_HEVC;
            if (@available(iOS 11.0, *)) 
                //iOS 11.0及以上才支持硬解 然后进一步进行判断
                isHevcSupported = VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC);
             else 
                // Fallback on earlier versions
                isHevcSupported = false;
            
            if (!isHevcSupported) 
                goto fail;
            
            break;
            
        case AV_CODEC_ID_H264:
            format_id = kCMVideoCodecType_H264;
            break;
            
        default:
            goto fail;
    
    ...

3、确定问题及解决

来看iPhone 7及以上并且iOS版本大于11.0的情况:

跟进到:IJKVideoToolBoxSync.m ----> vtbformat_init

static int vtbformat_init(VTBFormatDesc *fmt_desc, AVCodecParameters *codecpar)

...
    switch (codec) 
        case AV_CODEC_ID_HEVC:
            format_id = kCMVideoCodecType_HEVC;
            if (@available(iOS 11.0, *)) 
                isHevcSupported = VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC);
             else 
                // Fallback on earlier versions
                isHevcSupported = false;
            
            if (!isHevcSupported) 
                goto fail;
            
            break;

    
    
    ...
    //此处出现重大错误,
    //264的流调用ff_isom_write_avcc方法,h265(hevc)应该调用hevc.c里面的ff_isom_write_hvcc 才对        
    ff_isom_write_avcc(pb, extradata, extrasize);

    ...
    //h265(hevc)情况下不需要按264方式进行校验
    if (!validate_avcC_spc(extradata, extrasize, &fmt_desc->max_ref_frames,         &sps_level, &sps_profile)) 
            av_free(extradata);
            goto fail;
     



这里是真正出错的地方:

  • 错误1、ff_isom_write_avcc 方法是针对264流的,不能用于265流
  • 错误2、h265下不需要调用 validate_avcC_spc 方法进行校验

修改方式:

先说错误2、这个比较简单,不在直接执行校验,而是增加判断非hevc的情况下才执行,如下:

if (codec == AV_CODEC_ID_HEVC) 
    printf("---- zs log ----- vtbformat_init no check h265\\n");
 else 
    if (!validate_avcC_spc(extradata, extrasize,
                    &fmt_desc->max_ref_frames, &sps_level, &sps_profile)) 
         av_free(extradata);
         printf("---- zs log ----- vtbformat_init 11 \\n");
         goto fail;
    

针对错误1,请将 ff_isom_write_avcc(pb, extradata, extrasize); 这行代码 替换为:

//ff_isom_write_hvcc的作用是将extradata转为HEVCDecoderConfigurationRecord结构并写入。
if (codec == AV_CODEC_ID_HEVC) 
      printf("---- zs log ----- ff_isom_write_hvcc h265\\n");
      ff_isom_write_hvcc(pb, extradata, extrasize, 1); 
 else 
      ff_isom_write_avcc(pb, extradata, extrasize);

此时编译会报错,因为ff_isom_write_hvcc方法调用不到,没有暴露出来,因此需要先暴露出此方法,具体步骤如下:

第一步:打开ijkplayer-ios --->extra---->ffmpeg---->libavformat 下得Makefile文件

  在 HEADERS 这组里面加入:hevc.h \\ 

  在 OBJS 这组里面加入 hevc.o \\ 

 第二步:在以下文件中重复此操作

  • ijkplayer-ios/ios/ffmpeg-arm64/libavformat/Makefile
  • ijkplayer-ios/ios/ffmpeg-armv7/libavformat/Makefile
  • ijkplayer-ios/ios/ffmpeg-i386/libavformat/Makefile
  • ijkplayer-ios/ios/ffmpeg-x86_64/libavformat/Makefile

第三步:别忘了在IJKVideoToolBoxSync.m文件中头部加入

#include "libavformat/hevc.h"

第四步:clean 后重新编译即可

./compile-ffmpeg.sh clean
./compile-ffmpeg.sh all

以上就是iOS 下 ijkplayer 对 h265(hevc)4k 硬解的全部修改过程。

以上是关于iOS ijkplayer 硬解H265(hevc)4k视频问题解决的主要内容,如果未能解决你的问题,请参考以下文章

iOS ijkplayer 硬解H265(hevc)4k视频问题解决

iOS利用ffmpeg 转码hevc到h264 ,以及 保存h265 h264流

[超详细] 在Edge/Chrome浏览器上为B站开启HEVC硬解和AV1硬解(支持4K120Hz8KHDR真彩,杜比视界杜比全景声)

[树莓派]aarch64编译静态的ffmpeg 可硬解h264/hevc

如何推送和播放RTMP H265流 (RTMP HEVC)

MediaFoundation HEVC H265 编码