百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

「音视频直播技术」看ijkplayer如何使用JNI

cac55 2024-10-09 08:18 27 浏览 0 评论

前言

ijklayer可以说是目前最火的一款移动端播放器了。它同时支持Android和iOS,它之所以如此流行,主要是代码写的太美了,我认为把它当作艺术品也不过分。

ijkplayer为了提高性能做了大量的优化,其中一个关键点是使用了JNI。播边器里最关键的部分全部由C来实现。

今天我们就来看看 jikplayer 是如何使用JNI的。

导入动态库

ijkplayer 创建 IJKMediaPlayer 对象时,在其构造函数里会调到 loadLibrariesOnce 方法。代码如下:

public static void loadLibrariesOnce(IjkLibLoader libLoader) {
......
libLoader.loadLibrary("ijkffmpeg"); //加载 ffmpeg 用于解码
libLoader.loadLibrary("ijksdl"); // 加载 sdk 用于渲染
libLoader.loadLibrary("ijkplayer"); // 加载 ijkplayer 核心播放器
......
}

其中,loadLibrariesOnce方法中的libLoader是ijkplayer定义的IjkLibLoader类对象。该对象的 loadLibrary 方法最终会调用 System.loadLibrary 函数完成共享库的加载。

经过上面操作后 ijkffmpeg、ijksdl及ijkplayer就被加载到JavaVM里了。

在Android系统下,每一个进程只能有一个JavaVM。

我们第一步看下在ijkplayer里,如何通过 Java代码调到 C/C++接口。

在Java层定义本地方法

想通过 Java 调用 C/C++ 接口,首先需要让 Java 程序知道都有哪些 C/C++ 接口可用。这有点像C/C++中常说的符号表(名子与地址的对应关系表)。如何能做到这点呢?方法很简单,就是在 Java 类方法的前边加上 "native" 关键字。我们看一下 IJKPlayer 都提供了哪些本地方法吧:

......
private native void _setDataSource(IMediaDataSource mediaDataSource);
public native void _prepareAsync() throws IllegalStateException;
private native void _start() throws IllegalStateException;
private native void _stop() throws IllegalStateException;
private native void _pause() throws IllegalStateException;
public native void seekTo(long msec) throws IllegalStateException;
public native long getDuration();
private native void _release();
public native void setVolume(float leftVolume, float rightVolume);
private static native void native_init();
......

这一步是不是非常简单?

当然,只做到这一步还无法调用 C/C++接口,因为你还没告诉JavaVM你的C/C++接口在哪儿呢。

注册C/C++方法

仅在Java层定义本地方法只完成了工作的一半。当Java代码真正调用 “native” 方法时,JavaVM虚拟机会在符号表中查找有没有 Java 程序想调用的函数。如果此时没有的话,JavaVM 就会报错。所以现在我们要将 C/C++ 提供的接口注册到 JavaVM中。

首先,建好函数对应表。此表中的每一项都包括三个元素,分别是 外部调用的接口名、signature、内部真正的实现函数

signature 后面有专门的讲解。

代码如下:

static JNINativeMethod g_methods[] = {
......
{ "_setDataSource", "(Ltv/danmaku/ijk/media/player/misc/IMediaDataSource;)V", (void *)IjkMediaPlayer_setDataSourceCallback},
{ "_prepareAsync", "()V", (void *) IjkMediaPlayer_prepareAsync },
{ "_start", "()V", (void *) IjkMediaPlayer_start },
{ "_stop", "()V", (void *) IjkMediaPlayer_stop },
{ "seekTo", "(J)V", (void *) IjkMediaPlayer_seekTo },
{ "_pause", "()V", (void *) IjkMediaPlayer_pause },{ "getDuration", "()J", (void *) IjkMediaPlayer_getDuration },
{ "_release", "()V", (void *) IjkMediaPlayer_release },
{ "setVolume", "(FF)V", (void *) IjkMediaPlayer_setVolume },
{ "native_init", "()V", (void *) IjkMediaPlayer_native_init },
......
};

看看这里的外部调用函数名是不是与上面在 Java 层定义的方法名是一样的呢?只有一样它们之前才能建立起对应关系来。

然后,将上面表中的方法注册到JavaVM中。代码如下:

......

