ijkPlayer扩展native MediaCodec API实现硬解码

1,026 阅读4分钟

ijkPlayer扩展native MediaCodec实现硬解码

Android中native MediaCodec在API 21的时候就已经出来了,但是一直没有用过,之前也一直有在ijkPlayer上用native API替代jni调用java MediaCodec的想法,只不过一直没有付诸行动。最近在ijkPlayer的基础上进行了一次尝试,为ijkPlayer实现了native MediaCodec硬编码扩展。

MediaCodec在java层和native层在API层面是大同小异的,因此我们可以直接将ijkPlayer调用的java层API直接换成native API即可。

SDL_AMediaCodec

ijk中是通过调用SDL_AMediaCodec结构体中定义的函数指针来实现java API调用的,它将解码过程中调用的java层API全部抽取申明成函数指针,我们只需要去用对应的native API实现函数定义就可以完成替代。其中SDL_AMediaCodec_Opaque是内部实现所持有的数据结构,由具体实现自己定义自己需要的数据。

typedef struct SDL_AMediaCodec
{
    SDL_mutex    *mutex;
    volatile int  ref_count;

    SDL_Class              *opaque_class;
    SDL_AMediaCodec_Common *common;
    SDL_AMediaCodec_Opaque *opaque;// 内部实现数据结构,需要自己定义
    bool                    is_configured;
    bool                    is_started;
    int                     object_serial;

    sdl_amedia_status_t (*func_delete)(SDL_AMediaCodec *acodec);

    sdl_amedia_status_t (*func_configure)(
        SDL_AMediaCodec* acodec,
        const SDL_AMediaFormat* aformat,
        ANativeWindow* surface,
        SDL_AMediaCrypto *crypto,
        uint32_t flags);
    sdl_amedia_status_t (*func_configure_surface)(
        JNIEnv*env,
        SDL_AMediaCodec* acodec,
        const SDL_AMediaFormat* aformat,
        jobject android_surface,
        SDL_AMediaCrypto *crypto,
        uint32_t flags);

    sdl_amedia_status_t     (*func_start)(SDL_AMediaCodec* acodec);
    sdl_amedia_status_t     (*func_stop)(SDL_AMediaCodec* acodec);
    sdl_amedia_status_t     (*func_flush)(SDL_AMediaCodec* acodec);

    ssize_t                 (*func_writeInputData)(SDL_AMediaCodec* acodec, size_t idx, const uint8_t *data, size_t size);

    ssize_t                 (*func_dequeueInputBuffer)(SDL_AMediaCodec* acodec, int64_t timeoutUs);
    sdl_amedia_status_t     (*func_queueInputBuffer)(SDL_AMediaCodec* acodec, size_t idx, off_t offset, size_t size, uint64_t time, uint32_t flags);

    ssize_t                 (*func_dequeueOutputBuffer)(SDL_AMediaCodec* acodec, SDL_AMediaCodecBufferInfo *info, int64_t timeoutUs);
    SDL_AMediaFormat*       (*func_getOutputFormat)(SDL_AMediaCodec* acodec);
    sdl_amedia_status_t     (*func_releaseOutputBuffer)(SDL_AMediaCodec* acodec, size_t idx, bool render);

    bool                    (*func_isInputBuffersValid)(SDL_AMediaCodec* acodec);
} SDL_AMediaCodec;

下面是java API的实现,可以看到SDL_AMediaCodec_Opaque中存了java层的MediaCodec对象,在需要使用的时候再拿出来进行相关操作即可,如SDL_AMediaCodecJava_start函数里,核心逻辑就是拿出Opaque中的java对象,去执行start。

typedef struct SDL_AMediaCodec_Opaque {
    jobject android_media_codec;

    jobject         output_buffer_info;

    bool            is_input_buffer_valid;
} SDL_AMediaCodec_Opaque;

static sdl_amedia_status_t SDL_AMediaCodecJava_start(SDL_AMediaCodec* acodec)
{
    SDLTRACE("%s", __func__);

    JNIEnv *env = NULL;
    if (JNI_OK != SDL_JNI_SetupThreadEnv(&env)) {
        ALOGE("%s: SetupThreadEnv failed", __func__);
        return SDL_AMEDIA_ERROR_UNKNOWN;
    }

    SDL_AMediaCodec_Opaque *opaque = (SDL_AMediaCodec_Opaque *)acodec->opaque;
    jobject android_media_codec    = opaque->android_media_codec;
    J4AC_MediaCodec__start(env, android_media_codec);
    if (J4A_ExceptionCheck__catchAll(env)) {
        ALOGE("%s: start failed", __func__);
        return SDL_AMEDIA_ERROR_UNKNOWN;
    }

    return SDL_AMEDIA_OK;
}


