I created my own version of Android's MediaMetadataRetriever using the source code for MediaMetadataRetriever.java as the basis. My version uses FFmpeg to retrieve the metadata. This approach works however it relies on static variables in the C code to retain state in between JNI calls. This means I can only use one instance of this class at a time or the state can get corrupted. The two Java functions are defined as follows:
public class MediaMetadataRetriever
{
static {
System.loadLibrary("metadata_retriever_jni");
}
public MediaMetadataRetriever() {
}
public native void setDataSource(String path) throws IllegalArgumentException;
public native String extractMetadata(String key);
}
The corresponding C (JNI) code code is:
const char *TAG = "Java_com_example_metadataexample_MediaMetadataRetriever";
static AVFormatContext *pFormatCtx = NULL;
JNIEXPORT void JNICALL
Java_com_example_metadataexample_MediaMetadataRetriever_setDataSource(JNIEnv *env, jclass obj, jstring jpath) {
if (pFormatCtx) {
avformat_close_input(&pFormatCtx);
}
char duration[30] = "0";
const char *uri;
uri = (*env)->GetStringUTFChars(env, jpath, NULL);
if (avformat_open_input(&pFormatCtx, uri, NULL, NULL) != 0) {
__android_log_write(ANDROID_LOG_INFO, TAG, "Metadata could not be retrieved");
(*env)->ReleaseStringUTFChars(env, jpath, uri);
jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
return;
}
if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
__android_log_write(ANDROID_LOG_INFO, TAG, "Metadata could not be retrieved");
avformat_close_input(&pFormatCtx);
(*env)->ReleaseStringUTFChars(env, jpath, uri);
jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
return;
}
(*env)->ReleaseStringUTFChars(env, jpath, uri);
}
JNIEXPORT jstring JNICALL
Java_com_example_metadataexample_MediaMetadataRetriever_extractMetadata(JNIEnv *env, jclass obj, jstring jkey) {
const char *key;
jstring value = NULL;
key = (*env)->GetStringUTFChars(env, jkey, NULL) ;
if (!pFormatCtx) {
goto fail;
}
if (key) {
if (av_dict_get(pFormatCtx->metadata, key, NULL, AV_DICT_IGNORE_SUFFIX)) {
value = (*env)->NewStringUTF(env, av_dict_get(pFormatCtx->metadata, key, NULL, AV_DICT_IGNORE_SUFFIX)->value);
}
}
fail:
(*env)->ReleaseStringUTFChars(env, jkey, key);
return value;
}
Sample usage that outlines my issue would be:
MediaMetadataRetriever mmr = new MediaMetadataRetriever();
mmr.setDataSource("one.mp3");
MediaMetadataRetriever mmr2 = new MediaMetadataRetriever();
// This line resets the data source to two.mp3
mmr2.setDataSource("two.mp3");
// should retrieve the artist from one.mp3 but retrieves it from two.mp3 due to the static
// variable being reset in the previous statement
String artist = mmr.extractMetadata(MediaMetadataRetriever.ARTIST);
Can someone explain how I would structure this code so I could use multiple instances of MediaMetadataRetriever without them interfering with one another? I don't want to switch the code to C++ and I'm fairly certain I don't need to modify MediaMetadataRetriever.java since this code is taken line-for-line from the Android framework (which allows multiple instances, see example below). It appears I need to re-structure the C code but I'm unsure how to retain state across JNI calls without using a static variable. Thanks in advance.
File file1 = new File(Environment.getExternalStorageDirectory(), "Music/one.mp3");
File file2 = new File(Environment.getExternalStorageDirectory(), "Music/two.mp3");
android.media.MediaMetadataRetriever mmr = new android.media.MediaMetadataRetriever();
mmr.setDataSource(file1.toString());
android.media.MediaMetadataRetriever mmr2 = new android.media.MediaMetadataRetriever();
mmr2.setDataSource(file2.toString());
// Returns the artist of one.mp3, not two.mp3, as expected. This is the expected behavior
// and confirms that multiple instances of MediaMetadataRetriever can be used simultaneously
mmr.extractMetadata(android.media.MediaMetadataRetriever.METADATA_KEY_ARTIST));
Add a long field (or int if you know you'll only ever be on a 32-bit system) to store the native pointer, and to dispose of it in your object's finalizer when the object is GC'd. From JNI, you can then save to that field the pointer obtained when you "open" your resource.
From the JNI manual:
Restructuring the code would be very straight forward. Instead of using
pFormatCtx
you simply extend the interfaces of the JNI calls so you can pass around the pointer you stored inpFormatCtx
before. Now the big guestion is how to pass around the pointers while java doesn't know such a datatype? The most straight forward soulution would be to use ints (for 32 bit systems) or longs (for 64 bit systems) to passing pointers to and from the Java environment. Unfortunately you could get you a bit in hot water as soon as you switch between 64 and 32 bit versions of your library.While I was trying to solve this problem some months ago I stumbled over an article of Clebert Suconic. He pointed out a very elegant way for passing pointers safely through JNI without "hacking" around with typecasting. Instead he proposes to use
java.nio.ByteBuffer
.The concept put in a nutshell is: He suggest to create a new ByteBuffer object of length zero:
env->NewDirectByteBuffer(myPointer, 0);
and pass the resulting jobject through the JNI back and forth.The call
env->NewDirectByteBuffer(myPointer, 0);
creates a imutable byte buffer object pointing to the location you wanted to pass around. The fact that the buffer is imutable is perfect as you don't want to modify the memory location, you only want to store the location itself. What you get is an object encapsulating your pointer and you can leave the pointer size issues to the JVM.Edit: Just for completeness: The pointer can later be retrieved calling
env->GetDirectBufferAddress(myPointer);
.