//注册native方法,并与 IjkMediaPlayer 关联起来。
//g_clazz.clazz 存放的是 IjkMediaPlayer 类的 jclass 对象
//g_methods 就是上面定义的函数表//NELEM(g_methods) 计算函数表中一共有几项
(*env)->RegisterNatives(env, g_clazz.clazz, g_methods, NELEM(g_methods) );

......

ijkplayer通过FindClass找到IjkMediaPlayer类的 jclass 对象,再通过RegisterNatives函数将C/C++接口注册到JavaVM中,并与IjkMediaPlayer类绑定在一起。

在哪儿注册最好

上面我们知道了如何注册C/C++方法,那么在什么地方注册好呢?答案是在 JNI_OnLoad 函数中。因为在加载动态链接库时,JavaVM会主动调用JNI_OnLoad(JavaVM * jvm, void * reserved)。看一下ijkplayer的实现:

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
JNIEnv* env = NULL;
g_jvm = vm;
if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}
......
// FindClass returns GlobleReference
IJK_FIND_JAVA_CLASS(env, g_clazz.clazz, JNI_CLASS_IJKPLAYER);
(*env)->RegisterNatives(env, g_clazz.clazz, g_methods, NELEM(g_methods) );
......
return JNI_VERSION_1_4;
}
JNIEXPORT void JNI_OnUnload(JavaVM *jvm, void *reserved)
{
......
}

在 JNI_OnLoad 函数中首先获取 JNIEnv,之后找到 IjkMediaPlayer 类,最后注册C/C++方法,并将注册的方法与IjkMediaPlayer类关联起来。

当然,有了 JNI_OnLoad 还要有 JNI_OnUnload 函数。它在共享库被卸载时调用,可以在这里释放一些资源。

通过上面的操作我们就可以从 Java 调用 C++的代码了。有没有赶快去试试的冲动?先别急,现在只介绍了如何从 Java 调用 C/C++的方法。那么反回来如何从 C/C++ 调 Java 代码呢?

C/C++调用Java方法

ijkplayer 会使用C调用android下的 MediaCodec 类中的方法。首先,通过 FindClass 获取MediaCodec的 jclass 对象。然后设置该对象为全局引用,并将它的本地引用删除。

这些方法的调用都要做异常判断,如果出现异常所有的结果都是无效的值。

我们就以这个为例子,看一下它是如何从C调用的java方法吧。

......
//对异常的处理
bool J4A_ExceptionCheck__catchAll(JNIEnv *env)
{
if ((*env)->ExceptionCheck(env)) {
(*env)->ExceptionDescribe(env);
(*env)->ExceptionClear(env);
return true;
}
return false;
}
//通过 FindClass 得到jclass对象
jclass J4A_FindClass__catchAll(JNIEnv *env, const char *class_sign)
{
jclass clazz = (*env)->FindClass(env, class_sign);
......
}
//拿到 jclass 对象,并设置其为全局引用
jclass J4A_FindClass__asGlobalRef__catchAll(JNIEnv *env, const char *class_sign)
{
jclass clazz_global = NULL;
jclass clazz = J4A_FindClass__catchAll(env, class_sign);
......
//设置为全局引用
clazz_global = J4A_NewGlobalRef__catchAll(env, clazz);
......
fail:
J4A_DeleteLocalRef__p(env, &clazz);
return clazz_global;
}
......
//设置要获取的类名
sign = "android/media/MediaCodec";
class_J4AC_android_media_MediaCodec.id =
J4A_FindClass__asGlobalRef__catchAll(env, sign);
......

获得了 jclass 后,就可以通过 Get<Type>MethodID 获取类方法的jmethodID对象。

......
jmethodID J4A_GetStaticMethodID__catchAll(JNIEnv *env, jclass clazz, const char *method_name, const char *method_sign)
{
jmethodID method_id = (*env)->GetStaticMethodID(env, clazz, method_name, method_sign);
......
fail:
return method_id;
}
......
class_id = class_J4AC_android_media_MediaCodec.id; //jclass
name = "createByCodecName"; //方法名
sign = "(Ljava/lang/String;)Landroid/media/MediaCodec;"; //signature
class_J4AC_android_media_MediaCodec.method_createByCodecName =
J4A_GetStaticMethodID__catchAll(env, class_id, name, sign);
......

最后,通过 Call<Type>Method 调用 java 方法。