static SDL_AMediaCodec* SDL_AMediaCodecJava_init(JNIEnv *env, jobject android_media_codec)
{
    SDLTRACE("%s", __func__);

    jobject global_android_media_codec = (*env)->NewGlobalRef(env, android_media_codec);
    if (J4A_ExceptionCheck__catchAll(env) || !global_android_media_codec) {
        return NULL;
    }

    SDL_AMediaCodec *acodec = SDL_AMediaCodec_CreateInternal(sizeof(SDL_AMediaCodec_Opaque));
    if (!acodec) {
        SDL_JNI_DeleteGlobalRefP(env, &global_android_media_codec);
        return NULL;
    }

    SDL_AMediaCodec_Opaque *opaque = acodec->opaque;
    opaque->android_media_codec         = global_android_media_codec;

    acodec->opaque_class                = &g_amediacodec_class;
    acodec->func_delete                 = SDL_AMediaCodecJava_delete;
    acodec->func_configure              = NULL;
    acodec->func_configure_surface      = SDL_AMediaCodecJava_configure_surface;

    acodec->func_start                  = SDL_AMediaCodecJava_start;
    acodec->func_stop                   = SDL_AMediaCodecJava_stop;
    acodec->func_flush                  = SDL_AMediaCodecJava_flush;

    acodec->func_writeInputData         = SDL_AMediaCodecJava_writeInputData;

    acodec->func_dequeueInputBuffer     = SDL_AMediaCodecJava_dequeueInputBuffer;
    acodec->func_queueInputBuffer       = SDL_AMediaCodecJava_queueInputBuffer;

    acodec->func_dequeueOutputBuffer    = SDL_AMediaCodecJava_dequeueOutputBuffer;
    acodec->func_getOutputFormat        = SDL_AMediaCodecJava_getOutputFormat;
    acodec->func_releaseOutputBuffer    = SDL_AMediaCodecJava_releaseOutputBuffer;

    acodec->func_isInputBuffersValid    = SDL_AMediaCodecJava_isInputBuffersValid;

    SDL_AMediaCodec_increaseReference(acodec);
    return acodec;
}

SDL_AMediaCodec* SDL_AMediaCodecJava_createByCodecName(JNIEnv *env, const char *codec_name)
{
    SDLTRACE("%s", __func__);
    //构建java层MediaCodec
    jobject android_media_codec = J4AC_MediaCodec__createByCodecName__withCString__catchAll(env, codec_name);
    if (J4A_ExceptionCheck__catchAll(env) || !android_media_codec) {
        return NULL;
    }

    //初始化MediaCodec native层包装数据结构与接口
    SDL_AMediaCodec* acodec = SDL_AMediaCodecJava_init(env, android_media_codec);
    acodec->object_serial = SDL_AMediaCodec_create_object_serial();
    SDL_JNI_DeleteLocalRefP(env, &android_media_codec);
    return acodec;
}

因此只需要依葫芦画瓢在我们自己的Opaque中初始化并保存AMediaCodec指针,再在需要使用时取出进行操作即可。下面是相关实现的代码。

typedef struct SDL_AMediaCodec_Opaque {
    AMediaCodec *codec;
    bool            is_input_buffer_valid;
} SDL_AMediaCodec_Opaque;

static sdl_amedia_status_t SDL_AMediaCodecNative_start(SDL_AMediaCodec* acodec)
{
    SDLTRACE("%s", __func__);

    SDL_AMediaCodec_Opaque *opaque = (SDL_AMediaCodec_Opaque *) acodec->opaque;
    AMediaCodec_start(opaque->codec);
    return SDL_AMEDIA_OK;
}

static SDL_AMediaCodec* SDL_AMediaCodec_native_init(AMediaCodec *codec){
    SDL_AMediaCodec *acodec = SDL_AMediaCodec_CreateInternal(sizeof(SDL_AMediaCodec_Opaque));
    SDL_AMediaCodec_Opaque *opaque = acodec->opaque;
    opaque->codec = codec;
    acodec->opaque_class = g_amediacodec_class;
    acodec->func_delete = SDL_AMediaCodecNative_delete;
    acodec->func_configure              = NULL;
    acodec->func_configure_surface = SDL_AMediaCodecNative_configure_surface;
    acodec->func_start = SDL_AMediaCodecNative_start;
    acodec->func_stop = SDL_AMediaCodecNative_stop;
    acodec->func_flush = SDL_AMediaCodecNative_flush;
    acodec->func_writeInputData = SDL_AMediaCodecNative_writeInputData;
    acodec->func_dequeueInputBuffer = SDL_AMediaCodecNative_dequeueInputBuffer;
    acodec->func_queueInputBuffer = SDL_AMediaCodecNative_queueInputBuffer;
    acodec->func_dequeueOutputBuffer = SDL_AMediaCodecNative_dequeueOutputBuffer;
    acodec->func_getOutputFormat = SDL_AMediaCodecNative_getOutputFormat;
    acodec->func_releaseOutputBuffer = SDL_AMediaCodecNative_releaseOutputBuffer;
    acodec->func_isInputBuffersValid    = SDL_AMediaCodecNative_isInputBuffersValid;
    SDL_AMediaCodec_increaseReference(acodec);
    return acodec;
}

