13

Given the fact that public file paths will generally not be available in Android Q with scoped storage, I am attempting to figure out how to make my FFmpeg audio decoder work with file descriptors, without copying the file to my app's private directories.

We can easily get a file descriptor using the methods described in Android Q privacy changes, and it is possible to open the file descriptor using the pipe protocol as described in Passing a native fd int to FFMPEG from openable URI. However, the result is not seekable using av_seek_frame and also the duration is not available using the duration member of AVFormatContext.

Is there way to seek with a file descriptor with FFmpeg and retrieve the duration?

Steve M
  • 9,296
  • 11
  • 49
  • 98
  • 3
    I hope there's an answer for you. Last I wrestled with the seekable-streams problem, the answer was "no". Only file-backed streams were seekable. For short content, you could copy the content to some local file then play that, but I'm guessing you are looking to support content of semi-arbitrary length... – CommonsWare Aug 10 '19 at 20:55
  • @CommonsWare yes, copying is not good for me. I'm assuming my answer will be to use another decoder like the platform MediaCodec, but that's not ideal for me either. – Steve M Aug 10 '19 at 21:01
  • Prefix `cache:` to the pipe url you open and check. cache will attempt to create a temp file in `/tmp` if mkstemp is available or in pwd if not. – Gyan Aug 11 '19 at 05:27
  • Sorry my bounty didn't turn up an answer for you! If you think of it, and if you do find a solution, let me know what it is! These sorts of hiccups are things we need to get addressed in Android R, if we are then permanently limited to the Storage Access Framework, `MediaStore`, etc. – CommonsWare Aug 20 '19 at 22:22
  • 1
    @CommonsWare this is discussed at https://github.com/tanersener/mobile-ffmpeg/issues/334. Inspired by [**gkv311**'s answer](https://stackoverflow.com/a/60702386/192373), I have added a custom `saf:` protocol to handle this kind of access correctly. – Alex Cohn May 30 '20 at 19:27
  • 1
    @AlexCohn: Very cool! Thanks for pointing it out! – CommonsWare May 30 '20 at 19:39
  • @CommonsWare: still WIP – Alex Cohn May 30 '20 at 19:40
  • It seems this issue is not even highly troubling anymore, as we can once again use the _data column from the MediaStore and use the file path for FFmpeg if we wish in Android R. – Steve M May 30 '20 at 20:32
  • @SteveM, direct path access works on the latest R emulator (e.g. to `/sdcard/Movies/test.avi`, but not for files that are not in *public* directories). Hence, if used with a custom provider (e.g. for a file type that MediaStore does not support), this `saf:` may still be useful. …and nobody is going to throw away all these Android 10 devices, and only few of them will get a timely OTA upgrade. – Alex Cohn Jun 03 '20 at 21:23
  • @CommonsWare even better: [pure C way to handle `content:` Uri](https://github.com/alexcohn/mobile-ffmpeg/blob/6935c08bd1d2eb52860f07b5d0948656b41c56b9/android/test-app/src/main/java/com/arthenica/mobileffmpeg/test/ScopedStorageTabFragment.java#L151) – Alex Cohn Jun 11 '20 at 07:12
  • @AlexCohn useful for custom provider perhaps. But we can always use `android:requestLegacyExternalStorage="true"` for Android 10 devices and use file path without problem. – Steve M Jun 12 '20 at 15:05
  • @SteveM: they tell us that working via path names may be [much slower](https://developer.android.com/preview/privacy/storage#direct-file-media-access-performance). – Alex Cohn Aug 05 '20 at 11:49
  • @SteveM not work for me – K.Sopheak Mar 18 '23 at 04:21

2 Answers2

4

it is possible to open the file descriptor using the pipe protocol as described

I'm curious why it is necessary opening file descriptor via pipe protocol? sView player opens file descriptor by custom AVIOContext, which is seekable, at least on older tested versions of Android. Here is a pseudo-code opening AVFormatContext with custom AVIOContext.

    int aFileDescriptor = myResMgr->openFileDescriptor(theFileToLoad);
    AVFormatContext* aFormatCtx = avformat_alloc_context();
    StAVIOContext myAvioContext;
    if(!myAvioContext.openFromDescriptor(aFileDescriptor, "rb")) {
       // error
    }

    aFormatCtx->pb = myAvioContext.getAvioContext();
    int avErrCode = avformat_open_input(&aFormatCtx, theFileToLoad, NULL, NULL);

Below is an attempt to extract a simplified StAVIOFileContext class definition.

//! Wrapper over AVIOContext for passing the custom I/O.
class StAVIOContext {
public:
  //! Main constructor.
  StAVIOContext() {
    const int aBufferSize = 32768;
    unsigned char* aBufferIO = (unsigned char* )av_malloc(aBufferSize + AV_INPUT_BUFFER_PADDING_SIZE);
    AVIOContext* myAvioCtx = avio_alloc_context (aBufferIO, aBufferSize, 0, this, readCallback, writeCallback, seekCallback);
  }

  //! Destructor.
  virtual ~StAVIOContext() {
    close();
    if (myAvioCtx != NULL) { av_free (myAvioCtx); }
  }

  //! Close the file.
  void close() {
    if(myFile != NULL) {
        fclose(myFile);
        myFile = NULL;
    }
  }

  //! Associate a stream with a file that was previously opened for low-level I/O.
  //! The associated file will be automatically closed on destruction.
  bool openFromDescriptor(int theFD, const char* theMode) {
    close();
  #ifdef _WIN32
    myFile = ::_fdopen(theFD, theMode);
  #else
    myFile =  ::fdopen(theFD, theMode);
  #endif
    return myFile != NULL;
  }

  //! Access AVIO context.
  AVIOContext* getAvioContext() const { return myAvioCtx; }

public:

  //! Virtual method for reading the data.
  virtual int read (uint8_t* theBuf,
                    int theBufSize) {
    if(myFile == NULL) { return -1; }

    int aNbRead = (int )::fread(theBuf, 1, theBufSize, myFile);
    if(aNbRead == 0 && feof(myFile) != 0) { return AVERROR_EOF; }
    return aNbRead;
  }

  //! Virtual method for writing the data.
  virtual int write (uint8_t* theBuf,
                     int theBufSize) {
    if(myFile == NULL) { return -1; }
    return (int )::fwrite(theBuf, 1, theBufSize, myFile);
  }

  //! Virtual method for seeking to new position.
  virtual int64_t seek (int64_t theOffset,
                        int theWhence) {
    if(theWhence == AVSEEK_SIZE || myFile == NULL) { return -1; }
  #ifdef _WIN32
    bool isOk = ::_fseeki64(myFile, theOffset, theWhence) == 0;
  #else
    bool isOk =    ::fseeko(myFile, theOffset, theWhence) == 0;
  #endif
    if(!isOk) { return -1; }
  #ifdef _WIN32
    return ::_ftelli64(myFile);
  #else
    return ::ftello(myFile);
  #endif
  }

private:
  //! Callback for reading the data.
  static int readCallback(void* theOpaque,
                          uint8_t* theBuf,
                          int  theBufSize) {
    return theOpaque != NULL
         ? ((StAVIOContext* )theOpaque)->read(theBuf, theBufSize)
         : 0;
  }

  //! Callback for writing the data.
  static int writeCallback(void* theOpaque,
                           uint8_t* theBuf,
                           int theBufSize) {
    return theOpaque != NULL
         ? ((StAVIOContext* )theOpaque)->write(theBuf, theBufSize)
         : 0;
  }

  //! Callback for seeking to new position.
  static int64_t seekCallback(void*   theOpaque,
                              int64_t theOffset,
                              int     theWhence) {
    return theOpaque != NULL
        ? ((StAVIOContext* )theOpaque)->seek(theOffset, theWhence)
        : -1;
  }

protected:
  AVIOContext* myAvioCtx;
  FILE* myFile;
};

gkv311
  • 2,612
  • 1
  • 10
  • 11
1

A custom protocol can handle Uri like content://com.android.providers.downloads.documents/document/msf%3A62 or content://com.android.externalstorage.documents/document/primary%3ADownload%2Ftranscode.aac

Here is the C code that opens such Uri (error checks hidden for brevity):

int get_fd_from_content(const char *content, int access) {

    static jclass    android_net_Uri;
    static jmethodID android_net_Uri_parse = 0;
    static jmethodID android_content_Context_getContentResolver = 0;
    static jmethodID android_content_ContentResolver_openFileDescriptor = 0;
    static jmethodID android_os_ParcelFileDescriptor_getFd = 0;

    int fd = -1;

    JNIEnv *env;
    int ret = (*globalVm)->GetEnv(globalVm, (void **)&env, JNI_VERSION_1_6);

    android_net_Uri_parse = get_static_method_id(env, "android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", &android_net_Uri);
    android_content_Context_getContentResolver = get_method_id(env, "android/content/Context", "getContentResolver", "()Landroid/content/ContentResolver;");
    android_content_ContentResolver_openFileDescriptor = get_method_id(env, "android/content/ContentResolver", "openFileDescriptor", "(Landroid/net/Uri;Ljava/lang/String;)Landroid/os/ParcelFileDescriptor;");
    android_os_ParcelFileDescriptor_getFd = get_method_id(env, "android/os/ParcelFileDescriptor", "getFd", "()I"));

    const char *fmode = "r";
    if (access & (O_WRONLY | O_RDWR)) {
        fmode = "w";
    }

    LOGI("get_fd_from_content" " \"%s\" fd from %s", fmode, content);

    jstring uriString = (*env)->NewStringUTF(env, content);
    jstring fmodeString = (*env)->NewStringUTF(env, fmode);
    jobject uri = (*env)->CallStaticObjectMethod(env, android_net_Uri, android_net_Uri_parse, uriString);
    jobject contentResolver = (*env)->CallObjectMethod(env, appContext, android_content_Context_getContentResolver);
    jobject parcelFileDescriptor = (*env)->CallObjectMethod(env, contentResolver, android_content_ContentResolver_openFileDescriptor, uri, fmodeString);

    fd = (*env)->CallIntMethod(env, parcelFileDescriptor, android_os_ParcelFileDescriptor_getFd);

    (*env)->DeleteLocalRef(env, uriString);
    (*env)->DeleteLocalRef(env, fmodeString);
    (*env)->DeleteLocalRef(env, uri);
    (*env)->DeleteLocalRef(env, contentResolver);
    (*env)->DeleteLocalRef(env, parcelFileDescriptor);

    return fd;
}
Alex Cohn
  • 56,089
  • 9
  • 113
  • 307