如何在Android中使用JNI将资产FileDescriptor正确传递给FFmpeg

时间:2014-07-11 15:13:52

标签: android android-ndk ffmpeg java-native-interface metadata

我正在尝试使用FFmpeg,JNI和Java FileDescriptor在Android中检索元数据,但它并没有“正常工作”。我知道FFmpeg支持pipe protocol,所以我试图以编程方式“cat test.mp3 | ffmpeg i pipe:0”。我使用以下代码从与Android应用程序捆绑在一起的资产中获取FileDescriptor:

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

然后,在我的本机(In C ++)代码中,我通过调用:

获取FileDescriptor
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;
}

然后我将文件描述符管道#(在C中)传递给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;

我的问题是avformat_open_input没有失败,但它也不允许我检索任何元数据或框架,如果我使用常规文件URI(例如file:// sdcard / test),相同的代码也可以。 mp3)作为路径。我究竟做错了什么?提前谢谢。

注意:如果您想查看我正在尝试解决此问题的所有代码,以便为我的库提供此功能:FFmpegMediaMetadataRetriever

4 个答案:

答案 0 :(得分:9)

<强>爪哇

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");

现在我们可以照常继续:

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

更好的是,使用<android/asset_manager.h>,如下所示:

<强>爪哇

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);

免责声明:为简洁起见,省略了错误检查,但资源正确释放, fd 除外。完成后您必须close(fd)

答案 1 :(得分:2)

这篇文章非常重要。 这对我很有帮助,可以使用FileDescriptor将Android 10和范围存储与FFmpeg集成。

这是我在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);

答案 2 :(得分:2)

好的,我花了很多时间尝试通过 Assetfiledescriptor 将媒体数据传输到 ffmpeg。最后发现可能是mov.c有bug。当 mov.c 解析 trak 原子时,未设置相应的 skip_initial_bytes。我已尝试解决此问题。

详情请参考FFmpegForAndroidAssetFileDescriptor,演示请参考WhatTheCodec

答案 3 :(得分:0)

 FileDescriptor fd = getContext().getAssets().openFd("test.mp3").getFileDescriptor();

认为你应该从AssetFileDescripter开始。 http://developer.android.com/reference/android/content/res/AssetFileDescriptor.html