......
jobject J4AC_android_media_MediaCodec__createByCodecName(JNIEnv *env, jstring name)
{
return (*env)->CallStaticObjectMethod(env,
class_J4AC_android_media_MediaCodec.id,
class_J4AC_android_media_MediaCodec.method_createByCodecName,
name);
}
......

现在 C/C++ 也可以调用 Java 方法了。

最后,我们再来看一下C/C++如何访问 java 的字段吧,这个就更简单了。

C/C++访问Java字段

有了 C/C++访问Java的基础,再看访问Java字段就容易多了。它也是先获取 jclass, 之后通过 jclass 得到 jfieldID,最终 Get/Set java 字段。jclass的获取我们就不讲了,重点说说获取 jfieldID 和 Get/Set。

......
jfieldID J4A_GetFieldID__catchAll(JNIEnv *env, jclass clazz, const char *field_name, const char *field_sign)
{
//获得 jfieldID
jfieldID field_id = (*env)->GetFieldID(env, clazz, field_name, field_sign);
//异常判断
if (J4A_ExceptionCheck__catchAll(env) || !field_id) {
......
}
fail:
return field_id;
}
......
class_id = class_J4AC_android_media_MediaCodec__BufferInfo.id; //jclass
name = "flags"; // java 字段名
sign = "I"; // signature
class_J4AC_android_media_MediaCodec__BufferInfo.field_flags =
J4A_GetFieldID__catchAll(env, class_id, name, sign);
......

上面的代码通过GetFieldID方法就得到了我们想要的 jfieldID。下一步看看如何进行 Get/Set。

......
jint J4AC_android_media_MediaCodec__BufferInfo__flags__get(JNIEnv *env, jobject thiz)
{
return (*env)->GetIntField(env, thiz, class_J4AC_android_media_MediaCodec__BufferInfo.field_flags);
}
......
void J4AC_android_media_MediaCodec__BufferInfo__flags__set(JNIEnv *env, jobject thiz, jint value)
{
(*env)->SetIntField(env, thiz, class_J4AC_android_media_MediaCodec__BufferInfo.field_flags, value);
}
......

非常简单,JNI调用 Get<Type>Field或Set<Type>Field方法获取或设置Java的字段。

至此我们就分析完了 ijkplayer 对 JNI的使用。后面附上 Signature 的说明。

Signature

在JNI中Signature主要用于操作Java类中的方法。Signature一般由两部分组成:方法参数;方法返回值。

  • 方法参数包含在“()”中,返回值在括号外。

  • 方法参数个数较多时会依次以“;”隔开。

  • 当参数或者返回值是基本数据类型时,必须用其在JNI中的描述符表示。下表就是Java基本数据类型对应的JNI中的描述符。

