11

I'm trying to retrieve metadata in Android using FFmpeg, JNI and a Java FileDescriptor and it isn't' working. I know FFmpeg supports the pipe protocol so I'm trying to emmulate: "cat test.mp3 | ffmpeg i pipe:0" programmatically. I use the following code to get a FileDescriptor from an asset bundled with the Android application:

FileDescriptor fd = getContext().getAssets().openFd("test.mp3").getFileDescriptor();
setDataSource(fd, 0, 0x7ffffffffffffffL); // native function, shown below

Then, in my native (In C++) code I get the FileDescriptor by calling:

static void wseemann_media_FFmpegMediaMetadataRetriever_setDataSource(JNIEnv *env, jobject thiz, jobject fileDescriptor, jlong offset, jlong length)
{
    //...

    int fd = jniGetFDFromFileDescriptor(env, fileDescriptor); // function contents show below

    //...
}

// function contents
static int jniGetFDFromFileDescriptor(JNIEnv * env, jobject fileDescriptor) {
    jint fd = -1;
    jclass fdClass = env->FindClass("java/io/FileDescriptor");

    if (fdClass != NULL) {
        jfieldID fdClassDescriptorFieldID = env->GetFieldID(fdClass, "descriptor", "I");
        if (fdClassDescriptorFieldID != NULL && fileDescriptor != NULL) {
            fd = env->GetIntField(fileDescriptor, fdClassDescriptorFieldID);
        }
    }

    return fd;
}

I then pass the file descriptor pipe # (In C) to FFmpeg:

char path[256] = "";

FILE *file = fdopen(fd, "rb");

if (file && (fseek(file, offset, SEEK_SET) == 0)) {
    char str[20];
    sprintf(str, "pipe:%d", fd);
    strcat(path, str);
}

State *state = av_mallocz(sizeof(State));
state->pFormatCtx = NULL;

if (avformat_open_input(&state->pFormatCtx, path, NULL, &options) != 0) { // Note: path is in the format "pipe:<the FD #>"
    printf("Metadata could not be retrieved\n");
    *ps = NULL;
    return FAILURE;
}

if (avformat_find_stream_info(state->pFormatCtx, NULL) < 0) {
    printf("Metadata could not be retrieved\n");
    avformat_close_input(&state->pFormatCtx);
    *ps = NULL;
    return FAILURE;
}

// Find the first audio and video stream
for (i = 0; i < state->pFormatCtx->nb_streams; i++) {
    if (state->pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO && video_index < 0) {
        video_index = i;
    }

    if (state->pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO && audio_index < 0) {
        audio_index = i;
    }

    set_codec(state->pFormatCtx, i);
}

if (audio_index >= 0) {
    stream_component_open(state, audio_index);
}

if (video_index >= 0) {
    stream_component_open(state, video_index);
}

