Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

FFmpeg: seeking not possible with file descriptor on Android Q

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?

like image 438
Steve M Avatar asked Aug 10 '19 20:08

Steve M


2 Answers

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

like image 78
gkv311 Avatar answered Oct 14 '22 13:10

gkv311


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;
}
like image 27
Alex Cohn Avatar answered Oct 14 '22 14:10

Alex Cohn