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使用率截图。
java API CPU使用情况
native API CPU使用情况