From a1e7a0b9dadd0d5fd01bce9ac20dfd61dc41aa6a Mon Sep 17 00:00:00 2001 From: Taner Sener Date: Mon, 11 Jan 2021 01:09:39 +0000 Subject: [PATCH] update android api, fixes #1 --- android/app/build.gradle | 1 + android/app/src/main/AndroidManifest.xml | 3 +- android/app/src/main/cpp/ffmpegkit.c | 194 ++--- android/app/src/main/cpp/ffmpegkit.h | 11 +- .../app/src/main/cpp/ffmpegkit_abidetect.c | 2 +- .../app/src/main/cpp/ffmpegkit_abidetect.h | 2 +- .../app/src/main/cpp/ffmpegkit_exception.c | 2 +- .../app/src/main/cpp/ffmpegkit_exception.h | 2 +- android/app/src/main/cpp/ffprobekit.c | 19 +- android/app/src/main/cpp/ffprobekit.h | 6 +- android/app/src/main/cpp/fftools_cmdutils.h | 2 + android/app/src/main/cpp/fftools_ffmpeg.c | 16 +- android/app/src/main/cpp/saf_wrapper.c | 135 ++++ android/app/src/main/cpp/saf_wrapper.h | 46 ++ .../java/com/arthenica/ffmpegkit/Abi.java | 10 +- .../com/arthenica/ffmpegkit/AbiDetect.java | 8 +- .../arthenica/ffmpegkit/AbstractSession.java | 197 +++++ .../ffmpegkit/AsyncFFmpegExecuteTask.java | 45 +- .../ffmpegkit/AsyncFFprobeExecuteTask.java | 34 +- .../AsyncGetMediaInformationTask.java | 29 +- .../arthenica/ffmpegkit/CameraSupport.java | 6 +- .../arthenica/ffmpegkit/ExecuteCallback.java | 12 +- .../com/arthenica/ffmpegkit/FFmpegKit.java | 192 +++-- .../arthenica/ffmpegkit/FFmpegKitConfig.java | 744 +++++++++++++----- .../arthenica/ffmpegkit/FFmpegSession.java | 93 +++ .../com/arthenica/ffmpegkit/FFprobeKit.java | 286 +++++-- .../arthenica/ffmpegkit/FFprobeSession.java | 98 +++ .../java/com/arthenica/ffmpegkit/Level.java | 6 +- .../ffmpegkit/{LogMessage.java => Log.java} | 35 +- .../com/arthenica/ffmpegkit/LogCallback.java | 11 +- .../arthenica/ffmpegkit/MediaInformation.java | 21 +- .../ffmpegkit/MediaInformationParser.java | 13 +- .../ffmpegkit/MediaInformationSession.java | 74 ++ .../com/arthenica/ffmpegkit/Packages.java | 8 +- .../{FFmpegExecution.java => ReturnCode.java} | 34 +- .../java/com/arthenica/ffmpegkit/Session.java | 88 +++ ...rmationCallback.java => SessionState.java} | 15 +- .../java/com/arthenica/ffmpegkit/Signal.java | 4 +- .../com/arthenica/ffmpegkit/Statistics.java | 57 +- .../ffmpegkit/StatisticsCallback.java | 9 +- .../ffmpegkit/StreamInformation.java | 3 +- .../ffmpegkit/AbstractSessionTest.java | 44 ++ android/jni/Android.mk | 8 +- tools/release/android/build.gradle | 1 + tools/release/android/build.lts.gradle | 1 + 45 files changed, 1972 insertions(+), 655 deletions(-) create mode 100644 android/app/src/main/cpp/saf_wrapper.c create mode 100644 android/app/src/main/cpp/saf_wrapper.h create mode 100644 android/app/src/main/java/com/arthenica/ffmpegkit/AbstractSession.java create mode 100644 android/app/src/main/java/com/arthenica/ffmpegkit/FFmpegSession.java create mode 100644 android/app/src/main/java/com/arthenica/ffmpegkit/FFprobeSession.java rename android/app/src/main/java/com/arthenica/ffmpegkit/{LogMessage.java => Log.java} (65%) create mode 100644 android/app/src/main/java/com/arthenica/ffmpegkit/MediaInformationSession.java rename android/app/src/main/java/com/arthenica/ffmpegkit/{FFmpegExecution.java => ReturnCode.java} (54%) create mode 100644 android/app/src/main/java/com/arthenica/ffmpegkit/Session.java rename android/app/src/main/java/com/arthenica/ffmpegkit/{GetMediaInformationCallback.java => SessionState.java} (74%) create mode 100644 android/app/src/test/java/com/arthenica/ffmpegkit/AbstractSessionTest.java diff --git a/android/app/build.gradle b/android/app/build.gradle index c0d15c7..d64fa25 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -39,6 +39,7 @@ task javadoc(type: Javadoc) { } dependencies { + implementation 'com.arthenica:smart-exception-java:0.1.0' testImplementation "androidx.test.ext:junit:1.1.2" testImplementation "org.json:json:20190722" } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c6e1cd1..a815d2d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,2 +1 @@ - - + diff --git a/android/app/src/main/cpp/ffmpegkit.c b/android/app/src/main/cpp/ffmpegkit.c index 15d3bc7..240b283 100644 --- a/android/app/src/main/cpp/ffmpegkit.c +++ b/android/app/src/main/cpp/ffmpegkit.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2018-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -31,7 +31,7 @@ /** Callback data structure */ struct CallbackData { int type; // 1 (log callback) or 2 (statistics callback) - long executionId; // execution id + long sessionId; // session identifier int logLevel; // log level AVBPrint logData; // log data @@ -47,20 +47,16 @@ struct CallbackData { struct CallbackData *next; }; -/** Execution map variables */ -const int EXECUTION_MAP_SIZE = 1000; -static volatile int executionMap[EXECUTION_MAP_SIZE]; -static pthread_mutex_t executionMapMutex; +/** Session map variables */ +const int SESSION_MAP_SIZE = 1000; +static volatile int sessionMap[SESSION_MAP_SIZE]; +static pthread_mutex_t sessionMapMutex; /** Redirection control variables */ static pthread_mutex_t lockMutex; static pthread_mutex_t monitorMutex; static pthread_cond_t monitorCondition; -/** Last command output variables */ -static pthread_mutex_t logMutex; -static AVBPrint lastCommandOutput; - pthread_t callbackThread; int redirectionEnabled; @@ -79,6 +75,9 @@ static jmethodID logMethod; /** Global reference of statistics redirection method in Java */ static jmethodID statisticsMethod; +/** Global reference of closeParcelFileDescriptor method in Java */ +static jmethodID closeParcelFileDescriptorMethod; + /** Global reference of String class in Java */ static jclass stringClass; @@ -86,7 +85,7 @@ static jclass stringClass; static jmethodID stringConstructor; /** Full name of the Config class */ -const char *configClassName = "com/arthenica/ffmpegkit/Config"; +const char *configClassName = "com/arthenica/ffmpegkit/FFmpegKitConfig"; /** Full name of String class */ const char *stringClassName = "java/lang/String"; @@ -98,8 +97,8 @@ volatile int handleSIGTERM = 1; volatile int handleSIGXCPU = 1; volatile int handleSIGPIPE = 1; -/** Holds the id of the current execution */ -__thread volatile long executionId = 0; +/** Holds the id of the current session */ +__thread volatile long sessionId = 0; /** Holds the default log level */ int configuredLogLevel = AV_LOG_INFO; @@ -118,7 +117,6 @@ JNINativeMethod configMethods[] = { {"registerNewNativeFFmpegPipe", "(Ljava/lang/String;)I", (void*) Java_com_arthenica_ffmpegkit_FFmpegKitConfig_registerNewNativeFFmpegPipe}, {"getNativeBuildDate", "()Ljava/lang/String;", (void*) Java_com_arthenica_ffmpegkit_FFmpegKitConfig_getNativeBuildDate}, {"setNativeEnvironmentVariable", "(Ljava/lang/String;Ljava/lang/String;)I", (void*) Java_com_arthenica_ffmpegkit_FFmpegKitConfig_setNativeEnvironmentVariable}, - {"getNativeLastCommandOutput", "()Ljava/lang/String;", (void*) Java_com_arthenica_ffmpegkit_FFmpegKitConfig_getNativeLastCommandOutput}, {"ignoreNativeSignal", "(I)V", (void*) Java_com_arthenica_ffmpegkit_FFmpegKitConfig_ignoreNativeSignal} }; @@ -215,23 +213,12 @@ void monitorInit() { pthread_condattr_destroy(&cattributes); } -void logInit() { - pthread_mutexattr_t attributes; - pthread_mutexattr_init(&attributes); - pthread_mutexattr_settype(&attributes, PTHREAD_MUTEX_RECURSIVE_NP); - - pthread_mutex_init(&logMutex, &attributes); - pthread_mutexattr_destroy(&attributes); - - av_bprint_init(&lastCommandOutput, 0, AV_BPRINT_SIZE_UNLIMITED); -} - void executionMapLockInit() { pthread_mutexattr_t attributes; pthread_mutexattr_init(&attributes); pthread_mutexattr_settype(&attributes, PTHREAD_MUTEX_RECURSIVE_NP); - pthread_mutex_init(&executionMapMutex, &attributes); + pthread_mutex_init(&sessionMapMutex, &attributes); pthread_mutexattr_destroy(&attributes); } @@ -244,52 +231,24 @@ void monitorUnInit() { pthread_cond_destroy(&monitorCondition); } -void logUnInit() { - pthread_mutex_destroy(&logMutex); -} - void executionMapLockUnInit() { - pthread_mutex_destroy(&executionMapMutex); + pthread_mutex_destroy(&sessionMapMutex); } void mutexLock() { pthread_mutex_lock(&lockMutex); } -void lastCommandOutputLock() { - pthread_mutex_lock(&logMutex); -} - -void executionMapLock() { - pthread_mutex_lock(&executionMapMutex); +void sessionMapLock() { + pthread_mutex_lock(&sessionMapMutex); } void mutexUnlock() { pthread_mutex_unlock(&lockMutex); } -void lastCommandOutputUnlock() { - pthread_mutex_unlock(&logMutex); -} - -void executionMapUnlock() { - pthread_mutex_unlock(&executionMapMutex); -} - -void clearLastCommandOutput() { - lastCommandOutputLock(); - av_bprint_clear(&lastCommandOutput); - lastCommandOutputUnlock(); -} - -void appendLastCommandOutput(AVBPrint *logMessage) { - if (logMessage->len <= 0) { - return; - } - - lastCommandOutputLock(); - av_bprintf(&lastCommandOutput, "%s", logMessage->str); - lastCommandOutputUnlock(); +void sessionMapUnlock() { + pthread_mutex_unlock(&sessionMapMutex); } void monitorWait(int milliSeconds) { @@ -331,7 +290,7 @@ void logCallbackDataAdd(int level, AVBPrint *data) { // CREATE DATA STRUCT FIRST struct CallbackData *newData = (struct CallbackData*)av_malloc(sizeof(struct CallbackData)); newData->type = 1; - newData->executionId = executionId; + newData->sessionId = sessionId; newData->logLevel = level; av_bprint_init(&newData->logData, 0, AV_BPRINT_SIZE_UNLIMITED); av_bprintf(&newData->logData, "%s", data->str); @@ -368,7 +327,7 @@ void statisticsCallbackDataAdd(int frameNumber, float fps, float quality, int64_ // CREATE DATA STRUCT FIRST struct CallbackData *newData = (struct CallbackData*)av_malloc(sizeof(struct CallbackData)); newData->type = 2; - newData->executionId = executionId; + newData->sessionId = sessionId; newData->statisticsFrameNumber = frameNumber; newData->statisticsFps = fps; newData->statisticsQuality = quality; @@ -403,17 +362,17 @@ void statisticsCallbackDataAdd(int frameNumber, float fps, float quality, int64_ } /** - * Adds an execution id to the execution map. + * Adds a session id to the session map. * - * @param id execution id + * @param id session id */ -void addExecution(long id) { - executionMapLock(); +void addSession(long id) { + sessionMapLock(); - int key = id % EXECUTION_MAP_SIZE; - executionMap[key] = 1; + int key = id % SESSION_MAP_SIZE; + sessionMap[key] = 1; - executionMapUnlock(); + sessionMapUnlock(); } /** @@ -449,36 +408,50 @@ struct CallbackData *callbackDataRemove() { } /** - * Removes an execution id from the execution map. + * Removes a session id from the session map. * - * @param id execution id + * @param id session id */ -void removeExecution(long id) { - executionMapLock(); +void removeSession(long id) { + sessionMapLock(); - int key = id % EXECUTION_MAP_SIZE; - executionMap[key] = 0; + int key = id % SESSION_MAP_SIZE; + sessionMap[key] = 0; - executionMapUnlock(); + sessionMapUnlock(); } /** - * Checks whether a cancel request for the given execution id exists in the execution map. + * Adds a cancel session request to the session map. * - * @param id execution id + * @param id session id + */ +void cancelSession(long id) { + sessionMapLock(); + + int key = id % SESSION_MAP_SIZE; + sessionMap[key] = 2; + + sessionMapUnlock(); +} + +/** + * Checks whether a cancel request for the given session id exists in the session map. + * + * @param id session id * @return 1 if exists, false otherwise */ int cancelRequested(long id) { int found = 0; - executionMapLock(); + sessionMapLock(); - int key = id % EXECUTION_MAP_SIZE; - if (executionMap[key] == 0) { + int key = id % SESSION_MAP_SIZE; + if (sessionMap[key] == 2) { found = 1; } - executionMapUnlock(); + sessionMapUnlock(); return found; } @@ -519,7 +492,6 @@ void ffmpegkit_log_callback_function(void *ptr, int level, const char* format, v if (fullLine.len > 0) { logCallbackDataAdd(level, &fullLine); - appendLastCommandOutput(&fullLine); } av_bprint_finalize(part, NULL); @@ -576,7 +548,7 @@ void *callbackThreadFunction() { jbyteArray byteArray = (jbyteArray) (*env)->NewByteArray(env, size); (*env)->SetByteArrayRegion(env, byteArray, 0, size, callbackData->logData.str); - (*env)->CallStaticVoidMethod(env, configClass, logMethod, (jlong) callbackData->executionId, callbackData->logLevel, byteArray); + (*env)->CallStaticVoidMethod(env, configClass, logMethod, (jlong) callbackData->sessionId, callbackData->logLevel, byteArray); (*env)->DeleteLocalRef(env, byteArray); // CLEAN LOG DATA @@ -587,7 +559,7 @@ void *callbackThreadFunction() { // STATISTICS CALLBACK (*env)->CallStaticVoidMethod(env, configClass, statisticsMethod, - (jlong) callbackData->executionId, callbackData->statisticsFrameNumber, + (jlong) callbackData->sessionId, callbackData->statisticsFrameNumber, callbackData->statisticsFps, callbackData->statisticsQuality, callbackData->statisticsSize, callbackData->statisticsTime, callbackData->statisticsBitrate, callbackData->statisticsSpeed); @@ -610,6 +582,15 @@ void *callbackThreadFunction() { return NULL; } +/** + * Used by saf_wrapper; is expected to be called from a Java thread, therefore we don't need attach/detach + */ +void closeParcelFileDescriptor(int fd) { + JNIEnv *env = NULL; + (*globalVm)->GetEnv(globalVm, (void**) &env, JNI_VERSION_1_6); + (*env)->CallStaticVoidMethod(env, configClass, closeParcelFileDescriptorMethod, fd); +} + /** * Called when 'ffmpegkit' native library is loaded. * @@ -630,7 +611,7 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) { return JNI_FALSE; } - if ((*env)->RegisterNatives(env, localConfigClass, configMethods, 12) < 0) { + if ((*env)->RegisterNatives(env, localConfigClass, configMethods, 13) < 0) { LOGE("OnLoad failed to RegisterNatives for class %s.\n", configClassName); return JNI_FALSE; } @@ -655,6 +636,12 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) { return JNI_FALSE; } + closeParcelFileDescriptorMethod = (*env)->GetStaticMethodID(env, localConfigClass, "closeParcelFileDescriptor", "(I)V"); + if (logMethod == NULL) { + LOGE("OnLoad thread failed to GetStaticMethodID for %s.\n", "closeParcelFileDescriptor"); + return JNI_FALSE; + } + stringConstructor = (*env)->GetMethodID(env, localStringClass, "", "([BLjava/lang/String;)V"); if (stringConstructor == NULL) { LOGE("OnLoad thread failed to GetMethodID for %s.\n", ""); @@ -671,13 +658,12 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) { callbackDataHead = NULL; callbackDataTail = NULL; - for(int i = 0; i 0) { - jbyteArray byteArray = (*env)->NewByteArray(env, size); - (*env)->SetByteArrayRegion(env, byteArray, 0, size, lastCommandOutput.str); - jstring charsetName = (*env)->NewStringUTF(env, "UTF-8"); - return (jstring) (*env)->NewObject(env, stringClass, stringConstructor, byteArray, charsetName); - } - - return (*env)->NewStringUTF(env, ""); -} - /** * Registers a new ignored signal. Ignored signals are not handled by the library. * diff --git a/android/app/src/main/cpp/ffmpegkit.h b/android/app/src/main/cpp/ffmpegkit.h index 87f933e..6a357fd 100644 --- a/android/app/src/main/cpp/ffmpegkit.h +++ b/android/app/src/main/cpp/ffmpegkit.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2018-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -94,7 +94,7 @@ JNIEXPORT jstring JNICALL Java_com_arthenica_ffmpegkit_FFmpegKitConfig_getNative * Method: nativeFFmpegExecute * Signature: (J[Ljava/lang/String;)I */ -JNIEXPORT jint JNICALL Java_com_arthenica_ffmpegkit_FFmpegKitConfig_nativeFFmpegExecute(JNIEnv *, jclass, jlong id, jobjectArray); +JNIEXPORT jint JNICALL Java_com_arthenica_ffmpegkit_FFmpegKitConfig_nativeFFmpegExecute(JNIEnv *, jclass, jlong, jobjectArray); /* * Class: com_arthenica_ffmpegkit_FFmpegKitConfig @@ -124,13 +124,6 @@ JNIEXPORT jstring JNICALL Java_com_arthenica_ffmpegkit_FFmpegKitConfig_getNative */ JNIEXPORT int JNICALL Java_com_arthenica_ffmpegkit_FFmpegKitConfig_setNativeEnvironmentVariable(JNIEnv *env, jclass object, jstring variableName, jstring variableValue); -/* - * Class: com_arthenica_ffmpegkit_FFmpegKitConfig - * Method: getNativeLastCommandOutput - * Signature: ()Ljava/lang/String; - */ -JNIEXPORT jstring JNICALL Java_com_arthenica_ffmpegkit_FFmpegKitConfig_getNativeLastCommandOutput(JNIEnv *env, jclass object); - /* * Class: com_arthenica_ffmpegkit_FFmpegKitConfig * Method: ignoreNativeSignal diff --git a/android/app/src/main/cpp/ffmpegkit_abidetect.c b/android/app/src/main/cpp/ffmpegkit_abidetect.c index 5658e2e..698527e 100644 --- a/android/app/src/main/cpp/ffmpegkit_abidetect.c +++ b/android/app/src/main/cpp/ffmpegkit_abidetect.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2018-2021 Taner Sener * * This file is part of FFmpegKit. * diff --git a/android/app/src/main/cpp/ffmpegkit_abidetect.h b/android/app/src/main/cpp/ffmpegkit_abidetect.h index 1a4117f..558ecc5 100644 --- a/android/app/src/main/cpp/ffmpegkit_abidetect.h +++ b/android/app/src/main/cpp/ffmpegkit_abidetect.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2018-2021 Taner Sener * * This file is part of FFmpegKit. * diff --git a/android/app/src/main/cpp/ffmpegkit_exception.c b/android/app/src/main/cpp/ffmpegkit_exception.c index 88a049a..722fb96 100644 --- a/android/app/src/main/cpp/ffmpegkit_exception.c +++ b/android/app/src/main/cpp/ffmpegkit_exception.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2018-2021 Taner Sener * * This file is part of FFmpegKit. * diff --git a/android/app/src/main/cpp/ffmpegkit_exception.h b/android/app/src/main/cpp/ffmpegkit_exception.h index d58b77e..daf3acc 100644 --- a/android/app/src/main/cpp/ffmpegkit_exception.h +++ b/android/app/src/main/cpp/ffmpegkit_exception.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2018-2021 Taner Sener * * This file is part of FFmpegKit. * diff --git a/android/app/src/main/cpp/ffprobekit.c b/android/app/src/main/cpp/ffprobekit.c index f6b4597..027f148 100644 --- a/android/app/src/main/cpp/ffprobekit.c +++ b/android/app/src/main/cpp/ffprobekit.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Taner Sener + * Copyright (c) 2020-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -29,20 +29,21 @@ /** Forward declaration for function defined in fftools_ffprobe.c */ int ffprobe_execute(int argc, char **argv); -/** Forward declaration for function defined in ffmpegkit.c */ -void clearLastCommandOutput(); - extern int configuredLogLevel; +extern __thread volatile long sessionId; +extern void addSession(long id); +extern void removeSession(long id); /** * Synchronously executes FFprobe natively with arguments provided. * * @param env pointer to native method interface * @param object reference to the class on which this method is invoked + * @param id session id * @param stringArray reference to the object holding FFprobe command arguments * @return zero on successful execution, non-zero on error */ -JNIEXPORT jint JNICALL Java_com_arthenica_ffmpegkit_FFmpegKitConfig_nativeFFprobeExecute(JNIEnv *env, jclass object, jobjectArray stringArray) { +JNIEXPORT jint JNICALL Java_com_arthenica_ffmpegkit_FFmpegKitConfig_nativeFFprobeExecute(JNIEnv *env, jclass object, jlong id, jobjectArray stringArray) { jstring *tempArray = NULL; int argumentCount = 1; char **argv = NULL; @@ -75,12 +76,16 @@ JNIEXPORT jint JNICALL Java_com_arthenica_ffmpegkit_FFmpegKitConfig_nativeFFprob } } - // LAST COMMAND OUTPUT SHOULD BE CLEARED BEFORE STARTING A NEW EXECUTION - clearLastCommandOutput(); + // REGISTER THE ID BEFORE STARTING EXECUTION + sessionId = (long) id; + addSession((long) id); // RUN int retCode = ffprobe_execute(argumentCount, argv); + // ALWAYS REMOVE THE ID FROM THE MAP + removeSession((long) id); + // CLEANUP if (tempArray != NULL) { for (int i = 0; i < (argumentCount - 1); i++) { diff --git a/android/app/src/main/cpp/ffprobekit.h b/android/app/src/main/cpp/ffprobekit.h index 0b18f53..c01c210 100644 --- a/android/app/src/main/cpp/ffprobekit.h +++ b/android/app/src/main/cpp/ffprobekit.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Taner Sener + * Copyright (c) 2020-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -25,8 +25,8 @@ /* * Class: com_arthenica_ffmpegkit_FFmpegKitConfig * Method: nativeFFprobeExecute - * Signature: ([Ljava/lang/String;)I + * Signature: (J[Ljava/lang/String;)I */ -JNIEXPORT jint JNICALL Java_com_arthenica_ffmpegkit_FFmpegKitConfig_nativeFFprobeExecute(JNIEnv *, jclass, jobjectArray); +JNIEXPORT jint JNICALL Java_com_arthenica_ffmpegkit_FFmpegKitConfig_nativeFFprobeExecute(JNIEnv *, jclass, jlong, jobjectArray); #endif /* FFPROBE_KIT_H */ \ No newline at end of file diff --git a/android/app/src/main/cpp/fftools_cmdutils.h b/android/app/src/main/cpp/fftools_cmdutils.h index fe952a5..6ec4425 100644 --- a/android/app/src/main/cpp/fftools_cmdutils.h +++ b/android/app/src/main/cpp/fftools_cmdutils.h @@ -51,6 +51,8 @@ #include "libavformat/avformat.h" #include "libswscale/swscale.h" +#include "saf_wrapper.h" + #ifdef _WIN32 #undef main /* We don't want SDL to override our main() */ #endif diff --git a/android/app/src/main/cpp/fftools_ffmpeg.c b/android/app/src/main/cpp/fftools_ffmpeg.c index 65f5657..9fe0e29 100644 --- a/android/app/src/main/cpp/fftools_ffmpeg.c +++ b/android/app/src/main/cpp/fftools_ffmpeg.c @@ -253,8 +253,8 @@ extern volatile int handleSIGTERM; extern volatile int handleSIGXCPU; extern volatile int handleSIGPIPE; -extern __thread volatile long executionId; -extern void removeExecution(long id); +extern __thread volatile long sessionId; +extern void cancelSession(long id); extern int cancelRequested(long id); /* sub2video hack: @@ -729,7 +729,7 @@ static void ffmpeg_cleanup(int ret) if (received_sigterm) { av_log(NULL, AV_LOG_INFO, "Exiting normally, received signal %d.\n", (int) received_sigterm); - } else if (cancelRequested(executionId)) { + } else if (cancelRequested(sessionId)) { av_log(NULL, AV_LOG_INFO, "Exiting normally, received cancel signal.\n"); } else if (ret && atomic_load(&transcode_init_done)) { av_log(NULL, AV_LOG_INFO, "Conversion failed!\n"); @@ -2392,7 +2392,7 @@ static int ifilter_send_eof(InputFilter *ifilter, int64_t pts) if (ifilter->filter) { /* THIS VALIDATION IS REQUIRED TO COMPLETE CANCELLATION */ - if (!received_sigterm && !cancelRequested(executionId)) { + if (!received_sigterm && !cancelRequested(sessionId)) { ret = av_buffersrc_close(ifilter->filter, pts, AV_BUFFERSRC_FLAG_PUSH); } if (ret < 0) @@ -4859,7 +4859,7 @@ static int transcode(void) goto fail; #endif - while (!received_sigterm && !cancelRequested(executionId)) { + while (!received_sigterm && !cancelRequested(sessionId)) { int64_t cur_time= av_gettime_relative(); /* if 'q' pressed, exits */ @@ -5064,7 +5064,7 @@ void cancel_operation(long id) if (id == 0) { sigterm_handler(SIGINT); } else { - removeExecution(id); + cancelSession(id); } } @@ -5575,10 +5575,10 @@ int ffmpeg_execute(int argc, char **argv) if ((decode_error_stat[0] + decode_error_stat[1]) * max_error_rate < decode_error_stat[1]) exit_program(69); - exit_program((received_nb_signals || cancelRequested(executionId))? 255 : main_ffmpeg_return_code); + exit_program((received_nb_signals || cancelRequested(sessionId))? 255 : main_ffmpeg_return_code); } else { - main_ffmpeg_return_code = (received_nb_signals || cancelRequested(executionId)) ? 255 : longjmp_value; + main_ffmpeg_return_code = (received_nb_signals || cancelRequested(sessionId)) ? 255 : longjmp_value; } return main_ffmpeg_return_code; diff --git a/android/app/src/main/cpp/saf_wrapper.c b/android/app/src/main/cpp/saf_wrapper.c new file mode 100644 index 0000000..0221a3c --- /dev/null +++ b/android/app/src/main/cpp/saf_wrapper.c @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2020-2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +#include +#include +#include + +#include "config.h" +#include "libavformat/avformat.h" +#include "libavutil/avstring.h" + +#include "saf_wrapper.h" + +/** JNI wrapper in ffmpegkit.c */ +void closeParcelFileDescriptor(int fd); + +// in these wrappers, we call the original functions, so we remove the shadow defines +#undef avio_closep +#undef avformat_close_input +#undef avio_open +#undef avio_open2 +#undef avformat_open_input + +static int fd_read_packet(void* opaque, uint8_t* buf, int buf_size) { + int fd = (int)opaque; + return read(fd, buf, buf_size); +} + +static int fd_write_packet(void* opaque, uint8_t* buf, int buf_size) { + int fd = (int)opaque; + return write(fd, buf, buf_size); +} + +static int64_t fd_seek(void *opaque, int64_t offset, int whence) { + int fd = (int)opaque; + + if (fd < 0) { + return AVERROR(EINVAL); + } + + int64_t ret; + if (whence == AVSEEK_SIZE) { + struct stat st; + ret = fstat(fd, &st); + return ret < 0 ? AVERROR(errno) : (S_ISFIFO(st.st_mode) ? 0 : st.st_size); + } + + ret = lseek(fd, offset, whence); + + return ret < 0 ? AVERROR(errno) : ret; +} + +/* + * returns NULL if the filename is not of expected format (e.g. 'saf:72/video.md4') + */ +static AVIOContext *create_fd_avio_context(const char *filename, int flags) { + union {int fd; void* opaque;} fdunion; + fdunion.fd = -1; + const char *fd_ptr = NULL; + if (av_strstart(filename, "saf:", &fd_ptr)) { + char *final; + fdunion.fd = strtol(fd_ptr, &final, 10); + if (fd_ptr == final) { /* No digits found */ + fdunion.fd = -1; + } + } + + if (fdunion.fd >= 0) { + int write_flag = flags & AVIO_FLAG_WRITE ? 1 : 0; + return avio_alloc_context(av_malloc(4096), 4096, write_flag, fdunion.opaque, fd_read_packet, write_flag ? fd_write_packet : NULL, fd_seek); + } + return NULL; +} + +static void close_fd_avio_context(AVIOContext *ctx) { + if (fd_seek(ctx->opaque, 0, AVSEEK_SIZE) >= 0) { + int fd = (int)ctx->opaque; + close(fd); + closeParcelFileDescriptor(fd); + } + ctx->opaque = NULL; +} + +int android_avformat_open_input(AVFormatContext **ps, const char *filename, + ff_const59 AVInputFormat *fmt, AVDictionary **options) { + if (!(*ps) && !(*ps = avformat_alloc_context())) + return AVERROR(ENOMEM); + + (*ps)->pb = create_fd_avio_context(filename, AVIO_FLAG_READ); + + return avformat_open_input(ps, filename, fmt, options); +} + +int android_avio_open2(AVIOContext **s, const char *filename, int flags, + const AVIOInterruptCB *int_cb, AVDictionary **options) { + AVIOContext *fd_context = create_fd_avio_context(filename, flags); + + if (fd_context) { + *s = fd_context; + return 0; + } + return avio_open2(s, filename, flags, int_cb, options); +} + +int android_avio_open(AVIOContext **s, const char *url, int flags) { + return android_avio_open2(s, url, flags, NULL, NULL); +} + +int android_avio_closep(AVIOContext **s) { + close_fd_avio_context(*s); + return avio_closep(s); +} + +void android_avformat_close_input(AVFormatContext **ps) { + if (*ps && (*ps)->pb) { + close_fd_avio_context((*ps)->pb); + } + avformat_close_input(ps); +} diff --git a/android/app/src/main/cpp/saf_wrapper.h b/android/app/src/main/cpp/saf_wrapper.h new file mode 100644 index 0000000..e934d56 --- /dev/null +++ b/android/app/src/main/cpp/saf_wrapper.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020-2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +#ifndef FFMPEG_KIT_SAF_WRAPPER_H +#define FFMPEG_KIT_SAF_WRAPPER_H + +/* + * These wrappers are intended to be used instead of the ffmpeg apis. + * You don't even need to change the source to call them. + * Instead, we redefine the public api names so that the wrapper be used. + */ + +int android_avio_closep(AVIOContext **s); +#define avio_closep android_avio_closep + +void android_avformat_close_input(AVFormatContext **s); +#define avformat_close_input android_avformat_close_input + +int android_avio_open(AVIOContext **s, const char *url, int flags); +#define avio_open android_avio_open + +int android_avio_open2(AVIOContext **s, const char *url, int flags, + const AVIOInterruptCB *int_cb, AVDictionary **options); +#define avio_open2 android_avio_open2 + +int android_avformat_open_input(AVFormatContext **ps, const char *filename, + ff_const59 AVInputFormat *fmt, AVDictionary **options); +#define avformat_open_input android_avformat_open_input + +#endif //FFMPEG_KIT_SAF_WRAPPER_H diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/Abi.java b/android/app/src/main/java/com/arthenica/ffmpegkit/Abi.java index 68afbd3..a0c8688 100644 --- a/android/app/src/main/java/com/arthenica/ffmpegkit/Abi.java +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/Abi.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2018-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -20,7 +20,9 @@ package com.arthenica.ffmpegkit; /** - *