Java类型符号
booleanZ
byteB
charC
shortS
intI
longL
floatF
doublD
voidV
  • 方法参数或者返回值为java中的对象时,必须以“L”加上其路径,不过路径必须以“/”分开,自定义的对象也使用本规则,不在包中时直接“L”加上类名称。

  • 当参数或者返回值为数组时,前面必须加上“[”。

以上就是Signature表示方法的规则!

看看下面一些Signature,你能一个个转换为相应的方法吗?

  • ([LStudent;)[LStudent;

  • ([I[Ljava/lang/String;[LStudent;)Ljava/lang/Object;

  • ([LStudent;[LStudent;)[LStudent;

  • ([Ljava/util/Iterator;)[Ljava/util/Enumeration;

  • ([Ljava/lang/Object;)[Ljava/lang/Object;

  • ([Ljava/lang/String;)[Ljava/lang/String;

  • (LStudent;)LStudent;

小结

本篇文章介绍了ijkplayer是如何使用JNI的,主要包以下几点内容:

  1. Java 如何调用 C/C++ 接口。

  2. C/C++ 如何调用 Java 方法。

  3. C/C++ 如何设置/获取 Java 字段的值。

希望本文能对您有所帮助,并请多多观注。谢谢!

参考

JNI编程常见问题

相关推荐

QQ表情大图(QQ表情大图怎么发)

爷青回 | QQ经典老头像(爷青回这个梗出自哪里)

点个关注不迷路记得点击上方关注我呦点击表情包长按可保存至手机表情包素材来源于网络,仅供分享哦拿完图记得吱一声点击下方分享、在看让更多人看到...

史上最全QQ官方经典头像全面翻新,不光高清还会动

每当看到上面这些头像,总能想起那些年的"轻舞飞扬","缘分天空","追风少年",这些已经模糊的头像给我们留下了太深的印象。这次为了纪念QQ20周年,腾讯官方整合了早期的105个经典头像,进行了全面翻...

QQ最全表情含义图解意思(qq表情含义图解最新 新版 文字)

QQ都不陌生吧!对QQ的表情符号含义你了解多少呢?在本文中最全图解233个表情所表达的含义,供有需人享用。用过QQ的人都晓得它的创始人是马化腾。QQ于1999年2月10日正式推出。QQ是腾讯公司开发的...

海联真人版QQ经典表情(海联真人版qq经典表情在哪)

海联版傲娇的说声“耶”狂拽炫酷就是我淑女应该轻言细语萌萌哒的娇羞哎哟喂小丫头片子机智如我吓死宝宝了欧巴卡几嘛~今天天气好晴朗怎么样?是不是很有趣呢拿起手机给自己拍几张萌萌哒的美照吧...

QQ音乐·音乐灵感独家对话金曲奖「最佳单曲制作人奖」得主JADE

JADE-AllRightJADE-差-点JADE-Goodbye,GoodbyeJADE-IAmLovefeat.乔瑟夫Chillseph下面请听本期灵感电台节目:本期博客...

亿万富豪爱泼斯坦狱中“自杀”,他背后的神秘女人出现在洛杉矶快餐店

爱泼斯坦在狱中离奇“自杀”,但他身负同谋指控的前女友、英国社交名媛希莱恩·麦克斯维尔(GhislaineMaxwell),却意外地出现在了洛杉矶街头平民快餐店,边啃着汉堡,咽着薯条,嘬着奶昔,边埋头...

扛起星战大旗的你们 觉得星战女需要换一身衣裳吗?

马上进入2016年,除了各种总结盘点以外,2016年的新看点也是需要科普一下了。目前最令人期待的应该就是《星球大战》回归了!《StarWars:原力觉醒》1月10日上映,博主不是电影评论员,所以不会...

和人对话的时候,我,最怕的就是,看到了自己内心的惶恐和脆弱

IWannaBeYourSlave(LiveFromGlobalCitizenLive2021),Maneskin很多时候,哪怕最甘于寂寞的人,也需要和人发生关联,需要和这个世界沟...

2024年度串烧完整版(搞笑失败尴尬丢人版)来了

一首APT的时间带你回顾你的2024年年度歌单。·1.《免我蹉跎苦》黄龄。·2.《红昭愿》音阙诗听。·3.《苹果香》狼戈。·4.《免我蹉跎苦》黄龄。·5.《红昭愿》音阙诗听。·6.《苹果香》狼戈。·7...

一课译词:打工人(打工人的翻译)

下午好,各位打工人!近日,“打工人”爆红网络,受到各行各业年轻人的追捧,但这词到底说的是个啥?“打工人”是那些依靠体力或技术的劳动者的统称。除了赚钱这个最大的目标,别的啥也不想;他们意志坚定,也不会迟...

初级词汇题(一)柏拉图指出不是每个孩子都适合上学,你赞成吗?

初级词汇题(一)柏拉图在《理想国》中指出不是每个孩子都适合上学,你赞成吗?今天分享的题目是我基于英文原著改编的初级词汇题A开头的第81道题。背景知识拓展:什么是nativist(先天论者)?什么是哲学...

治愈系英文:每个说不想恋爱的人,心里都装着一个无法拥有的人

Therearesomanypeopleouttherewhowilltellyouthatyoucan't.Whatyou'vegottodoisturna...

首首经典!意大利流行乐队Maneskin作品I WANNA BE YOUR SLAVE

手机点击试听(上边)Maneskin是一支来自罗马的意大利流行摇滚乐队,由主唱DamianoDavid、贝斯手VictoriaDeAngelis、吉他手ThomasRaggi和鼓手...

国家电网新一代电子商务平台投标文件双层PDF制作最全教程

投标知识在招投标过程中,我们经常碰见有些文件要求制成双层PDF格式,那么双层PDF是什么呢?怎么制作呢?今天就给大家普及下。定义双层PDF双层PDF格式文件是一种具有多层结构的PDF格式文件,是PD...

取消回复欢迎 发表评论: