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

Posted

技术标签:

【中文标题】如何在 Android 中使用 JNI 将资产 FileDescriptor 正确传递给 FFmpeg【英文标题】:How to properly pass an asset FileDescriptor to FFmpeg using JNI in Android 【发布时间】:2014-09-02 06:34:16 【问题描述】:

我正在尝试使用 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

然后,在我的本机(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。

【问题讨论】:

你检查jniGetFDFromFileDescriptor()返回的值吗? 我做到了,我知道它是有效的,因为 FFmpeg 不会失败。我仍然可以从文件中检索一些基本信息(持续时间、编解码器),但与使用文件 URI 的信息不同。我知道 FFmpeg 代码没问题,因为运行“cat test.mp3 | ./mycode”会产生正确的输出。使用文件描述符或将文件馈送到我的 FFmpeg 代码的方式时,这似乎是一个查找问题。 见mbcdev.com/2012/04/03/… - 你有非零fd.getStartOffset() 很好的发现。让我试试看它是否有效。 据我所知,调用 getContext().getAssets().openFd("test.mp3") 返回的 FileDecriptor 指向 APK 本身, fd.getStartOffset() 提供了来自 APK 的特定 mp3 资产。在尝试使用 FFmpeg 打开文件之前,我需要找到该偏移量,否则它无法正确分析文件。这有意义吗? 【参考方案1】:
 FileDescriptor fd = getContext().getAssets().openFd("test.mp3").getFileDescriptor();

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

【讨论】:

我是,openFd 返回一个 AssetFileDescriptor。我正在模拟 MediaMetadataRetriever 中的 setDataSource(FileDescriptor fd) 方法。【参考方案2】:

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

现在我们可以像往常一样继续:

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

...

更好的是,使用&lt;android/asset_manager.h&gt;,像这样:

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

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

Post Scriptum:请注意某些媒体格式,例如mp4 需要可搜索的协议,pipe: 帮不上忙。在这种情况下,您可以尝试sprintf(path, "/proc/self/fd/%d", fd);,或使用custom saf: protocol。

【讨论】:

谢谢,我用你的代码可以打开它,但是当我调用av_read_frame方法时出现错误“处理输入时发现无效数据”?有什么参数需要设置吗? ifmt_ctx = avformat_alloc_context(); ifmt_ctx-&gt;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-&gt;iformat = iformat; this-&gt;open(filename); 首先,检查偏移量和文件描述符是否正确。在您的本机 setDataSource() 方法中,在预期的偏移量处读取可能 1KByte,您应该会看到结果与您放入 assetsmp4 文件匹配/i>。一个问题可能是资产管理器可能没有将视频文件识别为预压缩并决定对其进行压缩。 非常感谢@Alex Cohn,我确定我的偏移量和文件描述符是正确的。因为我可以在调用 avformat_open_input 函数后使用 AVCodecContext 获得正确的视频宽度和高度。我去这个存储库FFmpegMediaMetadataRetriever 但它也不支持 pipe: 协议。所以我想知道 ffmpeg pipe 协议在 android 平台是否支持,如果属实,你能提供你的例子吗,非常感谢。 不,我认为你不能使用pipe:。但我不明白这个协议如何帮助您访问嵌入在您的资产中的 mp4。无论如何,我会尝试将相同的 mp4 文件存储到 /sdcard/ 并检查相同的 ffmpeg 调用是否可以与该独立文件一起使用。 我认为mp4无效是因为mov.c没有正确处理Assetfiledescriptor的startoffset,这是我的answer【参考方案3】:

非常感谢这篇文章。 这对我使用 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);

【讨论】:

您在从 SD 卡中选择文件时是否对此进行了测试?我得到/proc/self/fd/66: Permission denied 不幸的是,这不适用于/sdcard。这可能会在 Android R 上得到解决,但我发现了一个 alternative method,添加了一个新的 saf: 协议,更加可靠。【参考方案4】:

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

详情请参考FFmpegForAndroidAssetFileDescriptor,demo参考WhatTheCodec。

【讨论】:

啊,你靠+faststart来避免seek! 只有管道协议需要快速启动。 你有其他协议来访问资产吗?

以上是关于如何在 Android 中使用 JNI 将资产 FileDescriptor 正确传递给 FFmpeg的主要内容,如果未能解决你的问题,请参考以下文章

如何将字节数组从android java类传递给JNI C NDK?

Android JNI开发二: SO库的使用

如何使用 android studio 将复制任务运行到资产文件夹中

如何使用 sqldelight 读取位于资产中的 db 文件 - Android

如何使用 JNI 在 C 中获取原始 Android 相机缓冲区?

如何在没有Android Studio的情况下编译JNI共享库,并在[关闭]中编译依赖项