Helper enumeration type for Android ABIs; includes only supported ABIs. + *

Enumeration type for Android ABIs. + * + * @author Taner Sener */ public enum Abi { @@ -59,7 +61,7 @@ public enum Abi { */ ABI_UNKNOWN("unknown"); - private String name; + private final String name; /** *

Returns enumeration defined by ABI name. @@ -97,7 +99,7 @@ public enum Abi { } /** - * Creates new enum. + * Creates a new enum. * * @param abiName ABI name */ diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/AbiDetect.java b/android/app/src/main/java/com/arthenica/ffmpegkit/AbiDetect.java index bfddd0d..08b9a8d 100644 --- a/android/app/src/main/java/com/arthenica/ffmpegkit/AbiDetect.java +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/AbiDetect.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2018-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -20,7 +20,7 @@ package com.arthenica.ffmpegkit; /** - *

This class is used to detect running ABI name using Google cpu-features library. + *

Detects running ABI name using Google cpu-features library. */ public class AbiDetect { @@ -46,8 +46,8 @@ public class AbiDetect { private AbiDetect() { } - static void setArmV7aNeonLoaded(final boolean armV7aNeonLoaded) { - AbiDetect.armV7aNeonLoaded = armV7aNeonLoaded; + static void setArmV7aNeonLoaded() { + armV7aNeonLoaded = true; } /** diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/AbstractSession.java b/android/app/src/main/java/com/arthenica/ffmpegkit/AbstractSession.java new file mode 100644 index 0000000..62750b8 --- /dev/null +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/AbstractSession.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +package com.arthenica.ffmpegkit; + +import com.arthenica.smartexception.java.Exceptions; + +import java.util.Date; +import java.util.Optional; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public abstract class AbstractSession implements Session { + + /** + * Generates ids for execute sessions. + */ + private static final AtomicLong sessionIdGenerator = new AtomicLong(1); + + protected final ExecuteCallback executeCallback; + protected final LogCallback logCallback; + protected final StatisticsCallback statisticsCallback; + protected final long sessionId; + protected final Date createTime; + protected Date startTime; + protected Date endTime; + protected final String[] arguments; + protected final Queue logs; + protected Future future; + protected SessionState state; + protected int returnCode; + protected String failStackTrace; + + public AbstractSession(final String[] arguments, + final ExecuteCallback executeCallback, + final LogCallback logCallback, + final StatisticsCallback statisticsCallback) { + this.sessionId = sessionIdGenerator.getAndIncrement(); + this.createTime = new Date(); + this.startTime = null; + this.arguments = arguments; + this.executeCallback = executeCallback; + this.logCallback = logCallback; + this.statisticsCallback = statisticsCallback; + this.logs = new ConcurrentLinkedQueue<>(); + this.future = null; + this.state = SessionState.CREATED; + this.returnCode = ReturnCode.NOT_SET; + this.failStackTrace = null; + } + + public ExecuteCallback getExecuteCallback() { + return executeCallback; + } + + public LogCallback getLogCallback() { + return logCallback; + } + + public StatisticsCallback getStatisticsCallback() { + return statisticsCallback; + } + + public long getSessionId() { + return sessionId; + } + + public Date getCreateTime() { + return createTime; + } + + public Date getStartTime() { + return startTime; + } + + public Date getEndTime() { + return endTime; + } + + public long getDuration() { + final Date startTime = this.startTime; + final Date endTime = this.endTime; + if (startTime != null && endTime != null) { + return (endTime.getTime() - startTime.getTime()); + } + + return -1; + } + + public String[] getArguments() { + return arguments; + } + + public String getCommand() { + return FFmpegKit.argumentsToString(arguments); + } + + public Queue getLogs() { + return logs; + } + + public Stream getLogsAsStream() { + return logs.stream(); + } + + public String getLogsAsString() { + final Optional concatenatedStringOption = logs.stream().map(new Function() { + @Override + public String apply(final Log log) { + return log.getMessage(); + } + }).reduce(new BinaryOperator() { + @Override + public String apply(final String s1, final String s2) { + return s1 + s2; + } + }); + + return concatenatedStringOption.orElseGet(new Supplier() { + + @Override + public String get() { + return ""; + } + }); + } + + public SessionState getState() { + return state; + } + + public int getReturnCode() { + return returnCode; + } + + public String getFailStackTrace() { + return failStackTrace; + } + + public void addLog(final Log log) { + this.logs.add(log); + } + + public Future getFuture() { + return future; + } + + public void setFuture(final Future future) { + this.future = future; + } + + public void startRunning() { + this.state = SessionState.RUNNING; + this.startTime = new Date(); + } + + public void complete(final int returnCode) { + this.returnCode = returnCode; + this.state = SessionState.COMPLETED; + this.endTime = new Date(); + } + + public void fail(final Exception exception) { + this.failStackTrace = Exceptions.getStackTraceString(exception); + this.state = SessionState.FAILED; + this.endTime = new Date(); + } + + public void cancel() { + if (state == SessionState.RUNNING) { + FFmpegKit.cancel(sessionId); + } + } + +} diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/AsyncFFmpegExecuteTask.java b/android/app/src/main/java/com/arthenica/ffmpegkit/AsyncFFmpegExecuteTask.java index ba1e2bb..f7148f9 100644 --- a/android/app/src/main/java/com/arthenica/ffmpegkit/AsyncFFmpegExecuteTask.java +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/AsyncFFmpegExecuteTask.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2018-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -19,44 +19,29 @@ package com.arthenica.ffmpegkit; -import android.os.AsyncTask; - /** - *

Utility class to execute an FFmpeg command asynchronously. + *

Executes an FFmpeg session asynchronously. */ -public class AsyncFFmpegExecuteTask extends AsyncTask { - private final String[] arguments; +public class AsyncFFmpegExecuteTask implements Runnable { + private final FFmpegSession ffmpegSession; private final ExecuteCallback executeCallback; - private final Long executionId; - public AsyncFFmpegExecuteTask(final String command, final ExecuteCallback executeCallback) { - this(FFmpegKit.parseArguments(command), executeCallback); - } - - public AsyncFFmpegExecuteTask(final String[] arguments, final ExecuteCallback executeCallback) { - this(FFmpegKit.DEFAULT_EXECUTION_ID, arguments, executeCallback); - } - - public AsyncFFmpegExecuteTask(final long executionId, final String command, final ExecuteCallback executeCallback) { - this(executionId, FFmpegKit.parseArguments(command), executeCallback); - } - - public AsyncFFmpegExecuteTask(final long executionId, final String[] arguments, final ExecuteCallback executeCallback) { - this.executionId = executionId; - this.arguments = arguments; - this.executeCallback = executeCallback; + public AsyncFFmpegExecuteTask(final FFmpegSession ffmpegSession) { + this.ffmpegSession = ffmpegSession; + this.executeCallback = ffmpegSession.getExecuteCallback(); } @Override - protected Integer doInBackground(final Void... unused) { - return FFmpegKitConfig.ffmpegExecute(executionId, this.arguments); - } + public void run() { + FFmpegKitConfig.ffmpegExecute(ffmpegSession); + + final ExecuteCallback globalExecuteCallbackFunction = FFmpegKitConfig.getGlobalExecuteCallbackFunction(); + if (globalExecuteCallbackFunction != null) { + globalExecuteCallbackFunction.apply(ffmpegSession); + } - @Override - protected void onPostExecute(final Integer rc) { if (executeCallback != null) { - executeCallback.apply(executionId, rc); + executeCallback.apply(ffmpegSession); } } - } diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/AsyncFFprobeExecuteTask.java b/android/app/src/main/java/com/arthenica/ffmpegkit/AsyncFFprobeExecuteTask.java index 6c6c0df..654d563 100644 --- a/android/app/src/main/java/com/arthenica/ffmpegkit/AsyncFFprobeExecuteTask.java +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/AsyncFFprobeExecuteTask.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2018-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -19,34 +19,24 @@ package com.arthenica.ffmpegkit; -import android.os.AsyncTask; - /** - *

Utility class to execute an FFprobe command asynchronously. + *

Executes an FFprobe execution asynchronously. */ -public class AsyncFFprobeExecuteTask extends AsyncTask { - private final String[] arguments; - private final ExecuteCallback ExecuteCallback; +public class AsyncFFprobeExecuteTask implements Runnable { + private final FFprobeSession ffprobeSession; + private final ExecuteCallback executeCallback; - public AsyncFFprobeExecuteTask(final String command, final ExecuteCallback executeCallback) { - this.arguments = FFmpegKit.parseArguments(command); - this.ExecuteCallback = executeCallback; - } - - public AsyncFFprobeExecuteTask(final String[] arguments, final ExecuteCallback executeCallback) { - this.arguments = arguments; - ExecuteCallback = executeCallback; + public AsyncFFprobeExecuteTask(final FFprobeSession ffprobeSession) { + this.ffprobeSession = ffprobeSession; + this.executeCallback = ffprobeSession.getExecuteCallback(); } @Override - protected Integer doInBackground(final Void... unused) { - return FFprobeKit.execute(this.arguments); - } + public void run() { + FFmpegKitConfig.ffprobeExecute(ffprobeSession); - @Override - protected void onPostExecute(final Integer rc) { - if (ExecuteCallback != null) { - ExecuteCallback.apply(FFmpegKit.DEFAULT_EXECUTION_ID, rc); + if (executeCallback != null) { + executeCallback.apply(ffprobeSession); } } diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/AsyncGetMediaInformationTask.java b/android/app/src/main/java/com/arthenica/ffmpegkit/AsyncGetMediaInformationTask.java index 7a02a83..8b08350 100644 --- a/android/app/src/main/java/com/arthenica/ffmpegkit/AsyncGetMediaInformationTask.java +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/AsyncGetMediaInformationTask.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2018-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -19,29 +19,24 @@ package com.arthenica.ffmpegkit; -import android.os.AsyncTask; - /** - *

Utility class to get media information asynchronously. + *

Executes a MediaInformation session asynchronously. */ -public class AsyncGetMediaInformationTask extends AsyncTask { - private final String path; - private final GetMediaInformationCallback getMediaInformationCallback; +public class AsyncGetMediaInformationTask implements Runnable { + private final MediaInformationSession mediaInformationSession; + private final ExecuteCallback executeCallback; - public AsyncGetMediaInformationTask(final String path, final GetMediaInformationCallback getMediaInformationCallback) { - this.path = path; - this.getMediaInformationCallback = getMediaInformationCallback; + public AsyncGetMediaInformationTask(final MediaInformationSession mediaInformationSession) { + this.mediaInformationSession = mediaInformationSession; + this.executeCallback = mediaInformationSession.getExecuteCallback(); } @Override - protected MediaInformation doInBackground(final String... arguments) { - return FFprobeKit.getMediaInformation(path); - } + public void run() { + FFmpegKitConfig.getMediaInformationExecute(mediaInformationSession); - @Override - protected void onPostExecute(final MediaInformation mediaInformation) { - if (getMediaInformationCallback != null) { - getMediaInformationCallback.apply(mediaInformation); + if (executeCallback != null) { + executeCallback.apply(mediaInformationSession); } } diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/CameraSupport.java b/android/app/src/main/java/com/arthenica/ffmpegkit/CameraSupport.java index 530384f..cf127bc 100644 --- a/android/app/src/main/java/com/arthenica/ffmpegkit/CameraSupport.java +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/CameraSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2020 Taner Sener + * Copyright (c) 2019-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -34,7 +34,9 @@ import static android.content.Context.CAMERA_SERVICE; import static com.arthenica.ffmpegkit.FFmpegKitConfig.TAG; /** - * Utility class for camera devices. + *

Helper class to find camera devices supported. + *

Note that camera devices can only be detected on Android API Level 24+. On older API levels + * an empty list will be returned. */ class CameraSupport { diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/ExecuteCallback.java b/android/app/src/main/java/com/arthenica/ffmpegkit/ExecuteCallback.java index e2f334e..6e460bd 100644 --- a/android/app/src/main/java/com/arthenica/ffmpegkit/ExecuteCallback.java +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/ExecuteCallback.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2018-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -20,18 +20,16 @@ package com.arthenica.ffmpegkit; /** - *

Represents a callback function to receive an asynchronous execution result. + *

Callback function to receive execution results. */ @FunctionalInterface public interface ExecuteCallback { /** - *

Called when an asynchronous FFmpeg execution is completed. + *

Called when an execution is completed. * - * @param executionId id of the execution that completed - * @param returnCode return code of the execution completed, 0 on successful completion, 255 - * on user cancel, other non-zero codes on error + * @param session of with completed execution */ - void apply(long executionId, int returnCode); + void apply(final Session session); } diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/FFmpegKit.java b/android/app/src/main/java/com/arthenica/ffmpegkit/FFmpegKit.java index 690c894..9ccfc25 100644 --- a/android/app/src/main/java/com/arthenica/ffmpegkit/FFmpegKit.java +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/FFmpegKit.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2018-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -19,12 +19,9 @@ package com.arthenica.ffmpegkit; -import android.os.AsyncTask; - import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicLong; /** *

Main class for FFmpeg operations. Supports synchronous {@link #execute(String...)} and @@ -40,10 +37,6 @@ import java.util.concurrent.atomic.AtomicLong; */ public class FFmpegKit { - static final long DEFAULT_EXECUTION_ID = 0; - - private static final AtomicLong executionIdCounter = new AtomicLong(3000); - static { AbiDetect.class.getName(); FFmpegKitConfig.class.getName(); @@ -59,26 +52,50 @@ public class FFmpegKit { *

Synchronously executes FFmpeg with arguments provided. * * @param arguments FFmpeg command options/arguments as string array - * @return 0 on successful execution, 255 on user cancel, other non-zero codes on error + * @return ffmpeg session created for this execution */ - public static int execute(final String[] arguments) { - return FFmpegKitConfig.ffmpegExecute(DEFAULT_EXECUTION_ID, arguments); + public static FFmpegSession execute(final String[] arguments) { + final FFmpegSession session = new FFmpegSession(arguments, null, null, null); + + FFmpegKitConfig.ffmpegExecute(session); + + return session; } /** *

Asynchronously executes FFmpeg with arguments provided. * * @param arguments FFmpeg command options/arguments as string array - * @param executeCallback callback that will be notified when execution is completed - * @return returns a unique id that represents this execution + * @param executeCallback callback that will be notified when the execution is completed + * @return ffmpeg session created for this execution */ - public static long executeAsync(final String[] arguments, final ExecuteCallback executeCallback) { - final long newExecutionId = executionIdCounter.incrementAndGet(); + public static FFmpegSession executeAsync(final String[] arguments, + final ExecuteCallback executeCallback) { + final FFmpegSession session = new FFmpegSession(arguments, executeCallback, null, null); - AsyncFFmpegExecuteTask asyncFFmpegExecuteTask = new AsyncFFmpegExecuteTask(newExecutionId, arguments, executeCallback); - asyncFFmpegExecuteTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + FFmpegKitConfig.asyncFFmpegExecute(session); - return newExecutionId; + return session; + } + + /** + *

Asynchronously executes FFmpeg with arguments provided. + * + * @param arguments FFmpeg command options/arguments as string array + * @param executeCallback callback that will be notified when execution is completed + * @param logCallback callback that will receive log entries + * @param statisticsCallback callback that will receive statistics + * @return ffmpeg session created for this execution + */ + public static FFmpegSession executeAsync(final String[] arguments, + final ExecuteCallback executeCallback, + final LogCallback logCallback, + final StatisticsCallback statisticsCallback) { + final FFmpegSession session = new FFmpegSession(arguments, executeCallback, logCallback, statisticsCallback); + + FFmpegKitConfig.asyncFFmpegExecute(session); + + return session; } /** @@ -87,31 +104,40 @@ public class FFmpegKit { * @param arguments FFmpeg command options/arguments as string array * @param executeCallback callback that will be notified when execution is completed * @param executor executor that will be used to run this asynchronous operation - * @return returns a unique id that represents this execution + * @return ffmpeg session created for this execution */ - public static long executeAsync(final String[] arguments, final ExecuteCallback executeCallback, final Executor executor) { - final long newExecutionId = executionIdCounter.incrementAndGet(); + public static FFmpegSession executeAsync(final String[] arguments, + final ExecuteCallback executeCallback, + final Executor executor) { + final FFmpegSession session = new FFmpegSession(arguments, executeCallback, null, null); - AsyncFFmpegExecuteTask asyncFFmpegExecuteTask = new AsyncFFmpegExecuteTask(newExecutionId, arguments, executeCallback); - asyncFFmpegExecuteTask.executeOnExecutor(executor); + AsyncFFmpegExecuteTask asyncFFmpegExecuteTask = new AsyncFFmpegExecuteTask(session); + executor.execute(asyncFFmpegExecuteTask); - return newExecutionId; + return session; } /** - *

Synchronously executes FFmpeg command provided. Command is split into arguments using - * provided delimiter character. + *

Asynchronously executes FFmpeg with arguments provided. * - * @param command FFmpeg command - * @param delimiter delimiter used to split arguments - * @return 0 on successful execution, 255 on user cancel, other non-zero codes on error - * @since 3.0 - * @deprecated argument splitting mechanism used in this method is pretty simple and prone to - * errors. Consider using a more advanced method like {@link #execute(String)} or - * {@link #execute(String[])} + * @param arguments FFmpeg command options/arguments as string array + * @param executeCallback callback that will be notified when execution is completed + * @param logCallback callback that will receive log entries + * @param statisticsCallback callback that will receive statistics + * @param executor executor that will be used to run this asynchronous operation + * @return ffmpeg session created for this execution */ - public static int execute(final String command, final String delimiter) { - return execute((command == null) ? new String[]{""} : command.split((delimiter == null) ? " " : delimiter)); + public static FFmpegSession executeAsync(final String[] arguments, + final ExecuteCallback executeCallback, + final LogCallback logCallback, + final StatisticsCallback statisticsCallback, + final Executor executor) { + final FFmpegSession session = new FFmpegSession(arguments, executeCallback, logCallback, statisticsCallback); + + AsyncFFmpegExecuteTask asyncFFmpegExecuteTask = new AsyncFFmpegExecuteTask(session); + executor.execute(asyncFFmpegExecuteTask); + + return session; } /** @@ -120,9 +146,9 @@ public class FFmpegKit { * your command. * * @param command FFmpeg command - * @return 0 on successful execution, 255 on user cancel, other non-zero codes on error + * @return ffmpeg session created for this execution */ - public static int execute(final String command) { + public static FFmpegSession execute(final String command) { return execute(parseArguments(command)); } @@ -133,15 +159,29 @@ public class FFmpegKit { * * @param command FFmpeg command * @param executeCallback callback that will be notified when execution is completed - * @return returns a unique id that represents this execution + * @return ffmpeg session created for this execution */ - public static long executeAsync(final String command, final ExecuteCallback executeCallback) { - final long newExecutionId = executionIdCounter.incrementAndGet(); + public static FFmpegSession executeAsync(final String command, + final ExecuteCallback executeCallback) { + return executeAsync(parseArguments(command), executeCallback); + } - AsyncFFmpegExecuteTask asyncFFmpegExecuteTask = new AsyncFFmpegExecuteTask(newExecutionId, command, executeCallback); - asyncFFmpegExecuteTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - - return newExecutionId; + /** + *

Asynchronously executes FFmpeg command provided. Space character is used to split command + * into arguments. You can use single and double quote characters to specify arguments inside + * your command. + * + * @param command FFmpeg command + * @param executeCallback callback that will be notified when execution is completed + * @param logCallback callback that will receive log entries + * @param statisticsCallback callback that will receive statistics + * @return ffmpeg session created for this execution + */ + public static FFmpegSession executeAsync(final String command, + final ExecuteCallback executeCallback, + final LogCallback logCallback, + final StatisticsCallback statisticsCallback) { + return executeAsync(parseArguments(command), executeCallback, logCallback, statisticsCallback); } /** @@ -152,44 +192,76 @@ public class FFmpegKit { * @param command FFmpeg command * @param executeCallback callback that will be notified when execution is completed * @param executor executor that will be used to run this asynchronous operation - * @return returns a unique id that represents this execution + * @return ffmpeg session created for this execution */ - public static long executeAsync(final String command, final ExecuteCallback executeCallback, final Executor executor) { - final long newExecutionId = executionIdCounter.incrementAndGet(); + public static FFmpegSession executeAsync(final String command, + final ExecuteCallback executeCallback, + final Executor executor) { + final FFmpegSession session = new FFmpegSession(parseArguments(command), executeCallback, null, null); - AsyncFFmpegExecuteTask asyncFFmpegExecuteTask = new AsyncFFmpegExecuteTask(newExecutionId, command, executeCallback); - asyncFFmpegExecuteTask.executeOnExecutor(executor); + AsyncFFmpegExecuteTask asyncFFmpegExecuteTask = new AsyncFFmpegExecuteTask(session); + executor.execute(asyncFFmpegExecuteTask); - return newExecutionId; + return session; } /** - *

Cancels an ongoing operation. + *

Asynchronously executes FFmpeg command provided. Space character is used to split command + * into arguments. You can use single and double quote characters to specify arguments inside + * your command. + * + * @param command FFmpeg command + * @param executeCallback callback that will be notified when execution is completed + * @param logCallback callback that will receive log entries + * @param statisticsCallback callback that will receive statistics + * @param executor executor that will be used to run this asynchronous operation + * @return ffmpeg session created for this execution + */ + public static FFmpegSession executeAsync(final String command, + final ExecuteCallback executeCallback, + final LogCallback logCallback, + final StatisticsCallback statisticsCallback, + final Executor executor) { + final FFmpegSession session = new FFmpegSession(parseArguments(command), executeCallback, logCallback, statisticsCallback); + + AsyncFFmpegExecuteTask asyncFFmpegExecuteTask = new AsyncFFmpegExecuteTask(session); + executor.execute(asyncFFmpegExecuteTask); + + return session; + } + + /** + *

Cancels the last execution started. * *

This function does not wait for termination to complete and returns immediately. */ public static void cancel() { - FFmpegKitConfig.nativeFFmpegCancel(DEFAULT_EXECUTION_ID); + Session lastSession = FFmpegKitConfig.getLastSession(); + if (lastSession != null) { + FFmpegKitConfig.nativeFFmpegCancel(lastSession.getSessionId()); + } else { + android.util.Log.w(FFmpegKitConfig.TAG, "FFmpegKit cancel skipped. The last execution does not exist."); + } } /** - *

Cancels an ongoing operation. + *

Cancels the given execution. * *

This function does not wait for termination to complete and returns immediately. * - * @param executionId id of the execution + * @param sessionId id of the session that will be stopped */ - public static void cancel(final long executionId) { - FFmpegKitConfig.nativeFFmpegCancel(executionId); + public static void cancel(final long sessionId) { + FFmpegKitConfig.nativeFFmpegCancel(sessionId); } /** - *

Lists ongoing executions. + *

Lists all FFmpeg sessions in the session history * - * @return list of ongoing executions + * @return all FFmpeg sessions in the session history */ - public static List listExecutions() { - return FFmpegKitConfig.listFFmpegExecutions(); + public static List listSessions() { + return FFmpegKitConfig.getFFmpegSessions(); } /** diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/FFmpegKitConfig.java b/android/app/src/main/java/com/arthenica/ffmpegkit/FFmpegKitConfig.java index 54b8e15..17f0eef 100644 --- a/android/app/src/main/java/com/arthenica/ffmpegkit/FFmpegKitConfig.java +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/FFmpegKitConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2018-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -20,20 +20,38 @@ package com.arthenica.ffmpegkit; import android.content.Context; +import android.database.Cursor; +import android.net.Uri; import android.os.Build; -import android.util.Log; +import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract; +import android.util.SparseArray; + +import com.arthenica.smartexception.java.Exceptions; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; /** - *

This class is used to configure FFmpegKit library utilities/tools. + *

This class is used to configure FFmpegKit library and tools coming with it. * *

1. {@link LogCallback}: This class redirects FFmpeg/FFprobe output to Logcat by default. As * an alternative, it is possible not to print messages to Logcat and pass them to a @@ -53,34 +71,43 @@ import java.util.concurrent.atomic.AtomicReference; */ public class FFmpegKitConfig { - public static final int RETURN_CODE_SUCCESS = 0; - - public static final int RETURN_CODE_CANCEL = 255; - - private static int lastReturnCode = 0; - /** - * Defines tag used for logging. + * The tag used for logging. */ public static final String TAG = "ffmpeg-kit"; - public static final String FFMPEG_KIT_PIPE_PREFIX = "mf_pipe_"; + /** + * Prefix of named pipes created by ffmpeg kit. + */ + public static final String FFMPEG_KIT_NAMED_PIPE_PREFIX = "fk_pipe_"; - private static LogCallback logCallbackFunction; + /** + * Generates ids for named ffmpeg kit pipes. + */ + private static final AtomicLong pipeIndexGenerator; + private static final Map sessionHistoryMap; + private static final Queue sessionHistoryQueue; + private static final Object sessionHistoryLock; + private static int asyncConcurrencyLimit; + private static final SparseArray pfdMap; + /** + * Executor service for async executions. + */ + private static ExecutorService asyncExecutorService; + private static LogCallback globalLogCallbackFunction; + private static StatisticsCallback globalStatisticsCallbackFunction; + private static ExecuteCallback globalExecuteCallbackFunction; private static Level activeLogLevel; - private static StatisticsCallback statisticsCallbackFunction; - - private static Statistics lastReceivedStatistics; - - private static int lastCreatedPipeIndex; - - private static final List executions; + /* SESSION HISTORY VARIABLES */ + private static int sessionHistorySize; static { - Log.i(FFmpegKitConfig.TAG, "Loading ffmpeg-kit."); + Exceptions.registerRootPackage("com.arthenica"); + + android.util.Log.i(FFmpegKitConfig.TAG, "Loading ffmpeg-kit."); boolean nativeFFmpegLoaded = false; boolean nativeFFmpegTriedAndFailed = false; @@ -103,7 +130,7 @@ public class FFmpegKitConfig { System.loadLibrary("avdevice_neon"); nativeFFmpegLoaded = true; } catch (final UnsatisfiedLinkError e) { - Log.i(FFmpegKitConfig.TAG, "NEON supported armeabi-v7a ffmpeg library not found. Loading default armeabi-v7a library.", e); + android.util.Log.i(FFmpegKitConfig.TAG, String.format("NEON supported armeabi-v7a ffmpeg library not found. Loading default armeabi-v7a library.%s", Exceptions.getStackTraceString(e))); nativeFFmpegTriedAndFailed = true; } } @@ -134,9 +161,9 @@ public class FFmpegKitConfig { System.loadLibrary("ffmpegkit_armv7a_neon"); nativeFFmpegKitLoaded = true; - AbiDetect.setArmV7aNeonLoaded(true); + AbiDetect.setArmV7aNeonLoaded(); } catch (final UnsatisfiedLinkError e) { - Log.i(FFmpegKitConfig.TAG, "NEON supported armeabi-v7a ffmpegkit library not found. Loading default armeabi-v7a library.", e); + android.util.Log.i(FFmpegKitConfig.TAG, String.format("NEON supported armeabi-v7a ffmpegkit library not found. Loading default armeabi-v7a library.%s", Exceptions.getStackTraceString(e))); } } @@ -144,18 +171,29 @@ public class FFmpegKitConfig { System.loadLibrary("ffmpegkit"); } - Log.i(FFmpegKitConfig.TAG, String.format("Loaded ffmpeg-kit-%s-%s-%s-%s.", getPackageName(), AbiDetect.getAbi(), getVersion(), getBuildDate())); + android.util.Log.i(FFmpegKitConfig.TAG, String.format("Loaded ffmpeg-kit-%s-%s-%s-%s.", getPackageName(), AbiDetect.getAbi(), getVersion(), getBuildDate())); + + pipeIndexGenerator = new AtomicLong(1); + asyncConcurrencyLimit = 10; + asyncExecutorService = Executors.newFixedThreadPool(asyncConcurrencyLimit); /* NATIVE LOG LEVEL IS RECEIVED ONLY ON STARTUP */ activeLogLevel = Level.from(getNativeLogLevel()); - lastReceivedStatistics = new Statistics(); + sessionHistorySize = 10; + sessionHistoryMap = Collections.synchronizedMap(new LinkedHashMap() { + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return (this.size() > sessionHistorySize); + } + }); + sessionHistoryQueue = new LinkedList<>(); + sessionHistoryLock = new Object(); + + pfdMap = new SparseArray<>(); enableRedirection(); - - lastCreatedPipeIndex = 0; - - executions = Collections.synchronizedList(new ArrayList()); } /** @@ -187,55 +225,17 @@ public class FFmpegKitConfig { disableNativeRedirection(); } - /** - * Returns log level. - * - * @return log level - */ - public static Level getLogLevel() { - return activeLogLevel; - } - - /** - * Sets log level. - * - * @param level log level - */ - public static void setLogLevel(final Level level) { - if (level != null) { - activeLogLevel = level; - setNativeLogLevel(level.getValue()); - } - } - - /** - *

Sets a callback function to redirect FFmpeg/FFprobe logs. - * - * @param newLogCallback new log callback function or NULL to disable a previously defined callback - */ - public static void enableLogCallback(final LogCallback newLogCallback) { - logCallbackFunction = newLogCallback; - } - - /** - *

Sets a callback function to redirect FFmpeg statistics. - * - * @param statisticsCallback new statistics callback function or NULL to disable a previously defined callback - */ - public static void enableStatisticsCallback(final StatisticsCallback statisticsCallback) { - statisticsCallbackFunction = statisticsCallback; - } - /** *

Log redirection method called by JNI/native part. * - * @param executionId id of the execution that generated this log, 0 by default - * @param levelValue log level as defined in {@link Level} - * @param logMessage redirected log message + * @param sessionId id of the session that generated this log, 0 by default + * @param levelValue log level as defined in {@link Level} + * @param logMessage redirected log message */ - private static void log(final long executionId, final int levelValue, final byte[] logMessage) { + private static void log(final long sessionId, final int levelValue, final byte[] logMessage) { final Level level = Level.from(levelValue); final String text = new String(logMessage); + final Log log = new Log(sessionId, level, text); // AV_LOG_STDERR logs are always redirected if ((activeLogLevel == Level.AV_LOG_QUIET && levelValue != Level.AV_LOG_STDERR.getValue()) || levelValue > activeLogLevel.getValue()) { @@ -243,11 +243,23 @@ public class FFmpegKitConfig { return; } - if (logCallbackFunction != null) { + final Session session = getSession(sessionId); + if (session != null && session.getLogCallback() != null) { try { - logCallbackFunction.apply(new LogMessage(executionId, level, text)); + // NOTIFY SESSION CALLBACK IF DEFINED + session.getLogCallback().apply(log); } catch (final Exception e) { - Log.e(FFmpegKitConfig.TAG, "Exception thrown inside LogCallback block", e); + android.util.Log.e(FFmpegKitConfig.TAG, String.format("Exception thrown inside session LogCallback block.%s", Exceptions.getStackTraceString(e))); + } + } + + final LogCallback globalLogCallbackFunction = FFmpegKitConfig.globalLogCallbackFunction; + if (globalLogCallbackFunction != null) { + try { + // NOTIFY GLOBAL CALLBACK IF DEFINED + globalLogCallbackFunction.apply(log); + } catch (final Exception e) { + android.util.Log.e(FFmpegKitConfig.TAG, String.format("Exception thrown inside global LogCallback block.%s", Exceptions.getStackTraceString(e))); } } else { switch (level) { @@ -290,7 +302,7 @@ public class FFmpegKitConfig { /** *

Statistics redirection method called by JNI/native part. * - * @param executionId id of the execution that generated this statistics, 0 by default + * @param sessionId id of the session that generated this statistics, 0 by default * @param videoFrameNumber last processed frame number for videos * @param videoFps frames processed per second for videos * @param videoQuality quality of the video stream @@ -299,17 +311,28 @@ public class FFmpegKitConfig { * @param bitrate output bit rate in kbits/s * @param speed processing speed = processed duration / operation duration */ - private static void statistics(final long executionId, final int videoFrameNumber, + private static void statistics(final long sessionId, final int videoFrameNumber, final float videoFps, final float videoQuality, final long size, final int time, final double bitrate, final double speed) { - final Statistics newStatistics = new Statistics(executionId, videoFrameNumber, videoFps, videoQuality, size, time, bitrate, speed); - lastReceivedStatistics.update(newStatistics); + final Statistics newStatistics = new Statistics(sessionId, videoFrameNumber, videoFps, videoQuality, size, time, bitrate, speed); - if (statisticsCallbackFunction != null) { + final Session session = getSession(sessionId); + if (session != null && session.getStatisticsCallback() != null) { try { - statisticsCallbackFunction.apply(lastReceivedStatistics); + // NOTIFY SESSION CALLBACK IF DEFINED + session.getStatisticsCallback().apply(newStatistics); } catch (final Exception e) { - Log.e(FFmpegKitConfig.TAG, "Exception thrown inside StatisticsCallback block", e); + android.util.Log.e(FFmpegKitConfig.TAG, String.format("Exception thrown inside session StatisticsCallback block.%s", Exceptions.getStackTraceString(e))); + } + } + + final StatisticsCallback globalStatisticsCallbackFunction = FFmpegKitConfig.globalStatisticsCallbackFunction; + if (globalStatisticsCallbackFunction != null) { + try { + // NOTIFY GLOBAL CALLBACK IF DEFINED + globalStatisticsCallbackFunction.apply(newStatistics); + } catch (final Exception e) { + android.util.Log.e(FFmpegKitConfig.TAG, String.format("Exception thrown inside global StatisticsCallback block.%s", Exceptions.getStackTraceString(e))); } } } @@ -317,17 +340,15 @@ public class FFmpegKitConfig { /** *

Returns the last received statistics data. * - * @return last received statistics data + * @return last received statistics data or null if no statistics data is available */ public static Statistics getLastReceivedStatistics() { - return lastReceivedStatistics; - } - - /** - *

Resets last received statistics. It is recommended to call it before starting a new execution. - */ - public static void resetStatistics() { - lastReceivedStatistics = new Statistics(); + final Session lastSession = getLastSession(); + if (lastSession != null) { + return lastSession.getStatistics().peek(); + } else { + return null; + } } /** @@ -341,14 +362,15 @@ public class FFmpegKitConfig { } /** - *

Registers fonts inside the given path, so they are available to use in FFmpeg filters. + *

Registers fonts inside the given path, so they become available to use in FFmpeg filters. * *

Note that you need to build FFmpegKit with fontconfig * enabled or use a prebuilt package with fontconfig inside to use this feature. * * @param context application context to access application data * @param fontDirectoryPath directory which contains fonts (.ttf and .otf files) - * @param fontNameMapping custom font name mappings, useful to access your fonts with more friendly names + * @param fontNameMapping custom font name mappings, useful to access your fonts with more + * friendly names */ public static void setFontDirectory(final Context context, final String fontDirectoryPath, final Map fontNameMapping) { final File cacheDir = context.getCacheDir(); @@ -357,13 +379,13 @@ public class FFmpegKitConfig { final File tempConfigurationDirectory = new File(cacheDir, ".ffmpegkit"); if (!tempConfigurationDirectory.exists()) { boolean tempFontConfDirectoryCreated = tempConfigurationDirectory.mkdirs(); - Log.d(TAG, String.format("Created temporary font conf directory: %s.", tempFontConfDirectoryCreated)); + android.util.Log.d(TAG, String.format("Created temporary font conf directory: %s.", tempFontConfDirectoryCreated)); } final File fontConfiguration = new File(tempConfigurationDirectory, "fonts.conf"); if (fontConfiguration.exists()) { boolean fontConfigurationDeleted = fontConfiguration.delete(); - Log.d(TAG, String.format("Deleted old temporary font configuration: %s.", fontConfigurationDeleted)); + android.util.Log.d(TAG, String.format("Deleted old temporary font configuration: %s.", fontConfigurationDeleted)); } /* PROCESS MAPPINGS FIRST */ @@ -405,14 +427,14 @@ public class FFmpegKitConfig { outputStream.write(fontConfig.getBytes()); outputStream.flush(); - Log.d(TAG, String.format("Saved new temporary font configuration with %d font name mappings.", validFontNameMappingCount)); + android.util.Log.d(TAG, String.format("Saved new temporary font configuration with %d font name mappings.", validFontNameMappingCount)); setFontconfigConfigurationPath(tempConfigurationDirectory.getAbsolutePath()); - Log.d(TAG, String.format("Font directory %s registered successfully.", fontDirectoryPath)); + android.util.Log.d(TAG, String.format("Font directory %s registered successfully.", fontDirectoryPath)); } catch (final IOException e) { - Log.e(TAG, String.format("Failed to set font directory: %s.", fontDirectoryPath), e); + android.util.Log.e(TAG, String.format("Failed to set font directory: %s.%s", fontDirectoryPath, Exceptions.getStackTraceString(e))); } finally { if (reference.get() != null) { try { @@ -425,20 +447,18 @@ public class FFmpegKitConfig { } /** - *

Returns package name. + *

Returns FFmpegKit package name. * - * @return guessed package name according to supported external libraries - * @since 3.0 + * @return FFmpegKit package name */ public static String getPackageName() { return Packages.getPackageName(); } /** - *

Returns supported external libraries. + *

Returns the list of supported external libraries. * * @return list of supported external libraries - * @since 3.0 */ public static List getExternalLibraries() { return Packages.getExternalLibraries(); @@ -450,14 +470,14 @@ public class FFmpegKitConfig { *

Please note that creator is responsible of closing created pipes. * * @param context application context - * @return the full path of named pipe + * @return the full path of the named pipe */ public static String registerNewFFmpegPipe(final Context context) { // PIPES ARE CREATED UNDER THE CACHE DIRECTORY final File cacheDir = context.getCacheDir(); - final String newFFmpegPipePath = cacheDir + File.separator + FFMPEG_KIT_PIPE_PREFIX + (++lastCreatedPipeIndex); + final String newFFmpegPipePath = MessageFormat.format("{0}{1}{2}{3}", cacheDir, File.separator, FFMPEG_KIT_NAMED_PIPE_PREFIX, pipeIndexGenerator.getAndIncrement()); // FIRST CLOSE OLD PIPES WITH THE SAME NAME closeFFmpegPipe(newFFmpegPipePath); @@ -466,7 +486,7 @@ public class FFmpegKitConfig { if (rc == 0) { return newFFmpegPipePath; } else { - Log.e(TAG, String.format("Failed to register new FFmpeg pipe %s. Operation failed with rc=%d.", newFFmpegPipePath, rc)); + android.util.Log.e(TAG, String.format("Failed to register new FFmpeg pipe %s. Operation failed with rc=%d.", newFFmpegPipePath, rc)); return null; } } @@ -477,7 +497,7 @@ public class FFmpegKitConfig { * @param ffmpegPipePath full path of ffmpeg pipe */ public static void closeFFmpegPipe(final String ffmpegPipePath) { - File file = new File(ffmpegPipePath); + final File file = new File(ffmpegPipePath); if (file.exists()) { file.delete(); } @@ -486,8 +506,11 @@ public class FFmpegKitConfig { /** * Returns the list of camera ids supported. * + *

Note that this method requires API Level >= 24. On older API levels it returns an empty + * list. + * * @param context application context - * @return the list of camera ids supported or an empty list if no supported camera is found + * @return the list of camera ids supported or an empty list if no supported cameras are found */ public static List getSupportedCameraIds(final Context context) { final List detectedCameraIdList = new ArrayList<>(); @@ -522,9 +545,9 @@ public class FFmpegKitConfig { } /** - *

Returns whether FFmpegKit release is a long term release or not. + *

Returns whether FFmpegKit release is a Long Term Release or not. * - * @return YES or NO + * @return true/yes or false/no */ public static boolean isLTSBuild() { return AbiDetect.isNativeLTSBuild(); @@ -540,63 +563,64 @@ public class FFmpegKitConfig { } /** - *

Returns return code of last executed command. + *

Returns the return code of the last completed execution. * - * @return return code of last executed command - * @since 3.0 + * @return return code of the last completed execution */ public static int getLastReturnCode() { - return lastReturnCode; + final Session lastSession = getLastSession(); + if (lastSession != null) { + return lastSession.getReturnCode(); + } else { + return 0; + } } /** - *

Returns log output of last executed single FFmpeg/FFprobe command. + *

Returns the log output of the last executed FFmpeg/FFprobe command. * - *

This method does not support executing multiple concurrent commands. If you execute - * multiple commands at the same time, this method will return output from all executions. - * - *

Please note that disabling redirection using {@link FFmpegKitConfig#disableRedirection()} method - * also disables this functionality. + *

Please note that disabling redirection using {@link FFmpegKitConfig#disableRedirection()} + * method also disables this functionality. * * @return output of the last executed command - * @since 3.0 */ public static String getLastCommandOutput() { - String nativeLastCommandOutput = getNativeLastCommandOutput(); - if (nativeLastCommandOutput != null) { + final Session lastSession = getLastSession(); + if (lastSession != null) { // REPLACING CH(13) WITH CH(10) - nativeLastCommandOutput = nativeLastCommandOutput.replace('\r', '\n'); + return lastSession.getLogsAsString().replace('\r', '\n'); + } else { + return ""; } - return nativeLastCommandOutput; } /** *

Prints the output of the last executed FFmpeg/FFprobe command to the Logcat at the * specified priority. * - *

This method does not support executing multiple concurrent commands. If you execute - * multiple commands at the same time, this method will print output from all executions. - * - * @param logPriority one of {@link Log#VERBOSE}, {@link Log#DEBUG}, {@link Log#INFO}, - * {@link Log#WARN}, {@link Log#ERROR}, {@link Log#ASSERT} - * @since 4.3 + * @param logPriority one of {@link android.util.Log#VERBOSE}, + * {@link android.util.Log#DEBUG}, + * {@link android.util.Log#INFO}, + * {@link android.util.Log#WARN}, + * {@link android.util.Log#ERROR}, + * {@link android.util.Log#ASSERT} */ - public static void printLastCommandOutput(int logPriority) { + public static void printLastCommandOutput(final int logPriority) { final int LOGGER_ENTRY_MAX_LEN = 4 * 1000; String buffer = getLastCommandOutput(); do { if (buffer.length() <= LOGGER_ENTRY_MAX_LEN) { - Log.println(logPriority, FFmpegKitConfig.TAG, buffer); + android.util.Log.println(logPriority, FFmpegKitConfig.TAG, buffer); buffer = ""; } else { final int index = buffer.substring(0, LOGGER_ENTRY_MAX_LEN).lastIndexOf('\n'); if (index < 0) { - Log.println(logPriority, FFmpegKitConfig.TAG, buffer.substring(0, LOGGER_ENTRY_MAX_LEN)); + android.util.Log.println(logPriority, FFmpegKitConfig.TAG, buffer.substring(0, LOGGER_ENTRY_MAX_LEN)); buffer = buffer.substring(LOGGER_ENTRY_MAX_LEN); } else { - Log.println(logPriority, FFmpegKitConfig.TAG, buffer.substring(0, index)); + android.util.Log.println(logPriority, FFmpegKitConfig.TAG, buffer.substring(0, index)); buffer = buffer.substring(index); } } @@ -624,43 +648,409 @@ public class FFmpegKitConfig { } /** - *

Synchronously executes FFmpeg with arguments provided. + *

Synchronously executes the ffmpeg session provided. * - * @param executionId id of the execution - * @param arguments FFmpeg command options/arguments as string array - * @return zero on successful execution, 255 on user cancel and non-zero on error + * @param ffmpegSession FFmpeg session which includes command options/arguments */ - static int ffmpegExecute(final long executionId, final String[] arguments) { - final FFmpegExecution currentFFmpegExecution = new FFmpegExecution(executionId, arguments); - executions.add(currentFFmpegExecution); + static void ffmpegExecute(final FFmpegSession ffmpegSession) { + addSession(ffmpegSession); + ffmpegSession.startRunning(); try { - final int lastReturnCode = nativeFFmpegExecute(executionId, arguments); - - FFmpegKitConfig.setLastReturnCode(lastReturnCode); - - return lastReturnCode; - } finally { - executions.remove(currentFFmpegExecution); + final int returnCode = nativeFFmpegExecute(ffmpegSession.getSessionId(), ffmpegSession.getArguments()); + ffmpegSession.complete(returnCode); + } catch (final Exception e) { + ffmpegSession.fail(e); + android.util.Log.w(FFmpegKitConfig.TAG, String.format("FFmpeg execute failed: %s.%s", FFmpegKit.argumentsToString(ffmpegSession.getArguments()), Exceptions.getStackTraceString(e))); } } /** - * Updates return code value for the last executed command. + *

Synchronously executes the ffprobe session provided. * - * @param newLastReturnCode new last return code value + * @param ffprobeSession FFprobe session which includes command options/arguments */ - static void setLastReturnCode(int newLastReturnCode) { - lastReturnCode = newLastReturnCode; + static void ffprobeExecute(final FFprobeSession ffprobeSession) { + addSession(ffprobeSession); + ffprobeSession.startRunning(); + + try { + final int returnCode = nativeFFprobeExecute(ffprobeSession.getSessionId(), ffprobeSession.getArguments()); + ffprobeSession.complete(returnCode); + } catch (final Exception e) { + ffprobeSession.fail(e); + android.util.Log.w(FFmpegKitConfig.TAG, String.format("FFprobe execute failed: %s.%s", FFmpegKit.argumentsToString(ffprobeSession.getArguments()), Exceptions.getStackTraceString(e))); + } } /** - *

Lists ongoing FFmpeg executions. + *

Synchronously executes the media information session provided. * - * @return list of ongoing FFmpeg executions + * @param mediaInformationSession media information session which includes command options/arguments */ - static List listFFmpegExecutions() { - return new ArrayList<>(executions); + static void getMediaInformationExecute(final MediaInformationSession mediaInformationSession) { + addSession(mediaInformationSession); + mediaInformationSession.startRunning(); + + try { + final int returnCode = nativeFFprobeExecute(mediaInformationSession.getSessionId(), mediaInformationSession.getArguments()); + mediaInformationSession.complete(returnCode); + if (returnCode == ReturnCode.SUCCESS) { + MediaInformation mediaInformation = MediaInformationParser.fromWithError(mediaInformationSession.getLogsAsString()); + mediaInformationSession.setMediaInformation(mediaInformation); + } + } catch (final Exception e) { + mediaInformationSession.fail(e); + android.util.Log.w(FFmpegKitConfig.TAG, String.format("Get media information execute failed: %s.%s", FFmpegKit.argumentsToString(mediaInformationSession.getArguments()), Exceptions.getStackTraceString(e))); + } + } + + /** + *

Asynchronously executes the ffmpeg session provided. + * + * @param ffmpegSession FFmpeg session which includes command options/arguments + */ + static void asyncFFmpegExecute(final FFmpegSession ffmpegSession) { + AsyncFFmpegExecuteTask asyncFFmpegExecuteTask = new AsyncFFmpegExecuteTask(ffmpegSession); + Future future = asyncExecutorService.submit(asyncFFmpegExecuteTask); + ffmpegSession.setFuture(future); + } + + /** + *

Asynchronously executes the ffprobe session provided. + * + * @param ffprobeSession FFprobe session which includes command options/arguments + */ + static void asyncFFprobeExecute(final FFprobeSession ffprobeSession) { + AsyncFFprobeExecuteTask asyncFFmpegExecuteTask = new AsyncFFprobeExecuteTask(ffprobeSession); + Future future = asyncExecutorService.submit(asyncFFmpegExecuteTask); + ffprobeSession.setFuture(future); + } + + /** + *

Asynchronously executes the media information session provided. + * + * @param mediaInformationSession media information session which includes command options/arguments + */ + static void asyncGetMediaInformationExecute(final MediaInformationSession mediaInformationSession) { + AsyncGetMediaInformationTask asyncGetMediaInformationTask = new AsyncGetMediaInformationTask(mediaInformationSession); + Future future = asyncExecutorService.submit(asyncGetMediaInformationTask); + mediaInformationSession.setFuture(future); + } + + /** + * Returns the maximum number of async operations that will be executed in parallel. + * + * @return maximum number of async operations that will be executed in parallel + */ + public static int getAsyncConcurrencyLimit() { + return asyncConcurrencyLimit; + } + + /** + * Sets the maximum number of async operations that will be executed in parallel. If more + * operations are submitted those will be queued. + * + * @param asyncConcurrencyLimit new async concurrency limit + */ + public static void setAsyncConcurrencyLimit(final int asyncConcurrencyLimit) { + + if (asyncConcurrencyLimit > 0) { + + /* SET THE NEW LIMIT */ + FFmpegKitConfig.asyncConcurrencyLimit = asyncConcurrencyLimit; + ExecutorService oldAsyncExecutorService = FFmpegKitConfig.asyncExecutorService; + + /* CREATE THE NEW ASYNC THREAD POOL */ + FFmpegKitConfig.asyncExecutorService = Executors.newFixedThreadPool(asyncConcurrencyLimit); + + /* STOP THE OLD ASYNC THREAD POOL */ + oldAsyncExecutorService.shutdown(); + } + } + + /** + *

Sets a global callback function to redirect FFmpeg/FFprobe logs. + * + * @param newLogCallback new log callback function or null to disable a previously defined + * callback + */ + public static void enableLogCallback(final LogCallback newLogCallback) { + globalLogCallbackFunction = newLogCallback; + } + + /** + *

Sets a global callback function to redirect FFmpeg statistics. + * + * @param statisticsCallback new statistics callback function or null to disable a previously + * defined callback + */ + public static void enableStatisticsCallback(final StatisticsCallback statisticsCallback) { + globalStatisticsCallbackFunction = statisticsCallback; + } + + /** + *

Sets a global callback function to receive execution results. + * + * @param executeCallback new execute callback function or null to disable a previously + * defined callback + */ + public static void enableExecuteCallback(final ExecuteCallback executeCallback) { + globalExecuteCallbackFunction = executeCallback; + } + + /** + *

Returns global execute callback function. + * + * @return global execute callback function + */ + public static ExecuteCallback getGlobalExecuteCallbackFunction() { + return globalExecuteCallbackFunction; + } + + /** + * Returns the current log level. + * + * @return current log level + */ + public static Level getLogLevel() { + return activeLogLevel; + } + + /** + * Sets the log level. + * + * @param level new log level + */ + public static void setLogLevel(final Level level) { + if (level != null) { + activeLogLevel = level; + setNativeLogLevel(level.getValue()); + } + } + + /** + *

Converts the given Structured Access Framework Uri ("content:…") into an + * input/output url that can be used in FFmpegKit and FFprobeKit. + * + * @return input/output url that can be passed to FFmpegKit or FFprobeKit + */ + private static String getSafParameter(final Context context, final Uri uri, final String openMode) { + String displayName = "unknown"; + try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + displayName = cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME)); + } + } catch (final Throwable t) { + android.util.Log.e(TAG, String.format("Failed to get %s column for %s.%s", DocumentsContract.Document.COLUMN_DISPLAY_NAME, uri.toString(), Exceptions.getStackTraceString(t))); + } + + int fd = -1; + try { + ParcelFileDescriptor parcelFileDescriptor = context.getContentResolver().openFileDescriptor(uri, openMode); + fd = parcelFileDescriptor.getFd(); + pfdMap.put(fd, parcelFileDescriptor); + } catch (final Throwable t) { + android.util.Log.e(TAG, String.format("Failed to obtain %s parcelFileDescriptor for %s.%s", openMode, uri.toString(), Exceptions.getStackTraceString(t))); + } + + // workaround for https://issuetracker.google.com/issues/162440528: ANDROID_CREATE_DOCUMENT generating file names like "transcode.mp3 (2)" + if (displayName.lastIndexOf('.') > 0 && displayName.lastIndexOf(' ') > displayName.lastIndexOf('.')) { + String extension = displayName.substring(displayName.lastIndexOf('.'), displayName.lastIndexOf(' ')); + displayName += extension; + } + // spaces can break argument list parsing, see https://github.com/alexcohn/mobile-ffmpeg/pull/1#issuecomment-688643836 + final char NBSP = (char) 0xa0; + return "saf:" + fd + "/" + displayName.replace(' ', NBSP); + } + + /** + *

Converts the given Structured Access Framework Uri ("content:…") into an + * input url that can be used in FFmpegKit and FFprobeKit. + * + * @return input url that can be passed to FFmpegKit or FFprobeKit + */ + public static String getSafParameterForRead(final Context context, final Uri uri) { + return getSafParameter(context, uri, "r"); + } + + /** + *

Converts the given Structured Access Framework Uri ("content:…") into an + * output url that can be used in FFmpegKit and FFprobeKit. + * + * @return output url that can be passed to FFmpegKit or FFprobeKit + */ + public static String getSafParameterForWrite(final Context context, final Uri uri) { + return getSafParameter(context, uri, "w"); + } + + /** + * Called by saf_wrapper from JNI/native part to close a parcel file descriptor. + * + * @param fd parcel file descriptor created for a saf uri + */ + private static void closeParcelFileDescriptor(final int fd) { + try { + ParcelFileDescriptor pfd = pfdMap.get(fd); + if (pfd != null) { + pfd.close(); + pfdMap.delete(fd); + } + } catch (final Throwable t) { + android.util.Log.e(TAG, String.format("Failed to close file descriptor %d.%s", fd, Exceptions.getStackTraceString(t))); + } + } + + /** + * Returns the session history size. + * + * @return session history size + */ + public static int getSessionHistorySize() { + return sessionHistorySize; + } + + /** + * Sets the session history size. + * + * @param sessionHistorySize new session history size + */ + public static void setSessionHistorySize(int sessionHistorySize) { + FFmpegKitConfig.sessionHistorySize = sessionHistorySize; + } + + /** + * Adds a session to the session history. + * + * @param session new session + */ + static void addSession(final Session session) { + synchronized (sessionHistoryLock) { + sessionHistoryMap.put(session.getSessionId(), session); + sessionHistoryQueue.offer(session); + + if (sessionHistoryQueue.size() > sessionHistorySize) { + final Session oldestElement = sessionHistoryQueue.poll(); + if (oldestElement != null) { + sessionHistoryMap.remove(oldestElement.getSessionId()); + } + } + } + } + + /** + * Returns the session specified with sessionId from the session history. + * + * @param sessionId session identifier + * @return session specified with sessionId or null if it is not found in the history + */ + public static Session getSession(final long sessionId) { + synchronized (sessionHistoryLock) { + return sessionHistoryMap.get(sessionId); + } + } + + /** + * Returns the last session from the session history. + * + * @return the last session from the session history + */ + public static Session getLastSession() { + synchronized (sessionHistoryLock) { + return sessionHistoryQueue.peek(); + } + } + + /** + *

Returns all sessions in the session history. + * + * @return all sessions in the session history + */ + public static List getSessions() { + synchronized (sessionHistoryLock) { + return new LinkedList<>(sessionHistoryQueue); + } + } + + /** + *

Returns all FFmpeg sessions in the session history. + * + * @return all FFmpeg sessions in the session history + */ + static List getFFmpegSessions() { + synchronized (sessionHistoryLock) { + return sessionHistoryQueue.stream().filter(new Predicate() { + + @Override + public boolean test(final Session session) { + return (session.isFFmpeg()); + } + }).map(new Function() { + + @Override + public FFmpegSession apply(final Session session) { + return (FFmpegSession) session; + } + }).collect(Collectors.toCollection(new Supplier>() { + + @Override + public List get() { + return new LinkedList<>(); + } + })); + } + } + + /** + *

Returns all FFprobe sessions in the session history. + * + * @return all FFprobe sessions in the session history + */ + static List getFFprobeSessions() { + synchronized (sessionHistoryLock) { + return sessionHistoryQueue.stream().filter(new Predicate() { + + @Override + public boolean test(final Session session) { + return (session.isFFprobe()); + } + }).map(new Function() { + + @Override + public FFprobeSession apply(final Session session) { + return (FFprobeSession) session; + } + }).collect(Collectors.toCollection(new Supplier>() { + + @Override + public List get() { + return new LinkedList<>(); + } + })); + } + } + + /** + *

Returns sessions that have the given state. + * + * @return sessions that have the given state from the session history + */ + public static List getSessionsByState(final SessionState state) { + synchronized (sessionHistoryLock) { + return sessionHistoryQueue.stream().filter(new Predicate() { + + @Override + public boolean test(final Session session) { + return (session.getState() == state); + } + }).collect(Collectors.toCollection(new Supplier>() { + + @Override + public List get() { + return new LinkedList<>(); + } + })); + } } /** @@ -673,13 +1063,6 @@ public class FFmpegKitConfig { */ private static native void disableNativeRedirection(); - /** - * Sets native log level - * - * @param level log level - */ - private static native void setNativeLogLevel(int level); - /** * Returns native log level. * @@ -687,6 +1070,13 @@ public class FFmpegKitConfig { */ private static native int getNativeLogLevel(); + /** + * Sets native log level + * + * @param level log level + */ + private static native void setNativeLogLevel(int level); + /** *

Returns FFmpeg version bundled within the library natively. * @@ -702,32 +1092,33 @@ public class FFmpegKitConfig { private native static String getNativeVersion(); /** - *

Synchronously executes FFmpeg natively with arguments provided. + *

Synchronously executes FFmpeg natively. * - * @param executionId id of the execution - * @param arguments FFmpeg command options/arguments as string array + * @param sessionId id of the session + * @param arguments FFmpeg command options/arguments as string array * @return zero on successful execution, 255 on user cancel and non-zero on error */ - private native static int nativeFFmpegExecute(final long executionId, final String[] arguments); + private native static int nativeFFmpegExecute(final long sessionId, final String[] arguments); /** *

Cancels an ongoing FFmpeg operation natively. This function does not wait for termination * to complete and returns immediately. * - * @param executionId id of the execution + * @param sessionId id of the session */ - native static void nativeFFmpegCancel(final long executionId); + native static void nativeFFmpegCancel(final long sessionId); /** - *

Synchronously executes FFprobe natively with arguments provided. + *

Synchronously executes FFprobe natively. * + * @param sessionId id of the session * @param arguments FFprobe command options/arguments as string array * @return zero on successful execution, 255 on user cancel and non-zero on error */ - native static int nativeFFprobeExecute(final String[] arguments); + native static int nativeFFprobeExecute(final long sessionId, final String[] arguments); /** - *

Creates natively a new named pipe to use in FFmpeg operations. + *

Creates a new named pipe to use in FFmpeg operations natively. * *

Please note that creator is responsible of closing created pipes. * @@ -752,13 +1143,6 @@ public class FFmpegKitConfig { */ private native static int setNativeEnvironmentVariable(final String variableName, final String variableValue); - /** - *

Returns log output of the last executed single command natively. - * - * @return output of the last executed single command - */ - private native static String getNativeLastCommandOutput(); - /** *

Registers a new ignored signal natively. Ignored signals are not handled by the library. * diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/FFmpegSession.java b/android/app/src/main/java/com/arthenica/ffmpegkit/FFmpegSession.java new file mode 100644 index 0000000..b4d388d --- /dev/null +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/FFmpegSession.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2020-2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +package com.arthenica.ffmpegkit; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.stream.Stream; + +/** + *

An FFmpeg execute session. + */ +public class FFmpegSession extends AbstractSession implements Session { + private final Queue statistics; + + public FFmpegSession(final String[] arguments, + final ExecuteCallback executeCallback, + final LogCallback logCallback, + final StatisticsCallback statisticsCallback) { + super(arguments, executeCallback, logCallback, statisticsCallback); + + this.statistics = new ConcurrentLinkedQueue<>(); + } + + public Queue getStatistics() { + return statistics; + } + + public Stream getStatisticsAsStream() { + return statistics.stream(); + } + + public void addStatistics(final Statistics statistics) { + this.statistics.add(statistics); + } + + @Override + public boolean isFFmpeg() { + return true; + } + + @Override + public boolean isFFprobe() { + return false; + } + + @Override + public String toString() { + final StringBuilder stringBuilder = new StringBuilder(); + + stringBuilder.append("FFmpegSession{"); + stringBuilder.append("sessionId="); + stringBuilder.append(sessionId); + stringBuilder.append(", createTime="); + stringBuilder.append(createTime); + stringBuilder.append(", startTime="); + stringBuilder.append(startTime); + stringBuilder.append(", endTime="); + stringBuilder.append(endTime); + stringBuilder.append(", arguments="); + stringBuilder.append(FFmpegKit.argumentsToString(arguments)); + stringBuilder.append(", logs="); + stringBuilder.append(getLogsAsString()); + stringBuilder.append(", state="); + stringBuilder.append(state); + stringBuilder.append(", returnCode="); + stringBuilder.append(returnCode); + stringBuilder.append(", failStackTrace="); + stringBuilder.append('\''); + stringBuilder.append(failStackTrace); + stringBuilder.append('\''); + stringBuilder.append('}'); + + return stringBuilder.toString(); + } + +} diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/FFprobeKit.java b/android/app/src/main/java/com/arthenica/ffmpegkit/FFprobeKit.java index d65bc2b..d3d7b81 100644 --- a/android/app/src/main/java/com/arthenica/ffmpegkit/FFprobeKit.java +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/FFprobeKit.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Taner Sener + * Copyright (c) 2020-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -19,14 +19,19 @@ package com.arthenica.ffmpegkit; -import android.util.Log; +import java.util.concurrent.Executor; /** - *

Main class for FFprobe operations. Provides {@link #execute(String...)} method to execute - * FFprobe commands. + *

Main class for FFprobe operations. + *

Supports running FFprobe commands using {@link #execute(String...)} method. *

- *      int rc = FFprobe.execute("-hide_banner -v error -show_entries format=size -of default=noprint_wrappers=1 file1.mp4");
- *      Log.i(Config.TAG, String.format("Command execution %s.", (rc == 0?"completed successfully":"failed with rc=" + rc));
+ *      FFprobeSession session = FFprobe.execute("-hide_banner -v error -show_entries format=size -of default=noprint_wrappers=1 file1.mp4");
+ *      Log.i(FFmpegKitConfig.TAG, String.format("Command execution %s.", (session.getReturnCode() == 0?"completed successfully":"failed with rc=" + session.getReturnCode()));
+ * 
+ *

It can also extract media information for a file or a url, using {@link #getMediaInformation(String)} method. + *

+ *      MediaInformationSession session = FFprobe.getMediaInformation("file1.mp4");
+ *      Log.i(FFmpegKitConfig.TAG, String.format("Media information %s.", (session.getReturnCode() == 0?"extracted successfully":"was not extracted due to rc=" + session.getReturnCode()));
  * 
*/ public class FFprobeKit { @@ -46,14 +51,14 @@ public class FFprobeKit { *

Synchronously executes FFprobe with arguments provided. * * @param arguments FFprobe command options/arguments as string array - * @return zero on successful execution, 255 on user cancel and non-zero on error + * @return ffprobe session created for this execution */ - public static int execute(final String[] arguments) { - final int lastReturnCode = FFmpegKitConfig.nativeFFprobeExecute(arguments); + public static FFprobeSession execute(final String[] arguments) { + final FFprobeSession session = new FFprobeSession(arguments, null, null, null); - FFmpegKitConfig.setLastReturnCode(lastReturnCode); + FFmpegKitConfig.ffprobeExecute(session); - return lastReturnCode; + return session; } /** @@ -62,69 +67,246 @@ public class FFprobeKit { * your command. * * @param command FFprobe command - * @return zero on successful execution, 255 on user cancel and non-zero on error + * @return ffprobe session created for this execution */ - public static int execute(final String command) { + public static FFprobeSession execute(final String command) { return execute(FFmpegKit.parseArguments(command)); } /** - *

Returns media information for the given file. + *

Asynchronously executes FFprobe command provided. Space character is used to split command + * into arguments. You can use single and double quote characters to specify arguments inside + * your command. * - *

This method does not support executing multiple concurrent operations. If you execute - * multiple operations (execute or getMediaInformation) at the same time, the response of this - * method is not predictable. - * - * @param path path or uri of media file - * @return media information - * @since 3.0 + * @param command FFprobe command + * @param executeCallback callback that will be notified when the execution is completed + * @return ffprobe session created for this execution */ - public static MediaInformation getMediaInformation(final String path) { - return getMediaInformationFromCommandArguments(new String[]{"-v", "error", "-hide_banner", "-print_format", "json", "-show_format", "-show_streams", "-i", path}); + public static FFprobeSession executeAsync(final String command, + final ExecuteCallback executeCallback) { + return executeAsync(FFmpegKit.parseArguments(command), executeCallback); + } + + /** + *

Asynchronously executes FFprobe with arguments provided. + * + * @param arguments FFprobe command options/arguments as string array + * @param executeCallback callback that will be notified when the execution is completed + * @return ffprobe session created for this execution + */ + public static FFprobeSession executeAsync(final String[] arguments, + final ExecuteCallback executeCallback) { + final FFprobeSession session = new FFprobeSession(arguments, executeCallback, null, null); + + FFmpegKitConfig.asyncFFprobeExecute(session); + + return session; + } + + /** + *

Asynchronously executes FFprobe command provided. Space character is used to split command + * into arguments. You can use single and double quote characters to specify arguments inside + * your command. + * + * @param command FFprobe command + * @param executeCallback callback that will be notified when execution is completed + * @param logCallback callback that will receive log entries + * @param statisticsCallback callback that will receive statistics + * @return ffprobe session created for this execution + */ + public static FFprobeSession executeAsync(final String command, + final ExecuteCallback executeCallback, + final LogCallback logCallback, + final StatisticsCallback statisticsCallback) { + return executeAsync(FFmpegKit.parseArguments(command), executeCallback, logCallback, statisticsCallback); + } + + /** + *

Asynchronously executes FFprobe with arguments provided. + * + * @param arguments FFprobe command options/arguments as string array + * @param executeCallback callback that will be notified when execution is completed + * @param logCallback callback that will receive log entries + * @param statisticsCallback callback that will receive statistics + * @return ffprobe session created for this execution + */ + public static FFprobeSession executeAsync(final String[] arguments, + final ExecuteCallback executeCallback, + final LogCallback logCallback, + final StatisticsCallback statisticsCallback) { + final FFprobeSession session = new FFprobeSession(arguments, executeCallback, logCallback, statisticsCallback); + + FFmpegKitConfig.asyncFFprobeExecute(session); + + return session; + } + + /** + *

Asynchronously executes FFprobe with arguments provided. + * + * @param arguments FFprobe command options/arguments as string array + * @param executeCallback callback that will be notified when the execution is completed + * @param executor executor that will be used to run this asynchronous operation + * @return ffprobe session created for this execution + */ + public static FFprobeSession executeAsync(final String[] arguments, + final ExecuteCallback executeCallback, + final Executor executor) { + final FFprobeSession session = new FFprobeSession(arguments, executeCallback, null, null); + + AsyncFFprobeExecuteTask asyncFFprobeExecuteTask = new AsyncFFprobeExecuteTask(session); + executor.execute(asyncFFprobeExecuteTask); + + return session; + } + + /** + *

Asynchronously executes FFprobe with arguments provided. + * + * @param arguments FFprobe command options/arguments as string array + * @param executeCallback callback that will be notified when execution is completed + * @param logCallback callback that will receive log entries + * @param statisticsCallback callback that will receive statistics + * @param executor executor that will be used to run this asynchronous operation + * @return ffprobe session created for this execution + */ + public static FFprobeSession executeAsync(final String[] arguments, + final ExecuteCallback executeCallback, + final LogCallback logCallback, + final StatisticsCallback statisticsCallback, + final Executor executor) { + final FFprobeSession session = new FFprobeSession(arguments, executeCallback, logCallback, statisticsCallback); + + AsyncFFprobeExecuteTask asyncFFprobeExecuteTask = new AsyncFFprobeExecuteTask(session); + executor.execute(asyncFFprobeExecuteTask); + + return session; + } + + /** + *

Returns media information for the given path. + * + * @param path path or uri of a media file + * @return media information session created for this execution + */ + public static MediaInformationSession getMediaInformation(final String path) { + return getMediaInformationFromCommandArguments(new String[]{"-v", "error", "-hide_banner", "-print_format", "json", "-show_format", "-show_streams", "-i", path}, null, null, null); + } + + /** + *

Returns media information for the given path asynchronously. + * + * @param path path or uri of a media file + * @param executeCallback callback that will be notified when the execution is completed + * @return media information session created for this execution + */ + public static MediaInformationSession getMediaInformationAsync(final String path, + final ExecuteCallback executeCallback) { + final MediaInformationSession session = new MediaInformationSession(new String[]{"-v", "error", "-hide_banner", "-print_format", "json", "-show_format", "-show_streams", "-i", path}, executeCallback, null, null); + + FFmpegKitConfig.asyncGetMediaInformationExecute(session); + + return session; + } + + /** + *

Returns media information for the given path asynchronously. + * + * @param path path or uri of a media file + * @param executeCallback callback that will be notified when execution is completed + * @param logCallback callback that will receive log entries + * @param statisticsCallback callback that will receive statistics + * @return media information session created for this execution + */ + public static MediaInformationSession getMediaInformationAsync(final String path, + final ExecuteCallback executeCallback, + final LogCallback logCallback, + final StatisticsCallback statisticsCallback) { + final MediaInformationSession session = new MediaInformationSession(new String[]{"-v", "error", "-hide_banner", "-print_format", "json", "-show_format", "-show_streams", "-i", path}, executeCallback, logCallback, statisticsCallback); + + FFmpegKitConfig.asyncGetMediaInformationExecute(session); + + return session; + } + + /** + *

Returns media information for the given path asynchronously. + * + * @param path path or uri of a media file + * @param executeCallback callback that will be notified when the execution is completed + * @param executor executor that will be used to run this asynchronous operation + * @return media information session created for this execution + */ + public static MediaInformationSession getMediaInformationAsync(final String path, + final ExecuteCallback executeCallback, + final Executor executor) { + final MediaInformationSession session = new MediaInformationSession(new String[]{"-v", "error", "-hide_banner", "-print_format", "json", "-show_format", "-show_streams", "-i", path}, executeCallback, null, null); + + AsyncGetMediaInformationTask asyncGetMediaInformationTask = new AsyncGetMediaInformationTask(session); + executor.execute(asyncGetMediaInformationTask); + + return session; + } + + /** + *

Returns media information for the given path asynchronously. + * + * @param path path or uri of a media file + * @param executeCallback callback that will be notified when execution is completed + * @param logCallback callback that will receive log entries + * @param statisticsCallback callback that will receive statistics + * @param executor executor that will be used to run this asynchronous operation + * @return media information session created for this execution + */ + public static MediaInformationSession getMediaInformationAsync(final String path, + final ExecuteCallback executeCallback, + final LogCallback logCallback, + final StatisticsCallback statisticsCallback, + final Executor executor) { + final MediaInformationSession session = new MediaInformationSession(new String[]{"-v", "error", "-hide_banner", "-print_format", "json", "-show_format", "-show_streams", "-i", path}, executeCallback, logCallback, statisticsCallback); + + AsyncGetMediaInformationTask asyncGetMediaInformationTask = new AsyncGetMediaInformationTask(session); + executor.execute(asyncGetMediaInformationTask); + + return session; } /** *

Returns media information for the given command. * - *

This method does not support executing multiple concurrent operations. If you execute - * multiple operations (execute or getMediaInformation) at the same time, the response of this - * method is not predictable. - * * @param command command to execute - * @return media information - * @since 4.3.3 + * @return media information session created for this execution */ - public static MediaInformation getMediaInformationFromCommand(final String command) { - return getMediaInformationFromCommandArguments(FFmpegKit.parseArguments(command)); + public static MediaInformationSession getMediaInformationFromCommand(final String command) { + return getMediaInformationFromCommandArguments(FFmpegKit.parseArguments(command), null, null, null); } + /** - *

Returns media information for given file. + *

Returns media information for the given command. * - *

This method does not support executing multiple concurrent operations. If you execute - * multiple operations (execute or getMediaInformation) at the same time, the response of this - * method is not predictable. - * - * @param path path or uri of media file - * @param timeout complete timeout - * @return media information - * @since 3.0 - * @deprecated this method is deprecated since v4.3.1. You can still use this method but - * timeout parameter is not effective anymore. + * @param command command to execute + * @param executeCallback callback that will be notified when execution is completed + * @param logCallback callback that will receive log entries + * @param statisticsCallback callback that will receive statistics + * @return media information session created for this execution */ - public static MediaInformation getMediaInformation(final String path, final Long timeout) { - return getMediaInformation(path); + public static MediaInformationSession getMediaInformationFromCommand(final String command, + final ExecuteCallback executeCallback, + final LogCallback logCallback, + final StatisticsCallback statisticsCallback) { + return getMediaInformationFromCommandArguments(FFmpegKit.parseArguments(command), executeCallback, logCallback, statisticsCallback); } - private static MediaInformation getMediaInformationFromCommandArguments(final String[] arguments) { - final int rc = execute(arguments); + private static MediaInformationSession getMediaInformationFromCommandArguments(final String[] arguments, + final ExecuteCallback executeCallback, + final LogCallback logCallback, + final StatisticsCallback statisticsCallback) { + final MediaInformationSession session = new MediaInformationSession(arguments, executeCallback, logCallback, statisticsCallback); - if (rc == 0) { - return MediaInformationParser.from(FFmpegKitConfig.getLastCommandOutput()); - } else { - Log.w(FFmpegKitConfig.TAG, FFmpegKitConfig.getLastCommandOutput()); - return null; - } + FFmpegKitConfig.getMediaInformationExecute(session); + + return session; } } diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/FFprobeSession.java b/android/app/src/main/java/com/arthenica/ffmpegkit/FFprobeSession.java new file mode 100644 index 0000000..0a72666 --- /dev/null +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/FFprobeSession.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2020-2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +package com.arthenica.ffmpegkit; + +import java.text.MessageFormat; +import java.util.LinkedList; +import java.util.Queue; +import java.util.stream.Stream; + +/** + *

An FFprobe execute session. + */ +public class FFprobeSession extends AbstractSession implements Session { + + public FFprobeSession(final String[] arguments, + final ExecuteCallback executeCallback, + final LogCallback logCallback, + final StatisticsCallback statisticsCallback) { + super(arguments, executeCallback, logCallback, statisticsCallback); + } + + @Override + public Queue getStatistics() { + return new LinkedList<>(); + } + + @Override + public Stream getStatisticsAsStream() { + return new LinkedList().stream(); + } + + @Override + public void addStatistics(final Statistics statistics) { + /* + * ffprobe does not support statistics. + * So, this method should never have been called. + */ + android.util.Log.w(FFmpegKitConfig.TAG, MessageFormat.format("FFprobe execute session {0} received statistics.", sessionId)); + } + + @Override + public boolean isFFmpeg() { + return false; + } + + @Override + public boolean isFFprobe() { + return true; + } + + @Override + public String toString() { + final StringBuilder stringBuilder = new StringBuilder(); + + stringBuilder.append("FFprobeSession{"); + stringBuilder.append("sessionId="); + stringBuilder.append(sessionId); + stringBuilder.append(", createTime="); + stringBuilder.append(createTime); + stringBuilder.append(", startTime="); + stringBuilder.append(startTime); + stringBuilder.append(", endTime="); + stringBuilder.append(endTime); + stringBuilder.append(", arguments="); + stringBuilder.append(FFmpegKit.argumentsToString(arguments)); + stringBuilder.append(", logs="); + stringBuilder.append(getLogsAsString()); + stringBuilder.append(", state="); + stringBuilder.append(state); + stringBuilder.append(", returnCode="); + stringBuilder.append(returnCode); + stringBuilder.append(", failStackTrace="); + stringBuilder.append('\''); + stringBuilder.append(failStackTrace); + stringBuilder.append('\''); + stringBuilder.append('}'); + + return stringBuilder.toString(); + } + +} diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/Level.java b/android/app/src/main/java/com/arthenica/ffmpegkit/Level.java index 45dea6e..f107b44 100644 --- a/android/app/src/main/java/com/arthenica/ffmpegkit/Level.java +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/Level.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2018-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -20,7 +20,7 @@ package com.arthenica.ffmpegkit; /** - *

Helper enumeration type for log levels. + *

Enumeration type for log levels. */ public enum Level { @@ -79,7 +79,7 @@ public enum Level { */ AV_LOG_TRACE(56); - private int value; + private final int value; /** *

Returns enumeration defined by value. diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/LogMessage.java b/android/app/src/main/java/com/arthenica/ffmpegkit/Log.java similarity index 65% rename from android/app/src/main/java/com/arthenica/ffmpegkit/LogMessage.java rename to android/app/src/main/java/com/arthenica/ffmpegkit/Log.java index a29b3ee..2b5d09e 100644 --- a/android/app/src/main/java/com/arthenica/ffmpegkit/LogMessage.java +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/Log.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2018-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -20,44 +20,43 @@ package com.arthenica.ffmpegkit; /** - *

Logs for running executions. + *

Log entry for an execute session. */ -public class LogMessage { - - private final long executionId; +public class Log { + private final long sessionId; private final Level level; - private final String text; + private final String message; - public LogMessage(final long executionId, final Level level, final String text) { - this.executionId = executionId; + public Log(final long sessionId, final Level level, final String message) { + this.sessionId = sessionId; this.level = level; - this.text = text; + this.message = message; } - public long getExecutionId() { - return executionId; + public long getSessionId() { + return sessionId; } public Level getLevel() { return level; } - public String getText() { - return text; + public String getMessage() { + return message; } @Override public String toString() { final StringBuilder stringBuilder = new StringBuilder(); - stringBuilder.append("LogMessage{"); - stringBuilder.append("executionId="); - stringBuilder.append(executionId); + stringBuilder.append("Log{"); + stringBuilder.append("sessionId="); + stringBuilder.append(sessionId); stringBuilder.append(", level="); stringBuilder.append(level); - stringBuilder.append(", text="); + stringBuilder.append(", message="); stringBuilder.append("\'"); - stringBuilder.append(text); + stringBuilder.append(message); stringBuilder.append('\''); stringBuilder.append('}'); diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/LogCallback.java b/android/app/src/main/java/com/arthenica/ffmpegkit/LogCallback.java index 3923886..aa10572 100644 --- a/android/app/src/main/java/com/arthenica/ffmpegkit/LogCallback.java +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/LogCallback.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2018-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -20,11 +20,16 @@ package com.arthenica.ffmpegkit; /** - *

Represents a callback function to receive logs from running executions + *

Callback function to receive logs for executions. */ @FunctionalInterface public interface LogCallback { - void apply(final LogMessage message); + /** + *

Called when a log entry is received. + * + * @param log log entry + */ + void apply(final Log log); } diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/MediaInformation.java b/android/app/src/main/java/com/arthenica/ffmpegkit/MediaInformation.java index 662dcd0..83d04a3 100644 --- a/android/app/src/main/java/com/arthenica/ffmpegkit/MediaInformation.java +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/MediaInformation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2018-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -28,15 +28,16 @@ import java.util.List; */ public class MediaInformation { - private static final String KEY_MEDIA_PROPERTIES = "format"; - private static final String KEY_FILENAME = "filename"; - private static final String KEY_FORMAT = "format_name"; - private static final String KEY_FORMAT_LONG = "format_long_name"; - private static final String KEY_START_TIME = "start_time"; - private static final String KEY_DURATION = "duration"; - private static final String KEY_SIZE = "size"; - private static final String KEY_BIT_RATE = "bit_rate"; - private static final String KEY_TAGS = "tags"; + /* COMMON KEYS */ + public static final String KEY_MEDIA_PROPERTIES = "format"; + public static final String KEY_FILENAME = "filename"; + public static final String KEY_FORMAT = "format_name"; + public static final String KEY_FORMAT_LONG = "format_long_name"; + public static final String KEY_START_TIME = "start_time"; + public static final String KEY_DURATION = "duration"; + public static final String KEY_SIZE = "size"; + public static final String KEY_BIT_RATE = "bit_rate"; + public static final String KEY_TAGS = "tags"; /** * Stores all properties. diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/MediaInformationParser.java b/android/app/src/main/java/com/arthenica/ffmpegkit/MediaInformationParser.java index 8324e7a..de5734b 100644 --- a/android/app/src/main/java/com/arthenica/ffmpegkit/MediaInformationParser.java +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/MediaInformationParser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2018-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -21,6 +21,8 @@ package com.arthenica.ffmpegkit; import android.util.Log; +import com.arthenica.smartexception.java.Exceptions; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -28,12 +30,14 @@ import org.json.JSONObject; import java.util.ArrayList; /** - * Helper class for parsing {@link MediaInformation}. + * Parser for {@link MediaInformation}. */ public class MediaInformationParser { /** - * Extracts MediaInformation from the given ffprobe json output. + * Extracts MediaInformation from the given ffprobe json output. Note that this + * method does not throw {@link JSONException} as {@link #fromWithError(String)} does and + * handles errors internally. * * @param ffprobeJsonOutput ffprobe json output * @return created {@link MediaInformation} instance of null if a parsing error occurs @@ -42,8 +46,7 @@ public class MediaInformationParser { try { return fromWithError(ffprobeJsonOutput); } catch (JSONException e) { - Log.e(FFmpegKitConfig.TAG, "MediaInformation parsing failed.", e); - e.printStackTrace(); + Log.e(FFmpegKitConfig.TAG, String.format("MediaInformation parsing failed.%s", Exceptions.getStackTraceString(e))); return null; } } diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/MediaInformationSession.java b/android/app/src/main/java/com/arthenica/ffmpegkit/MediaInformationSession.java new file mode 100644 index 0000000..fb94d34 --- /dev/null +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/MediaInformationSession.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +package com.arthenica.ffmpegkit; + +/** + *

A custom FFprobe execute session, which produces a MediaInformation object + * using the output of the execution. + */ +public class MediaInformationSession extends FFprobeSession implements Session { + private MediaInformation mediaInformation; + + public MediaInformationSession(final String[] arguments, + final ExecuteCallback executeCallback, + final LogCallback logCallback, + final StatisticsCallback statisticsCallback) { + super(arguments, executeCallback, logCallback, statisticsCallback); + } + + public MediaInformation getMediaInformation() { + return mediaInformation; + } + + public void setMediaInformation(MediaInformation mediaInformation) { + this.mediaInformation = mediaInformation; + } + + @Override + public String toString() { + final StringBuilder stringBuilder = new StringBuilder(); + + stringBuilder.append("MediaInformationSession{"); + stringBuilder.append("sessionId="); + stringBuilder.append(sessionId); + stringBuilder.append(", createTime="); + stringBuilder.append(createTime); + stringBuilder.append(", startTime="); + stringBuilder.append(startTime); + stringBuilder.append(", endTime="); + stringBuilder.append(endTime); + stringBuilder.append(", arguments="); + stringBuilder.append(FFmpegKit.argumentsToString(arguments)); + stringBuilder.append(", logs="); + stringBuilder.append(getLogsAsString()); + stringBuilder.append(", state="); + stringBuilder.append(state); + stringBuilder.append(", returnCode="); + stringBuilder.append(returnCode); + stringBuilder.append(", failStackTrace="); + stringBuilder.append('\''); + stringBuilder.append(failStackTrace); + stringBuilder.append('\''); + stringBuilder.append('}'); + + return stringBuilder.toString(); + } + +} diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/Packages.java b/android/app/src/main/java/com/arthenica/ffmpegkit/Packages.java index 6ab534d..cdb571a 100644 --- a/android/app/src/main/java/com/arthenica/ffmpegkit/Packages.java +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/Packages.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2018-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -24,7 +24,7 @@ import java.util.Collections; import java.util.List; /** - *

Provides helper methods to extract binary package information. + *

Helper class to extract binary package information. */ class Packages { @@ -39,7 +39,6 @@ class Packages { supportedExternalLibraries.add("gnutls"); supportedExternalLibraries.add("kvazaar"); supportedExternalLibraries.add("mp3lame"); - supportedExternalLibraries.add("libaom"); supportedExternalLibraries.add("libass"); supportedExternalLibraries.add("iconv"); supportedExternalLibraries.add("libilbc"); @@ -137,7 +136,6 @@ class Packages { externalLibraryList.contains("gnutls") && externalLibraryList.contains("kvazaar") && externalLibraryList.contains("mp3lame") && - externalLibraryList.contains("libaom") && externalLibraryList.contains("libass") && externalLibraryList.contains("iconv") && externalLibraryList.contains("libilbc") && @@ -172,7 +170,6 @@ class Packages { externalLibraryList.contains("gnutls") && externalLibraryList.contains("kvazaar") && externalLibraryList.contains("mp3lame") && - externalLibraryList.contains("libaom") && externalLibraryList.contains("libass") && externalLibraryList.contains("iconv") && externalLibraryList.contains("libilbc") && @@ -200,7 +197,6 @@ class Packages { externalLibraryList.contains("freetype") && externalLibraryList.contains("fribidi") && externalLibraryList.contains("kvazaar") && - externalLibraryList.contains("libaom") && externalLibraryList.contains("libass") && externalLibraryList.contains("iconv") && externalLibraryList.contains("libtheora") && diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/FFmpegExecution.java b/android/app/src/main/java/com/arthenica/ffmpegkit/ReturnCode.java similarity index 54% rename from android/app/src/main/java/com/arthenica/ffmpegkit/FFmpegExecution.java rename to android/app/src/main/java/com/arthenica/ffmpegkit/ReturnCode.java index 6cd7ed3..7affedf 100644 --- a/android/app/src/main/java/com/arthenica/ffmpegkit/FFmpegExecution.java +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/ReturnCode.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Taner Sener + * Copyright (c) 2021 Taner Sener * * This file is part of FFmpegKit. * @@ -19,32 +19,24 @@ package com.arthenica.ffmpegkit; -import java.util.Date; +public class ReturnCode { -/** - *

Represents an ongoing FFmpeg execution. - */ -public class FFmpegExecution { - private final Date startTime; - private final long executionId; - private final String command; + public static int NOT_SET = -999; - public FFmpegExecution(final long executionId, final String[] arguments) { - this.startTime = new Date(); - this.executionId = executionId; - this.command = FFmpegKit.argumentsToString(arguments); + public static int SUCCESS = 0; + + public static int CANCEL = 255; + + public static boolean isSuccess(final int returnCode) { + return (returnCode == SUCCESS); } - public Date getStartTime() { - return startTime; + public static boolean isFailure(final int returnCode) { + return (returnCode != NOT_SET) && (returnCode != SUCCESS) && (returnCode != CANCEL); } - public long getExecutionId() { - return executionId; - } - - public String getCommand() { - return command; + public static boolean isCancel(final int returnCode) { + return (returnCode == CANCEL); } } diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/Session.java b/android/app/src/main/java/com/arthenica/ffmpegkit/Session.java new file mode 100644 index 0000000..fa10329 --- /dev/null +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/Session.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General License for more details. + * + * You should have received a copy of the GNU Lesser General License + * along with FFmpegKit. If not, see . + */ + +package com.arthenica.ffmpegkit; + +import java.util.Date; +import java.util.Queue; +import java.util.concurrent.Future; +import java.util.stream.Stream; + +/** + *

Interface for ffmpeg and ffprobe execute sessions. + */ +public interface Session { + + ExecuteCallback getExecuteCallback(); + + LogCallback getLogCallback(); + + StatisticsCallback getStatisticsCallback(); + + long getSessionId(); + + Date getCreateTime(); + + Date getStartTime(); + + Date getEndTime(); + + long getDuration(); + + String[] getArguments(); + + String getCommand(); + + Queue getLogs(); + + Stream getLogsAsStream(); + + String getLogsAsString(); + + Queue getStatistics(); + + Stream getStatisticsAsStream(); + + SessionState getState(); + + int getReturnCode(); + + String getFailStackTrace(); + + void addLog(final Log log); + + void addStatistics(final Statistics statistics); + + Future getFuture(); + + void setFuture(final Future future); + + void startRunning(); + + void complete(final int returnCode); + + void fail(final Exception exception); + + boolean isFFmpeg(); + + boolean isFFprobe(); + + void cancel(); + +} diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/GetMediaInformationCallback.java b/android/app/src/main/java/com/arthenica/ffmpegkit/SessionState.java similarity index 74% rename from android/app/src/main/java/com/arthenica/ffmpegkit/GetMediaInformationCallback.java rename to android/app/src/main/java/com/arthenica/ffmpegkit/SessionState.java index 22a504a..f9a8356 100644 --- a/android/app/src/main/java/com/arthenica/ffmpegkit/GetMediaInformationCallback.java +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/SessionState.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2021 Taner Sener * * This file is part of FFmpegKit. * @@ -19,12 +19,9 @@ package com.arthenica.ffmpegkit; -/** - *

Represents a callback function to receive asynchronous getMediaInformation result. - */ -@FunctionalInterface -public interface GetMediaInformationCallback { - - void apply(MediaInformation mediaInformation); - +public enum SessionState { + CREATED, + RUNNING, + FAILED, + COMPLETED } diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/Signal.java b/android/app/src/main/java/com/arthenica/ffmpegkit/Signal.java index 88f06da..85b27be 100644 --- a/android/app/src/main/java/com/arthenica/ffmpegkit/Signal.java +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/Signal.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Taner Sener + * Copyright (c) 2020-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -30,7 +30,7 @@ public enum Signal { SIGTERM(15), SIGXCPU(24); - private int value; + private final int value; Signal(int value) { this.value = value; diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/Statistics.java b/android/app/src/main/java/com/arthenica/ffmpegkit/Statistics.java index 025b52b..710c62f 100644 --- a/android/app/src/main/java/com/arthenica/ffmpegkit/Statistics.java +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/Statistics.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2018-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -20,11 +20,10 @@ package com.arthenica.ffmpegkit; /** - *

Statistics for running executions. + *

Statistics entry for an FFmpeg execute session. */ public class Statistics { - - private long executionId; + private long sessionId; private int videoFrameNumber; private float videoFps; private float videoQuality; @@ -34,7 +33,7 @@ public class Statistics { private double speed; public Statistics() { - executionId = 0; + sessionId = 0; videoFrameNumber = 0; videoFps = 0; videoQuality = 0; @@ -44,8 +43,8 @@ public class Statistics { speed = 0; } - public Statistics(long executionId, int videoFrameNumber, float videoFps, float videoQuality, long size, int time, double bitrate, double speed) { - this.executionId = executionId; + public Statistics(final long sessionId, final int videoFrameNumber, final float videoFps, final float videoQuality, final long size, final int time, final double bitrate, final double speed) { + this.sessionId = sessionId; this.videoFrameNumber = videoFrameNumber; this.videoFps = videoFps; this.videoQuality = videoQuality; @@ -55,44 +54,12 @@ public class Statistics { this.speed = speed; } - public void update(final Statistics newStatistics) { - if (newStatistics != null) { - this.executionId = newStatistics.getExecutionId(); - if (newStatistics.getVideoFrameNumber() > 0) { - this.videoFrameNumber = newStatistics.getVideoFrameNumber(); - } - if (newStatistics.getVideoFps() > 0) { - this.videoFps = newStatistics.getVideoFps(); - } - - if (newStatistics.getVideoQuality() > 0) { - this.videoQuality = newStatistics.getVideoQuality(); - } - - if (newStatistics.getSize() > 0) { - this.size = newStatistics.getSize(); - } - - if (newStatistics.getTime() > 0) { - this.time = newStatistics.getTime(); - } - - if (newStatistics.getBitrate() > 0) { - this.bitrate = newStatistics.getBitrate(); - } - - if (newStatistics.getSpeed() > 0) { - this.speed = newStatistics.getSpeed(); - } - } + public long getSessionId() { + return sessionId; } - public long getExecutionId() { - return executionId; - } - - public void setExecutionId(long executionId) { - this.executionId = executionId; + public void setSessionId(long sessionId) { + this.sessionId = sessionId; } public int getVideoFrameNumber() { @@ -156,8 +123,8 @@ public class Statistics { final StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("Statistics{"); - stringBuilder.append("executionId="); - stringBuilder.append(executionId); + stringBuilder.append("sessionId="); + stringBuilder.append(sessionId); stringBuilder.append(", videoFrameNumber="); stringBuilder.append(videoFrameNumber); stringBuilder.append(", videoFps="); diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/StatisticsCallback.java b/android/app/src/main/java/com/arthenica/ffmpegkit/StatisticsCallback.java index e3e9964..940a74d 100644 --- a/android/app/src/main/java/com/arthenica/ffmpegkit/StatisticsCallback.java +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/StatisticsCallback.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2018-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -20,11 +20,16 @@ package com.arthenica.ffmpegkit; /** - *

Represents a callback function to receive statistics from running executions. + *

Callback function to receive statistics for executions. */ @FunctionalInterface public interface StatisticsCallback { + /** + *

Called when a statistics entry is received. + * + * @param statistics statistics entry + */ void apply(final Statistics statistics); } diff --git a/android/app/src/main/java/com/arthenica/ffmpegkit/StreamInformation.java b/android/app/src/main/java/com/arthenica/ffmpegkit/StreamInformation.java index c441c19..012b712 100644 --- a/android/app/src/main/java/com/arthenica/ffmpegkit/StreamInformation.java +++ b/android/app/src/main/java/com/arthenica/ffmpegkit/StreamInformation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Taner Sener + * Copyright (c) 2018-2021 Taner Sener * * This file is part of FFmpegKit. * @@ -26,6 +26,7 @@ import org.json.JSONObject; */ public class StreamInformation { + /* COMMON KEYS */ private static final String KEY_INDEX = "index"; private static final String KEY_TYPE = "codec_type"; private static final String KEY_CODEC = "codec_name"; diff --git a/android/app/src/test/java/com/arthenica/ffmpegkit/AbstractSessionTest.java b/android/app/src/test/java/com/arthenica/ffmpegkit/AbstractSessionTest.java new file mode 100644 index 0000000..e6f26bc --- /dev/null +++ b/android/app/src/test/java/com/arthenica/ffmpegkit/AbstractSessionTest.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021 Taner Sener + * + * This file is part of FFmpegKit. + * + * FFmpegKit is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FFmpegKit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with FFmpegKit. If not, see . + */ + +package com.arthenica.ffmpegkit; + +import org.junit.Assert; +import org.junit.Test; + +public class AbstractSessionTest { + + private static final String[] TEST_ARGUMENTS = new String[]{"argument1", "argument2"}; + + @Test + public void getLogsAsStringTest() { + final FFprobeSession ffprobeSession = new FFprobeSession(TEST_ARGUMENTS, null, null, null); + + String logMessage1 = "i am log one"; + String logMessage2 = "i am log two"; + + ffprobeSession.addLog(new Log(ffprobeSession.getSessionId(), Level.AV_LOG_DEBUG, logMessage1)); + ffprobeSession.addLog(new Log(ffprobeSession.getSessionId(), Level.AV_LOG_DEBUG, logMessage2)); + + String logsAsString = ffprobeSession.getLogsAsString(); + + Assert.assertEquals(logMessage1 + logMessage2, logsAsString); + } + +} diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 9ef41b2..2fae4ec 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -61,12 +61,12 @@ include $(BUILD_SHARED_LIBRARY) $(call import-module, cpu-features) +MY_SRC_FILES := ffmpegkit.c ffprobekit.c ffmpegkit_exception.c fftools_cmdutils.c fftools_ffmpeg.c fftools_ffprobe.c fftools_ffmpeg_opt.c fftools_ffmpeg_hw.c fftools_ffmpeg_filter.c saf_wrapper.c + ifeq ($(TARGET_PLATFORM),android-16) - MY_SRC_FILES := ffmpegkit.c ffprobekit.c android_lts_support.c ffmpegkit_exception.c fftools_cmdutils.c fftools_ffmpeg.c fftools_ffprobe.c fftools_ffmpeg_opt.c fftools_ffmpeg_hw.c fftools_ffmpeg_filter.c + MY_SRC_FILES += android_lts_support.c else ifeq ($(TARGET_PLATFORM),android-17) - MY_SRC_FILES := ffmpegkit.c ffprobekit.c android_lts_support.c ffmpegkit_exception.c fftools_cmdutils.c fftools_ffmpeg.c fftools_ffprobe.c fftools_ffmpeg_opt.c fftools_ffmpeg_hw.c fftools_ffmpeg_filter.c -else - MY_SRC_FILES := ffmpegkit.c ffprobekit.c ffmpegkit_exception.c fftools_cmdutils.c fftools_ffmpeg.c fftools_ffprobe.c fftools_ffmpeg_opt.c fftools_ffmpeg_hw.c fftools_ffmpeg_filter.c + MY_SRC_FILES += android_lts_support.c endif MY_CFLAGS := -Wall -Werror -Wno-unused-parameter -Wno-switch -Wno-sign-compare diff --git a/tools/release/android/build.gradle b/tools/release/android/build.gradle index c0d15c7..d64fa25 100644 --- a/tools/release/android/build.gradle +++ b/tools/release/android/build.gradle @@ -39,6 +39,7 @@ task javadoc(type: Javadoc) { } dependencies { + implementation 'com.arthenica:smart-exception-java:0.1.0' testImplementation "androidx.test.ext:junit:1.1.2" testImplementation "org.json:json:20190722" } diff --git a/tools/release/android/build.lts.gradle b/tools/release/android/build.lts.gradle index 1e0ab07..4422359 100644 --- a/tools/release/android/build.lts.gradle +++ b/tools/release/android/build.lts.gradle @@ -39,6 +39,7 @@ task javadoc(type: Javadoc) { } dependencies { + implementation 'com.arthenica:smart-exception-java:0.1.0' testImplementation "androidx.test.ext:junit:1.1.2" testImplementation "org.json:json:20190722" }