printf("Found metadata\n");
AVDictionaryEntry *tag = NULL;
while ((tag = av_dict_get(state->pFormatCtx->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) {
    printf("Key %s: \n", tag->key);
    printf("Value %s: \n", tag->value);
}

*ps = state;
return SUCCESS;

My issue is avformat_open_input doesn't fail but it also doesn't let me retrieve any metadata or frames, The same code works if I use a regular file URI (e.g file://sdcard/test.mp3) as the path. What am I doing wrong? Thanks in advance.

Note: if you would like to look at all of the code I'm trying to solve the issue in order to provide this functionality for my library: FFmpegMediaMetadataRetriever.

William Seemann
  • 3,440
  • 10
  • 44
  • 78
  • do you check the value returned by `jniGetFDFromFileDescriptor()`? – Alex Cohn Jul 13 '14 at 18:26
  • I did and I know it's valid because FFmpeg doesn't fail. I can still retrieve some basic information from the file (Duration, codec) but not the same info as using the file URI. I know the FFmpeg code is OK because running "cat test.mp3 | ./mycode" produces the correct output. It appears to be an issue of seeking when using the file descriptor or the way the file is being fed to my FFmpeg code. – William Seemann Jul 13 '14 at 18:33
  • see http://mbcdev.com/2012/04/03/psa-assetfiledescriptor-and-filedescriptor-android-raw-resources/ - you have non-zero `fd.getStartOffset()` – Alex Cohn Jul 13 '14 at 18:33
  • 1
    Good find. Let me try that and see if it works. – William Seemann Jul 13 '14 at 18:38
  • This didn't make a difference. I tried passing in the offset and seeking to the starting position of the asset but FFmpeg still returns the same result, I'm stumped. – William Seemann Jul 13 '14 at 23:12
  • I believe I found the issue, when I initially seek to the asset location the File handle is repositioned, not the FileDescriptor itself. However, I'm still passing the FileDescriptor to FFmpeg which I believe causes the metadata not to be found. Any idea how I would pass the repositioned file handle to FFmpeg?! – William Seemann Jul 14 '14 at 00:13
  • I am not sure I understand the difference between `FileDecriptor` and File handle being repositioned. You can use `avio_alloc_context` with JNI callbacks and ignore the _path_ parameter, but maybe there is an easier way. – Alex Cohn Jul 14 '14 at 04:12
  • 1
    To my knowledge, the FileDecriptor returned by calling getContext().getAssets().openFd("test.mp3") points to the APK itself, fd.getStartOffset() provides the starting point of the specific mp3 asset from with the APK. I need to seek to that offset before attempting to open the file with FFmpeg otherwise it isn't analyzing the file correctly. Does that make sense? – William Seemann Jul 14 '14 at 04:24

4 Answers4

13

Java

AssetFileDescriptor afd = getContext().getAssets().openFd("test.mp3");
setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), fd.getLength());

C

void ***_setDataSource(JNIEnv *env, jobject thiz, 
    jobject fileDescriptor, jlong offset, jlong length)
{
    int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);

    char path[20];
    sprintf(path, "pipe:%d", fd);

    State *state = av_mallocz(sizeof(State));
    state->pFormatCtx =  avformat_alloc_context();
    state->pFormatCtx->skip_initial_bytes = offset;
    state->pFormatCtx->iformat = av_find_input_format("mp3");

and now we can continue as usual:

if (avformat_open_input(&state->pFormatCtx, path, NULL, &options) != 0) {
    printf("Metadata could not be retrieved\n");
    *ps = NULL;
    return FAILURE;
}
...

Even better, use <android/asset_manager.h>, like this:

Java

setDataSource(getContext().getAssets(), "test.mp3");

C

#include <android/asset_manager_jni.h>

void ***_setDataSource(JNIEnv *env, jobject thiz, 
    jobject assetManager, jstring assetName)
{
    AAssetManager* assetManager = AAssetManager_fromJava(env, assetManager);
    const char *szAssetName = (*env)->GetStringUTFChars(env, assetName, NULL);
    AAsset* asset = AAssetManager_open(assetManager, szAssetName, AASSET_MODE_RANDOM);
    (*env)->ReleaseStringUTFChars(env, assetName, szAssetName);
    off_t offset, length;
    int fd = AAsset_openFileDescriptor(asset, &offset, &length);
    AAsset_close(asset);

Disclaimer: error checking was omitted for brevity, but resources are released correctly, except for fd. You must close(fd) when finished.

Post Scriptum: note that some media formats, e.g. mp4 need seekable protocol, and pipe: cannot help. In such case, you may try sprintf(path, "/proc/self/fd/%d", fd);, or use the custom saf: protocol.

Alex Cohn
  • 56,089
  • 9
  • 113
  • 307
  • Thanks, I use your code can open it,But when i called av_read_frame method it's occur error "Invalid data found when proccesing input"? Is there any parameter should be set? `ifmt_ctx = avformat_alloc_context(); ifmt_ctx->skip_initial_bytes = offset; AVInputFormat *iformat = av_find_input_format("mp4"); FFMP_LOGI("av_find_input_format mp4:%p",iformat); //NOTE: MUST SET iformat . ifmt_ctx->iformat = iformat; this->open(filename);` – jhondge Dec 10 '14 at 02:25
  • First of all, check that the offset and the file descriptor are correct. In your native **setDataSource()** method, read maybe 1KByte at the expected offset, and you should see that the result matches the original **mp4** file you put into _assets_. One problem could be that the asset manager may not have recognized the video file as pre-compressed and decided to zip it. – Alex Cohn Dec 10 '14 at 12:08
  • Thanks a lot @Alex Cohn, I'm sure my offset and file descriptor are correct .Because I can get correct video width and height that after called avformat_open_input function use AVCodecContext .I'm goto this respository [FFmpegMediaMetadataRetriever](https://github.com/wseemann/FFmpegMediaMetadataRetriever) But it's can't support pipe: protocol too.So I want to know ffmpeg pipe protocol is be supported in android platform ,if true,can you provide your example for me, thanks a lot. – jhondge Dec 11 '14 at 02:14
  • No, I don't think you can use `pipe:`. But I don't understand how this protocol can help you access **mp4** embedded in your assets. Anyways, I would try to store the same mp4 file to `/sdcard/` and check if same ffmpeg calls can work with that standalone file. – Alex Cohn Dec 11 '14 at 13:10
  • Thanks for your help @Alex Cohn.`void ***_setDataSource(JNIEnv *env, jobject thiz, jobject assetManager, jstring assetName)` this function(your answer) you was implement it.It's can't access mp4? but mp3 can work? – jhondge Dec 12 '14 at 03:25
  • The reason why **mp3** works, is that the Android asset packager knows this extension and knows that it should be saved in the APK without compression. I don't think same knowledge extends to **mp4**. – Alex Cohn Dec 12 '14 at 07:42
  • I really appreciate that you – jhondge Dec 15 '14 at 01:59
  • 1
    I think mp4 is invalid because `mov.c` did not handle the startoffset of Assetfiledescriptor correctly, this is my [answer](https://stackoverflow.com/questions/24701029/how-to-properly-pass-an-asset-filedescriptor-to-ffmpeg-using-jni-in-android/65589858#65589858) – Chao Yi Jan 06 '21 at 03:45
5

Thks a lot for this post. That help me a lot to integrate Android 10 and scoped storage with FFmpeg using FileDescriptor.

Here the solution I'm using on Android 10:

Java

URI uri = ContentUris.withAppendedId(
   MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
   trackId // Coming from `MediaStore.Audio.Media._ID`
);
ParcelFileDescriptor parcelFileDescriptor = getContentResolver().openFileDescriptor(
   uri,
   "r"
);
int pid = android.os.Process.myPid();
String path = "/proc/" + pid + "/fd/" + parcelFileDescriptor.dup().getFd();
loadFFmpeg(path); // Call native code

CPP

// Native code, `path` coming from Java `loadFFmpeg(String)`
avformat_open_input(&format, path, nullptr, nullptr);
  • Have you tested this when selecting a file from the SD Card? I get `/proc/self/fd/66: Permission denied`. – ClassA Feb 18 '20 at 05:22
  • Unfortunately, this does not work on `/sdcard`. This may get addressed on **Android R**, but I found an [alternative method](https://github.com/tanersener/mobile-ffmpeg/pull/440), adding a new `saf:` protocol, to be more reliable. – Alex Cohn May 30 '20 at 19:36
2

OK, I spent a lot of time trying to transfer media data to ffmpeg through Assetfiledescriptor. Finally, I found that there may be a bug in mov.c. When mov.c parsed the trak atom, the corresponding skip_initial_bytes was not set. I have tried to fix this problem.

Detail please refer to FFmpegForAndroidAssetFileDescriptor, demo refer to WhatTheCodec.

Chao Yi
  • 171
  • 1
  • 4
0
 FileDescriptor fd = getContext().getAssets().openFd("test.mp3").getFileDescriptor();

Think you should start with AssetFileDescripter. http://developer.android.com/reference/android/content/res/AssetFileDescriptor.html

greenapps
  • 11,154
  • 2
  • 16
  • 19
  • 1
    I am, openFd return an an AssetFileDescriptor. I'm emulating the setDataSource(FileDescriptor fd) method found in MediaMetadataRetriever. – William Seemann Jul 11 '14 at 19:07