SDL_AMediaCodec* SDL_AMediaCodec_native_create(const char *codec_name){
    AMediaCodec *codec = AMediaCodec_createCodecByName(codec_name);
    if (codec == NULL) {
        return NULL;
    }
    return SDL_AMediaCodec_native_init(codec);
}

选择解码策略时判断api版本,大于等于21选择native的api

static SDL_AMediaCodec *create_codec_l(JNIEnv *env, IJKFF_Pipenode *node)
{
    IJKFF_Pipenode_Opaque        *opaque   = node->opaque;
    ijkmp_mediacodecinfo_context *mcc      = &opaque->mcc;
    SDL_AMediaCodec              *acodec   = NULL;

    if (opaque->jsurface == NULL) {
        // we don't need real codec if we don't have a surface
        acodec = SDL_AMediaCodecDummy_create();
    } else {
        //构建MediaCodec
        if (opaque->ffp->sdkVersion >= 21) {
            acodec = SDL_AMediaCodec_native_create(mcc->codec_name);
        } else {
            acodec = SDL_AMediaCodecJava_createByCodecName(env, mcc->codec_name);
        }

        if (acodec) {
            strncpy(opaque->acodec_name, mcc->codec_name, sizeof(opaque->acodec_name) / sizeof(*opaque->acodec_name));
            opaque->acodec_name[sizeof(opaque->acodec_name) / sizeof(*opaque->acodec_name) - 1] = 0;
        }
    }
}

SDL_AMediaFormat

需要注意的是在java API中会使用到MediaFormat API,在native也需要进行替换,通过查看java API,是直接new一个对象,然后配置参数。ijk里面通过SDL_AMediaFormat封装了MediaFormat java API的函数指针调用,和上面的SDL_AMediaCodec一样,将ijk调用的java API对应实现成native API即可。

public static final MediaFormat createVideoFormat(
        String mime,
        int width,
        int height) {
    MediaFormat format = new MediaFormat();
    format.setString(KEY_MIME, mime);
    format.setInteger(KEY_WIDTH, width);
    format.setInteger(KEY_HEIGHT, height);

    return format;
}

native层参照java传参,保持一致。

SDL_AMediaFormat *SDL_AMediaFormatNative_createVideoFormat(const char *mime, int width, int height){
    SDLTRACE("%s", __func__);
    SDL_AMediaFormat *aformat = SDL_AMediaFormat_CreateInternal(sizeof(SDL_AMediaFormat_Opaque));
    AMediaFormat *format = AMediaFormat_new();
    AMediaFormat_setString(format, "mime", mime);
    AMediaFormat_setInt32(format, "width", width);
    AMediaFormat_setInt32(format, "height", height);
    setup_aformat(aformat, format);
    SDL_AMediaFormat_setInt32(aformat, AMEDIAFORMAT_KEY_MAX_INPUT_SIZE, 0);
    return aformat;
}

配置AMediaCodec,需要传入ANativeWindow,Android原生提供了API ANativeWindow_fromSurface ,可以实现从java层Surface对象获取ANativeWindow。

static sdl_amedia_status_t SDL_AMediaCodecNative_configure_surface(
        JNIEnv* env,
        SDL_AMediaCodec* acodec,
        const SDL_AMediaFormat* aformat,
        jobject android_surface,
        SDL_AMediaCrypto *crypto,
        uint32_t flags) {
    SDLTRACE("%s", __func__);
    SDL_AMediaCodec_Opaque *opaque = (SDL_AMediaCodec_Opaque *)acodec->opaque;
    AMediaCodec *codec = opaque->codec;
    if (codec) {
        ANativeWindow *window = ANativeWindow_fromSurface(env, android_surface);
        AMediaCodec_configure(codec, SDL_AMediaFormatNative_get(aformat), window, NULL, flags);
    } else {
        return SDL_AMEDIA_ERROR_UNKNOWN;
    }
    opaque->is_input_buffer_valid = true;
    return SDL_AMEDIA_OK;
}

需要注意的地方

1.编译时minSdkVersion需要设置大于21。

2.ijk自己定义了一些宏与native MediaCodec中定义的宏冲突,可以考虑删除或加上ANDROID_API判断。

结论

上面记录了实现扩展的大致过程,ijk中的硬解码过程和其它调用MediaCodec播放视频过程是差不多的,感兴趣可以自行搜索相关的文章,我们并不关心解码过程,直接按照ijk原有的接口(函数指针)就实现了替换,这也充分说明ijk在这块的扩展性。

通过看代码,调用java API时都会走jni,去做attach与detach操作去绑定/解绑JVM环境,再通过java API调用到native层,并且这些操作还是高频的,本以为使用native API会在一定程度上降低CPU使用率,测试播放本地1920*1080分辨率视频,两者CPU使用率在小米10pro和一款低端的4核设备上相差无几。下面是在4核设备上的CPU使用率截图。

image-20230411182104219.png

java API CPU使用情况

image-20230411181913698.png

native API CPU使用情况