fix android runtime errors

This commit is contained in:
Taner Sener 2021-01-19 22:55:07 +00:00
parent 90b5fca4ee
commit 18a6a9c066
16 changed files with 492 additions and 198 deletions

View File

@ -50,6 +50,7 @@ struct CallbackData {
/** Session map variables */
const int SESSION_MAP_SIZE = 1000;
static volatile int sessionMap[SESSION_MAP_SIZE];
static volatile int sessionInTransitMessageCountMap[SESSION_MAP_SIZE];
static pthread_mutex_t sessionMapMutex;
/** Redirection control variables */
@ -113,11 +114,12 @@ JNINativeMethod configMethods[] = {
{"getNativeVersion", "()Ljava/lang/String;", (void*) Java_com_arthenica_ffmpegkit_FFmpegKitConfig_getNativeVersion},
{"nativeFFmpegExecute", "(J[Ljava/lang/String;)I", (void*) Java_com_arthenica_ffmpegkit_FFmpegKitConfig_nativeFFmpegExecute},
{"nativeFFmpegCancel", "(J)V", (void*) Java_com_arthenica_ffmpegkit_FFmpegKitConfig_nativeFFmpegCancel},
{"nativeFFprobeExecute", "([Ljava/lang/String;)I", (void*) Java_com_arthenica_ffmpegkit_FFmpegKitConfig_nativeFFprobeExecute},
{"nativeFFprobeExecute", "(J[Ljava/lang/String;)I", (void*) Java_com_arthenica_ffmpegkit_FFmpegKitConfig_nativeFFprobeExecute},
{"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},
{"ignoreNativeSignal", "(I)V", (void*) Java_com_arthenica_ffmpegkit_FFmpegKitConfig_ignoreNativeSignal}
{"ignoreNativeSignal", "(I)V", (void*) Java_com_arthenica_ffmpegkit_FFmpegKitConfig_ignoreNativeSignal},
{"messagesInTransmit", "(J)I", (void*) Java_com_arthenica_ffmpegkit_FFmpegKitConfig_messagesInTransmit}
};
/** Forward declaration for function defined in fftools_ffmpeg.c */
@ -213,7 +215,7 @@ void monitorInit() {
pthread_condattr_destroy(&cattributes);
}
void executionMapLockInit() {
void sessionMapLockInit() {
pthread_mutexattr_t attributes;
pthread_mutexattr_init(&attributes);
pthread_mutexattr_settype(&attributes, PTHREAD_MUTEX_RECURSIVE_NP);
@ -231,7 +233,7 @@ void monitorUnInit() {
pthread_cond_destroy(&monitorCondition);
}
void executionMapLockUnInit() {
void sessionMapLockUnInit() {
pthread_mutex_destroy(&sessionMapMutex);
}
@ -314,6 +316,9 @@ void logCallbackDataAdd(int level, AVBPrint *data) {
callbackDataTail = newData;
}
int key = sessionId % SESSION_MAP_SIZE;
sessionInTransitMessageCountMap[key] += 1;
mutexUnlock();
monitorNotify();
@ -356,6 +361,9 @@ void statisticsCallbackDataAdd(int frameNumber, float fps, float quality, int64_
callbackDataTail = newData;
}
int key = sessionId % SESSION_MAP_SIZE;
sessionInTransitMessageCountMap[key] += 1;
mutexUnlock();
monitorNotify();
@ -456,6 +464,20 @@ int cancelRequested(long id) {
return found;
}
/**
* Resets the number of messages in transmit for this session.
*
* @param id session id
*/
void resetMessagesInTransmit(long id) {
mutexLock();
int key = id % SESSION_MAP_SIZE;
sessionInTransitMessageCountMap[key] = 0;
mutexUnlock();
}
/**
* Callback function for FFmpeg logs.
*
@ -566,6 +588,9 @@ void *callbackThreadFunction() {
}
int key = callbackData->sessionId % SESSION_MAP_SIZE;
sessionInTransitMessageCountMap[key] -= 1;
// CLEAN STRUCT
callbackData->next = NULL;
av_free(callbackData);
@ -611,7 +636,7 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) {
return JNI_FALSE;
}
if ((*env)->RegisterNatives(env, localConfigClass, configMethods, 13) < 0) {
if ((*env)->RegisterNatives(env, localConfigClass, configMethods, 14) < 0) {
LOGE("OnLoad failed to RegisterNatives for class %s.\n", configClassName);
return JNI_FALSE;
}
@ -664,7 +689,7 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) {
mutexInit();
monitorInit();
executionMapLockInit();
sessionMapLockInit();
return JNI_VERSION_1_6;
}
@ -805,10 +830,12 @@ JNIEXPORT jint JNICALL Java_com_arthenica_ffmpegkit_FFmpegKitConfig_nativeFFmpeg
}
}
// REGISTER THE ID BEFORE STARTING EXECUTION
// REGISTER THE ID BEFORE STARTING THE EXECUTION
sessionId = (long) id;
addSession((long) id);
resetMessagesInTransmit(sessionId);
// RUN
int retCode = ffmpeg_execute(argumentCount, argv);
@ -907,3 +934,22 @@ JNIEXPORT void JNICALL Java_com_arthenica_ffmpegkit_FFmpegKitConfig_ignoreNative
handleSIGPIPE = 0;
}
}
/**
* Returns the number of native messages which are not transmitted to the Java callbacks for the
* given session.
*
* @param env pointer to native method interface
* @param object reference to the class on which this method is invoked
* @param id session id
*/
JNIEXPORT int JNICALL Java_com_arthenica_ffmpegkit_FFmpegKitConfig_messagesInTransmit(JNIEnv *env, jclass object, jlong id) {
mutexLock();
int key = id % SESSION_MAP_SIZE;
int count = sessionInTransitMessageCountMap[key];
mutexUnlock();
return count;
}

View File

@ -131,4 +131,11 @@ JNIEXPORT int JNICALL Java_com_arthenica_ffmpegkit_FFmpegKitConfig_setNativeEnvi
*/
JNIEXPORT void JNICALL Java_com_arthenica_ffmpegkit_FFmpegKitConfig_ignoreNativeSignal(JNIEnv *env, jclass object, jint signum);
/*
* Class: com_arthenica_ffmpegkit_FFmpegKitConfig
* Method: messagesInTransmit
* Signature: (J)I
*/
JNIEXPORT int JNICALL Java_com_arthenica_ffmpegkit_FFmpegKitConfig_messagesInTransmit(JNIEnv *env, jclass object, jlong id);
#endif /* FFMPEG_KIT_H */

View File

@ -33,6 +33,7 @@ extern int configuredLogLevel;
extern __thread volatile long sessionId;
extern void addSession(long id);
extern void removeSession(long id);
extern void resetMessagesInTransmit(long id);
/**
* Synchronously executes FFprobe natively with arguments provided.
@ -76,10 +77,12 @@ JNIEXPORT jint JNICALL Java_com_arthenica_ffmpegkit_FFmpegKitConfig_nativeFFprob
}
}
// REGISTER THE ID BEFORE STARTING EXECUTION
// REGISTER THE ID BEFORE STARTING THE EXECUTION
sessionId = (long) id;
addSession((long) id);
resetMessagesInTransmit(sessionId);
// RUN
int retCode = ffprobe_execute(argumentCount, argv);

View File

@ -1846,12 +1846,6 @@ static void print_report(int is_last_report, int64_t timer_start, int64_t cur_ti
int ret;
float t;
// FORWARD IT BEFORE PROCESSING
forward_report(is_last_report, timer_start, cur_time);
if (!print_stats && !is_last_report && !progress_avio)
return;
if (!is_last_report) {
if (last_time == -1) {
last_time = cur_time;
@ -1862,6 +1856,11 @@ static void print_report(int is_last_report, int64_t timer_start, int64_t cur_ti
last_time = cur_time;
}
forward_report(is_last_report, timer_start, cur_time);
if (!print_stats && !is_last_report && !progress_avio)
return;
t = (cur_time-timer_start) / 1000000.0;

View File

@ -22,22 +22,22 @@ 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 static final AtomicLong sessionIdGenerator = new AtomicLong(1);
/**
* Defines how long default `getAll` methods wait.
*/
protected static final int DEFAULT_TIMEOUT_FOR_CALLBACK_MESSAGES_IN_TRANSMIT = 5000;
protected final ExecuteCallback executeCallback;
protected final LogCallback logCallback;
@ -52,11 +52,13 @@ public abstract class AbstractSession implements Session {
protected SessionState state;
protected int returnCode;
protected String failStackTrace;
protected final LogRedirectionStrategy logRedirectionStrategy;
public AbstractSession(final String[] arguments,
final ExecuteCallback executeCallback,
final LogCallback logCallback,
final StatisticsCallback statisticsCallback) {
final StatisticsCallback statisticsCallback,
final LogRedirectionStrategy logRedirectionStrategy) {
this.sessionId = sessionIdGenerator.getAndIncrement();
this.createTime = new Date();
this.startTime = null;
@ -69,36 +71,45 @@ public abstract class AbstractSession implements Session {
this.state = SessionState.CREATED;
this.returnCode = ReturnCode.NOT_SET;
this.failStackTrace = null;
this.logRedirectionStrategy = logRedirectionStrategy;
}
@Override
public ExecuteCallback getExecuteCallback() {
return executeCallback;
}
@Override
public LogCallback getLogCallback() {
return logCallback;
}
@Override
public StatisticsCallback getStatisticsCallback() {
return statisticsCallback;
}
@Override
public long getSessionId() {
return sessionId;
}
@Override
public Date getCreateTime() {
return createTime;
}
@Override
public Date getStartTime() {
return startTime;
}
@Override
public Date getEndTime() {
return endTime;
}
@Override
public long getDuration() {
final Date startTime = this.startTime;
final Date endTime = this.endTime;
@ -109,85 +120,141 @@ public abstract class AbstractSession implements Session {
return -1;
}
@Override
public String[] getArguments() {
return arguments;
}
@Override
public String getCommand() {
return FFmpegKit.argumentsToString(arguments);
}
protected void waitForCallbackMessagesInTransmit(final int timeout) {
final long start = System.currentTimeMillis();
/*
* WE GIVE MAX 5 SECONDS TO TRANSMIT ALL NATIVE MESSAGES
*/
while (thereAreCallbackMessagesInTransmit() && (System.currentTimeMillis() < (start + timeout))) {
synchronized (this) {
try {
wait(100);
} catch (InterruptedException ignored) {
}
}
}
}
@Override
public Queue<Log> getAllLogs(final int waitTimeout) {
waitForCallbackMessagesInTransmit(waitTimeout);
if (thereAreCallbackMessagesInTransmit()) {
android.util.Log.i(FFmpegKitConfig.TAG, String.format("getAllLogs was asked to return all logs but there are still logs being transmitted for session id %d.", sessionId));
}
return logs;
}
@Override
public Queue<Log> getAllLogs() {
return getAllLogs(DEFAULT_TIMEOUT_FOR_CALLBACK_MESSAGES_IN_TRANSMIT);
}
@Override
public Queue<Log> getLogs() {
return logs;
}
public Stream<Log> getLogsAsStream() {
return logs.stream();
@Override
public String getAllLogsAsString(final int waitTimeout) {
waitForCallbackMessagesInTransmit(waitTimeout);
if (thereAreCallbackMessagesInTransmit()) {
android.util.Log.i(FFmpegKitConfig.TAG, String.format("getAllLogsAsString was asked to return all logs but there are still logs being transmitted for session id %d.", sessionId));
}
return getLogsAsString();
}
@Override
public String getAllLogsAsString() {
return getAllLogsAsString(DEFAULT_TIMEOUT_FOR_CALLBACK_MESSAGES_IN_TRANSMIT);
}
@Override
public String getLogsAsString() {
final Optional<String> concatenatedStringOption = logs.stream().map(new Function<Log, String>() {
@Override
public String apply(final Log log) {
return log.getMessage();
}
}).reduce(new BinaryOperator<String>() {
@Override
public String apply(final String s1, final String s2) {
return s1 + s2;
}
});
final StringBuilder concatenatedString = new StringBuilder();
return concatenatedStringOption.orElseGet(new Supplier<String>() {
for (Log log : logs) {
concatenatedString.append(log.getMessage());
}
@Override
public String get() {
return "";
}
});
return concatenatedString.toString();
}
@Override
public SessionState getState() {
return state;
}
@Override
public int getReturnCode() {
return returnCode;
}
@Override
public String getFailStackTrace() {
return failStackTrace;
}
@Override
public LogRedirectionStrategy getLogRedirectionStrategy() {
return logRedirectionStrategy;
}
@Override
public boolean thereAreCallbackMessagesInTransmit() {
return (FFmpegKitConfig.messagesInTransmit(sessionId) != 0);
}
@Override
public void addLog(final Log log) {
this.logs.add(log);
}
@Override
public Future<?> getFuture() {
return future;
}
@Override
public void setFuture(final Future<?> future) {
this.future = future;
}
@Override
public void startRunning() {
this.state = SessionState.RUNNING;
this.startTime = new Date();
}
@Override
public void complete(final int returnCode) {
this.returnCode = returnCode;
this.state = SessionState.COMPLETED;
this.endTime = new Date();
}
@Override
public void fail(final Exception exception) {
this.failStackTrace = Exceptions.getStackTraceString(exception);
this.state = SessionState.FAILED;
this.endTime = new Date();
}
@Override
public void cancel() {
if (state == SessionState.RUNNING) {
FFmpegKit.cancel(sessionId);

View File

@ -35,6 +35,11 @@ public class AsyncFFprobeExecuteTask implements Runnable {
public void run() {
FFmpegKitConfig.ffprobeExecute(ffprobeSession);
final ExecuteCallback globalExecuteCallbackFunction = FFmpegKitConfig.getGlobalExecuteCallbackFunction();
if (globalExecuteCallbackFunction != null) {
globalExecuteCallbackFunction.apply(ffprobeSession);
}
if (executeCallback != null) {
executeCallback.apply(ffprobeSession);
}

View File

@ -25,15 +25,26 @@ package com.arthenica.ffmpegkit;
public class AsyncGetMediaInformationTask implements Runnable {
private final MediaInformationSession mediaInformationSession;
private final ExecuteCallback executeCallback;
private final Integer waitTimeout;
public AsyncGetMediaInformationTask(final MediaInformationSession mediaInformationSession) {
this(mediaInformationSession, AbstractSession.DEFAULT_TIMEOUT_FOR_CALLBACK_MESSAGES_IN_TRANSMIT);
}
public AsyncGetMediaInformationTask(final MediaInformationSession mediaInformationSession, final Integer waitTimeout) {
this.mediaInformationSession = mediaInformationSession;
this.executeCallback = mediaInformationSession.getExecuteCallback();
this.waitTimeout = waitTimeout;
}
@Override
public void run() {
FFmpegKitConfig.getMediaInformationExecute(mediaInformationSession);
FFmpegKitConfig.getMediaInformationExecute(mediaInformationSession, waitTimeout);
final ExecuteCallback globalExecuteCallbackFunction = FFmpegKitConfig.getGlobalExecuteCallbackFunction();
if (globalExecuteCallbackFunction != null) {
globalExecuteCallbackFunction.apply(mediaInformationSession);
}
if (executeCallback != null) {
executeCallback.apply(mediaInformationSession);

View File

@ -27,12 +27,12 @@ import java.util.concurrent.Executor;
* <p>Main class for FFmpeg operations. Supports synchronous {@link #execute(String...)} and
* asynchronous {@link #executeAsync(String, ExecuteCallback)} methods to execute FFmpeg commands.
* <pre>
* int rc = FFmpeg.execute("-i file1.mp4 -c:v libxvid file1.avi");
* Log.i(Config.TAG, String.format("Command execution %s.", (rc == 0?"completed successfully":"failed with rc=" + rc));
* FFmpegSession session = FFmpeg.execute("-i file1.mp4 -c:v libxvid file1.avi");
* Log.i(Config.TAG, String.format("Command execution completed with %d.", session.getReturnCode());
* </pre>
* <pre>
* long executionId = FFmpeg.executeAsync("-i file1.mp4 -c:v libxvid file1.avi", executeCallback);
* Log.i(Config.TAG, String.format("Asynchronous execution %d started.", executionId));
* FFmpegSession session = FFmpeg.executeAsync("-i file1.mp4 -c:v libxvid file1.avi", executeCallback);
* Log.i(Config.TAG, String.format("Asynchronous session %d started.", session.getSessionId()));
* </pre>
*/
public class FFmpegKit {
@ -231,17 +231,18 @@ public class FFmpegKit {
}
/**
* <p>Cancels the last execution started.
* <p>Cancels all ongoing executions.
*
* <p>This function does not wait for termination to complete and returns immediately.
*/
public static void cancel() {
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.");
}
/*
* ZERO (0) IS A SPECIAL SESSION ID
* WHEN IT IS PASSED TO THIS METHOD, A SIGINT IS GENERATED WHICH CANCELS ALL ONGOING
* EXECUTIONS
*/
FFmpegKitConfig.nativeFFmpegCancel(0);
}
/**
@ -249,7 +250,7 @@ public class FFmpegKit {
*
* <p>This function does not wait for termination to complete and returns immediately.
*
* @param sessionId id of the session that will be stopped
* @param sessionId id of the session that will be cancelled
*/
public static void cancel(final long sessionId) {
FFmpegKitConfig.nativeFFmpegCancel(sessionId);

View File

@ -45,10 +45,6 @@ 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;
/**
* <p>This class is used to configure FFmpegKit library and tools coming with it.
@ -85,11 +81,12 @@ public class FFmpegKitConfig {
* Generates ids for named ffmpeg kit pipes.
*/
private static final AtomicLong pipeIndexGenerator;
/* SESSION HISTORY VARIABLES */
private static int sessionHistorySize;
private static final Map<Long, Session> sessionHistoryMap;
private static final Queue<Session> sessionHistoryQueue;
private static final Object sessionHistoryLock;
private static int asyncConcurrencyLimit;
private static final SparseArray<ParcelFileDescriptor> pfdMap;
/**
* Executor service for async executions.
@ -99,9 +96,9 @@ public class FFmpegKitConfig {
private static StatisticsCallback globalStatisticsCallbackFunction;
private static ExecuteCallback globalExecuteCallbackFunction;
private static Level activeLogLevel;
/* SESSION HISTORY VARIABLES */
private static int sessionHistorySize;
private static int asyncConcurrencyLimit;
private static final SparseArray<ParcelFileDescriptor> pfdMap;
private static LogRedirectionStrategy logRedirectionStrategy;
static {
@ -192,6 +189,7 @@ public class FFmpegKitConfig {
sessionHistoryLock = new Object();
pfdMap = new SparseArray<>();
logRedirectionStrategy = LogRedirectionStrategy.PRINT_LOGS_WHEN_NO_CALLBACKS_DEFINED;
enableRedirection();
}
@ -236,6 +234,9 @@ public class FFmpegKitConfig {
final Level level = Level.from(levelValue);
final String text = new String(logMessage);
final Log log = new Log(sessionId, level, text);
boolean globalCallbackDefined = false;
boolean sessionCallbackDefined = false;
LogRedirectionStrategy activeLogRedirectionStrategy = FFmpegKitConfig.logRedirectionStrategy;
// AV_LOG_STDERR logs are always redirected
if ((activeLogLevel == Level.AV_LOG_QUIET && levelValue != Level.AV_LOG_STDERR.getValue()) || levelValue > activeLogLevel.getValue()) {
@ -244,58 +245,88 @@ public class FFmpegKitConfig {
}
final Session session = getSession(sessionId);
if (session != null && session.getLogCallback() != null) {
try {
// NOTIFY SESSION CALLBACK IF DEFINED
session.getLogCallback().apply(log);
} catch (final Exception e) {
android.util.Log.e(FFmpegKitConfig.TAG, String.format("Exception thrown inside session LogCallback block.%s", Exceptions.getStackTraceString(e)));
if (session != null) {
activeLogRedirectionStrategy = session.getLogRedirectionStrategy();
session.addLog(log);
if (session.getLogCallback() != null) {
sessionCallbackDefined = true;
try {
// NOTIFY SESSION CALLBACK IF DEFINED
session.getLogCallback().apply(log);
} catch (final Exception 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) {
globalCallbackDefined = true;
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) {
case AV_LOG_QUIET: {
// PRINT NO OUTPUT
}
break;
case AV_LOG_TRACE:
case AV_LOG_DEBUG: {
android.util.Log.d(TAG, text);
}
break;
case AV_LOG_STDERR:
case AV_LOG_VERBOSE: {
android.util.Log.v(TAG, text);
}
break;
case AV_LOG_INFO: {
android.util.Log.i(TAG, text);
}
break;
case AV_LOG_WARNING: {
android.util.Log.w(TAG, text);
}
break;
case AV_LOG_ERROR:
case AV_LOG_FATAL:
case AV_LOG_PANIC: {
android.util.Log.e(TAG, text);
}
break;
default: {
android.util.Log.v(TAG, text);
}
break;
}
// EXECUTE THE LOG STRATEGY
switch (activeLogRedirectionStrategy) {
case NEVER_PRINT_LOGS: {
return;
}
case PRINT_LOGS_WHEN_GLOBAL_CALLBACK_NOT_DEFINED: {
if (globalCallbackDefined) {
return;
}
}
break;
case PRINT_LOGS_WHEN_SESSION_CALLBACK_NOT_DEFINED: {
if (sessionCallbackDefined) {
return;
}
}
case PRINT_LOGS_WHEN_NO_CALLBACKS_DEFINED: {
if (globalCallbackDefined || sessionCallbackDefined) {
return;
}
}
}
// PRINT LOGS
switch (level) {
case AV_LOG_QUIET: {
// PRINT NO OUTPUT
}
break;
case AV_LOG_TRACE:
case AV_LOG_DEBUG: {
android.util.Log.d(TAG, text);
}
break;
case AV_LOG_INFO: {
android.util.Log.i(TAG, text);
}
break;
case AV_LOG_WARNING: {
android.util.Log.w(TAG, text);
}
break;
case AV_LOG_ERROR:
case AV_LOG_FATAL:
case AV_LOG_PANIC: {
android.util.Log.e(TAG, text);
}
break;
case AV_LOG_STDERR:
case AV_LOG_VERBOSE:
default: {
android.util.Log.v(TAG, text);
}
break;
}
}
@ -314,15 +345,19 @@ public class FFmpegKitConfig {
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(sessionId, videoFrameNumber, videoFps, videoQuality, size, time, bitrate, speed);
final Statistics statistics = new Statistics(sessionId, videoFrameNumber, videoFps, videoQuality, size, time, bitrate, speed);
final Session session = getSession(sessionId);
if (session != null && session.getStatisticsCallback() != null) {
try {
// NOTIFY SESSION CALLBACK IF DEFINED
session.getStatisticsCallback().apply(newStatistics);
} catch (final Exception e) {
android.util.Log.e(FFmpegKitConfig.TAG, String.format("Exception thrown inside session StatisticsCallback block.%s", Exceptions.getStackTraceString(e)));
if (session != null) {
session.addStatistics(statistics);
if (session.getStatisticsCallback() != null) {
try {
// NOTIFY SESSION CALLBACK IF DEFINED
session.getStatisticsCallback().apply(statistics);
} catch (final Exception e) {
android.util.Log.e(FFmpegKitConfig.TAG, String.format("Exception thrown inside session StatisticsCallback block.%s", Exceptions.getStackTraceString(e)));
}
}
}
@ -330,7 +365,7 @@ public class FFmpegKitConfig {
if (globalStatisticsCallbackFunction != null) {
try {
// NOTIFY GLOBAL CALLBACK IF DEFINED
globalStatisticsCallbackFunction.apply(newStatistics);
globalStatisticsCallbackFunction.apply(statistics);
} catch (final Exception e) {
android.util.Log.e(FFmpegKitConfig.TAG, String.format("Exception thrown inside global StatisticsCallback block.%s", Exceptions.getStackTraceString(e)));
}
@ -589,7 +624,7 @@ public class FFmpegKitConfig {
if (lastSession != null) {
// REPLACING CH(13) WITH CH(10)
return lastSession.getLogsAsString().replace('\r', '\n');
return lastSession.getAllLogsAsString().replace('\r', '\n');
} else {
return "";
}
@ -687,8 +722,9 @@ public class FFmpegKitConfig {
* <p>Synchronously executes the media information session provided.
*
* @param mediaInformationSession media information session which includes command options/arguments
* @param waitTimeout max time to wait until media information is transmitted
*/
static void getMediaInformationExecute(final MediaInformationSession mediaInformationSession) {
static void getMediaInformationExecute(final MediaInformationSession mediaInformationSession, final int waitTimeout) {
addSession(mediaInformationSession);
mediaInformationSession.startRunning();
@ -696,7 +732,7 @@ public class FFmpegKitConfig {
final int returnCode = nativeFFprobeExecute(mediaInformationSession.getSessionId(), mediaInformationSession.getArguments());
mediaInformationSession.complete(returnCode);
if (returnCode == ReturnCode.SUCCESS) {
MediaInformation mediaInformation = MediaInformationParser.fromWithError(mediaInformationSession.getLogsAsString());
MediaInformation mediaInformation = MediaInformationParser.fromWithError(mediaInformationSession.getAllLogsAsString(waitTimeout));
mediaInformationSession.setMediaInformation(mediaInformation);
}
} catch (final Exception e) {
@ -731,9 +767,10 @@ public class FFmpegKitConfig {
* <p>Asynchronously executes the media information session provided.
*
* @param mediaInformationSession media information session which includes command options/arguments
* @param waitTimeout max time to wait until media information is transmitted
*/
static void asyncGetMediaInformationExecute(final MediaInformationSession mediaInformationSession) {
AsyncGetMediaInformationTask asyncGetMediaInformationTask = new AsyncGetMediaInformationTask(mediaInformationSession);
static void asyncGetMediaInformationExecute(final MediaInformationSession mediaInformationSession, final Integer waitTimeout) {
AsyncGetMediaInformationTask asyncGetMediaInformationTask = new AsyncGetMediaInformationTask(mediaInformationSession, waitTimeout);
Future<?> future = asyncExecutorService.submit(asyncGetMediaInformationTask);
mediaInformationSession.setFuture(future);
}
@ -804,7 +841,7 @@ public class FFmpegKitConfig {
*
* @return global execute callback function
*/
public static ExecuteCallback getGlobalExecuteCallbackFunction() {
static ExecuteCallback getGlobalExecuteCallbackFunction() {
return globalExecuteCallbackFunction;
}
@ -833,9 +870,16 @@ public class FFmpegKitConfig {
* <p>Converts the given Structured Access Framework Uri (<code>"content:…"</code>) into an
* input/output url that can be used in FFmpegKit and FFprobeKit.
*
* <p>Requires API Level >= 19. On older API levels it returns an empty url.
*
* @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) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
android.util.Log.i(TAG, String.format("getSafParameter is not supported on API Level %d", Build.VERSION.SDK_INT));
return "";
}
String displayName = "unknown";
try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
@ -868,6 +912,8 @@ public class FFmpegKitConfig {
* <p>Converts the given Structured Access Framework Uri (<code>"content:…"</code>) into an
* input url that can be used in FFmpegKit and FFprobeKit.
*
* <p>Requires API Level >= 19. On older API levels it returns an empty url.
*
* @return input url that can be passed to FFmpegKit or FFprobeKit
*/
public static String getSafParameterForRead(final Context context, final Uri uri) {
@ -878,6 +924,8 @@ public class FFmpegKitConfig {
* <p>Converts the given Structured Access Framework Uri (<code>"content:…"</code>) into an
* output url that can be used in FFmpegKit and FFprobeKit.
*
* <p>Requires API Level >= 19. On older API levels it returns an empty url.
*
* @return output url that can be passed to FFmpegKit or FFprobeKit
*/
public static String getSafParameterForWrite(final Context context, final Uri uri) {
@ -913,9 +961,16 @@ public class FFmpegKitConfig {
/**
* Sets the session history size.
*
* @param sessionHistorySize new session history size
* @param sessionHistorySize new session history size, should be smaller than 1000
*/
public static void setSessionHistorySize(int sessionHistorySize) {
public static void setSessionHistorySize(final int sessionHistorySize) {
if (sessionHistorySize >= 1000) {
/*
* THERE IS A HARD LIMIT ON THE NATIVE SIDE. HISTORY SIZE MUST BE SMALLER THAN 1000
*/
throw new IllegalArgumentException("Session history size must not exceed the hard limit!");
}
FFmpegKitConfig.sessionHistorySize = sessionHistorySize;
}
@ -978,27 +1033,17 @@ public class FFmpegKitConfig {
* @return all FFmpeg sessions in the session history
*/
static List<FFmpegSession> getFFmpegSessions() {
final LinkedList<FFmpegSession> list = new LinkedList<>();
synchronized (sessionHistoryLock) {
return sessionHistoryQueue.stream().filter(new Predicate<Session>() {
@Override
public boolean test(final Session session) {
return (session.isFFmpeg());
for (Session session : sessionHistoryQueue) {
if (session.isFFmpeg()) {
list.add((FFmpegSession) session);
}
}).map(new Function<Session, FFmpegSession>() {
@Override
public FFmpegSession apply(final Session session) {
return (FFmpegSession) session;
}
}).collect(Collectors.toCollection(new Supplier<List<FFmpegSession>>() {
@Override
public List<FFmpegSession> get() {
return new LinkedList<>();
}
}));
}
}
return list;
}
/**
@ -1007,27 +1052,17 @@ public class FFmpegKitConfig {
* @return all FFprobe sessions in the session history
*/
static List<FFprobeSession> getFFprobeSessions() {
final LinkedList<FFprobeSession> list = new LinkedList<>();
synchronized (sessionHistoryLock) {
return sessionHistoryQueue.stream().filter(new Predicate<Session>() {
@Override
public boolean test(final Session session) {
return (session.isFFprobe());
for (Session session : sessionHistoryQueue) {
if (session.isFFprobe()) {
list.add((FFprobeSession) session);
}
}).map(new Function<Session, FFprobeSession>() {
@Override
public FFprobeSession apply(final Session session) {
return (FFprobeSession) session;
}
}).collect(Collectors.toCollection(new Supplier<List<FFprobeSession>>() {
@Override
public List<FFprobeSession> get() {
return new LinkedList<>();
}
}));
}
}
return list;
}
/**
@ -1036,21 +1071,35 @@ public class FFmpegKitConfig {
* @return sessions that have the given state from the session history
*/
public static List<Session> getSessionsByState(final SessionState state) {
final LinkedList<Session> list = new LinkedList<>();
synchronized (sessionHistoryLock) {
return sessionHistoryQueue.stream().filter(new Predicate<Session>() {
@Override
public boolean test(final Session session) {
return (session.getState() == state);
for (Session session : sessionHistoryQueue) {
if (session.getState() == state) {
list.add(session);
}
}).collect(Collectors.toCollection(new Supplier<List<Session>>() {
@Override
public List<Session> get() {
return new LinkedList<>();
}
}));
}
}
return list;
}
/**
* Returns the active log redirection strategy.
*
* @return log redirection strategy
*/
public static LogRedirectionStrategy getLogRedirectionStrategy() {
return logRedirectionStrategy;
}
/**
* <p>Sets the log redirection strategy
*
* @param logRedirectionStrategy new log redirection strategy
*/
public static void setLogRedirectionStrategy(final LogRedirectionStrategy logRedirectionStrategy) {
FFmpegKitConfig.logRedirectionStrategy = logRedirectionStrategy;
}
/**
@ -1096,7 +1145,9 @@ public class FFmpegKitConfig {
*
* @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
* @return {@link ReturnCode#SUCCESS} on successful execution and {@link ReturnCode#CANCEL} on
* user cancel. Other non-zero values are returned on error. Use {@link ReturnCode} class to
* handle the value
*/
private native static int nativeFFmpegExecute(final long sessionId, final String[] arguments);
@ -1113,10 +1164,22 @@ public class FFmpegKitConfig {
*
* @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
* @return {@link ReturnCode#SUCCESS} on successful execution and {@link ReturnCode#CANCEL} on
* user cancel. Other non-zero values are returned on error. Use {@link ReturnCode} class to
* handle the value
*/
native static int nativeFFprobeExecute(final long sessionId, final String[] arguments);
/**
* <p>Returns the number of native messages which are not transmitted to the Java callbacks for
* this session natively.
*
* @param sessionId id of the session
* @return number of native messages which are not transmitted to the Java callbacks for
* this session natively
*/
native static int messagesInTransmit(final long sessionId);
/**
* <p>Creates a new named pipe to use in <code>FFmpeg</code> operations natively.
*

View File

@ -21,7 +21,6 @@ package com.arthenica.ffmpegkit;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.stream.Stream;
/**
* <p>An FFmpeg execute session.
@ -33,19 +32,33 @@ public class FFmpegSession extends AbstractSession implements Session {
final ExecuteCallback executeCallback,
final LogCallback logCallback,
final StatisticsCallback statisticsCallback) {
super(arguments, executeCallback, logCallback, statisticsCallback);
super(arguments, executeCallback, logCallback, statisticsCallback, FFmpegKitConfig.getLogRedirectionStrategy());
this.statistics = new ConcurrentLinkedQueue<>();
}
@Override
public Queue<Statistics> getAllStatistics(final int waitTimeout) {
waitForCallbackMessagesInTransmit(waitTimeout);
if (thereAreCallbackMessagesInTransmit()) {
android.util.Log.i(FFmpegKitConfig.TAG, String.format("getAllStatistics was asked to return all statistics but there are still statistics being transmitted for session id %d.", sessionId));
}
return getStatistics();
}
@Override
public Queue<Statistics> getAllStatistics() {
return getAllStatistics(DEFAULT_TIMEOUT_FOR_CALLBACK_MESSAGES_IN_TRANSMIT);
}
@Override
public Queue<Statistics> getStatistics() {
return statistics;
}
public Stream<Statistics> getStatisticsAsStream() {
return statistics.stream();
}
@Override
public void addStatistics(final Statistics statistics) {
this.statistics.add(statistics);
}

View File

@ -19,6 +19,7 @@
package com.arthenica.ffmpegkit;
import java.util.List;
import java.util.concurrent.Executor;
/**
@ -183,6 +184,15 @@ public class FFprobeKit {
return session;
}
/**
* <p>Lists all FFprobe sessions in the session history
*
* @return all FFprobe sessions in the session history
*/
public static List<FFprobeSession> listSessions() {
return FFmpegKitConfig.getFFprobeSessions();
}
/**
* <p>Returns media information for the given path.
*
@ -190,7 +200,18 @@ public class FFprobeKit {
* @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);
return getMediaInformationFromCommandArguments(new String[]{"-v", "error", "-hide_banner", "-print_format", "json", "-show_format", "-show_streams", "-i", path}, null, null, null, AbstractSession.DEFAULT_TIMEOUT_FOR_CALLBACK_MESSAGES_IN_TRANSMIT);
}
/**
* <p>Returns media information for the given path.
*
* @param path path or uri of a media file
* @param waitTimeout max time to wait until media information is transmitted
* @return media information session created for this execution
*/
public static MediaInformationSession getMediaInformation(final String path, final Integer waitTimeout) {
return getMediaInformationFromCommandArguments(new String[]{"-v", "error", "-hide_banner", "-print_format", "json", "-show_format", "-show_streams", "-i", path}, null, null, null, waitTimeout);
}
/**
@ -204,7 +225,7 @@ public class FFprobeKit {
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);
FFmpegKitConfig.asyncGetMediaInformationExecute(session, AbstractSession.DEFAULT_TIMEOUT_FOR_CALLBACK_MESSAGES_IN_TRANSMIT);
return session;
}
@ -216,15 +237,17 @@ public class FFprobeKit {
* @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 waitTimeout max time to wait until media information is transmitted
* @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 StatisticsCallback statisticsCallback,
final Integer waitTimeout) {
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);
FFmpegKitConfig.asyncGetMediaInformationExecute(session, waitTimeout);
return session;
}
@ -256,16 +279,18 @@ public class FFprobeKit {
* @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
* @param waitTimeout max time to wait until media information is transmitted
* @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 Executor executor,
final Integer waitTimeout) {
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);
AsyncGetMediaInformationTask asyncGetMediaInformationTask = new AsyncGetMediaInformationTask(session, waitTimeout);
executor.execute(asyncGetMediaInformationTask);
return session;
@ -278,7 +303,7 @@ public class FFprobeKit {
* @return media information session created for this execution
*/
public static MediaInformationSession getMediaInformationFromCommand(final String command) {
return getMediaInformationFromCommandArguments(FFmpegKit.parseArguments(command), null, null, null);
return getMediaInformationFromCommandArguments(FFmpegKit.parseArguments(command), null, null, null, AbstractSession.DEFAULT_TIMEOUT_FOR_CALLBACK_MESSAGES_IN_TRANSMIT);
}
@ -289,22 +314,25 @@ public class FFprobeKit {
* @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 waitTimeout max time to wait until media information is transmitted
* @return media information session created for this execution
*/
public static MediaInformationSession getMediaInformationFromCommand(final String command,
final ExecuteCallback executeCallback,
final LogCallback logCallback,
final StatisticsCallback statisticsCallback) {
return getMediaInformationFromCommandArguments(FFmpegKit.parseArguments(command), executeCallback, logCallback, statisticsCallback);
final StatisticsCallback statisticsCallback,
final Integer waitTimeout) {
return getMediaInformationFromCommandArguments(FFmpegKit.parseArguments(command), executeCallback, logCallback, statisticsCallback, waitTimeout);
}
private static MediaInformationSession getMediaInformationFromCommandArguments(final String[] arguments,
final ExecuteCallback executeCallback,
final LogCallback logCallback,
final StatisticsCallback statisticsCallback) {
final StatisticsCallback statisticsCallback,
final Integer waitTimeout) {
final MediaInformationSession session = new MediaInformationSession(arguments, executeCallback, logCallback, statisticsCallback);
FFmpegKitConfig.getMediaInformationExecute(session);
FFmpegKitConfig.getMediaInformationExecute(session, waitTimeout);
return session;
}

View File

@ -22,7 +22,6 @@ package com.arthenica.ffmpegkit;
import java.text.MessageFormat;
import java.util.LinkedList;
import java.util.Queue;
import java.util.stream.Stream;
/**
* <p>An FFprobe execute session.
@ -33,7 +32,25 @@ public class FFprobeSession extends AbstractSession implements Session {
final ExecuteCallback executeCallback,
final LogCallback logCallback,
final StatisticsCallback statisticsCallback) {
super(arguments, executeCallback, logCallback, statisticsCallback);
this(arguments, executeCallback, logCallback, statisticsCallback, FFmpegKitConfig.getLogRedirectionStrategy());
}
FFprobeSession(final String[] arguments,
final ExecuteCallback executeCallback,
final LogCallback logCallback,
final StatisticsCallback statisticsCallback,
final LogRedirectionStrategy logRedirectionStrategy) {
super(arguments, executeCallback, logCallback, statisticsCallback, logRedirectionStrategy);
}
@Override
public Queue<Statistics> getAllStatistics(final int waitTimeout) {
return new LinkedList<>();
}
@Override
public Queue<Statistics> getAllStatistics() {
return new LinkedList<>();
}
@Override
@ -41,11 +58,6 @@ public class FFprobeSession extends AbstractSession implements Session {
return new LinkedList<>();
}
@Override
public Stream<Statistics> getStatisticsAsStream() {
return new LinkedList<Statistics>().stream();
}
@Override
public void addStatistics(final Statistics statistics) {
/*

View File

@ -0,0 +1,28 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package com.arthenica.ffmpegkit;
public enum LogRedirectionStrategy {
ALWAYS_PRINT_LOGS,
PRINT_LOGS_WHEN_NO_CALLBACKS_DEFINED,
PRINT_LOGS_WHEN_GLOBAL_CALLBACK_NOT_DEFINED,
PRINT_LOGS_WHEN_SESSION_CALLBACK_NOT_DEFINED,
NEVER_PRINT_LOGS
}

View File

@ -30,7 +30,7 @@ public class MediaInformationSession extends FFprobeSession implements Session {
final ExecuteCallback executeCallback,
final LogCallback logCallback,
final StatisticsCallback statisticsCallback) {
super(arguments, executeCallback, logCallback, statisticsCallback);
super(arguments, executeCallback, logCallback, statisticsCallback, LogRedirectionStrategy.NEVER_PRINT_LOGS);
}
public MediaInformation getMediaInformation() {

View File

@ -22,7 +22,6 @@ package com.arthenica.ffmpegkit;
import java.util.Date;
import java.util.Queue;
import java.util.concurrent.Future;
import java.util.stream.Stream;
/**
* <p>Interface for ffmpeg and ffprobe execute sessions.
@ -49,15 +48,23 @@ public interface Session {
String getCommand();
Queue<Log> getAllLogs(final int waitTimeout);
Queue<Log> getAllLogs();
Queue<Log> getLogs();
Stream<Log> getLogsAsStream();
String getAllLogsAsString(final int waitTimeout);
String getAllLogsAsString();
String getLogsAsString();
Queue<Statistics> getStatistics();
Queue<Statistics> getAllStatistics(final int waitTimeout);
Stream<Statistics> getStatisticsAsStream();
Queue<Statistics> getAllStatistics();
Queue<Statistics> getStatistics();
SessionState getState();
@ -65,6 +72,10 @@ public interface Session {
String getFailStackTrace();
LogRedirectionStrategy getLogRedirectionStrategy();
boolean thereAreCallbackMessagesInTransmit();
void addLog(final Log log);
void addStatistics(final Statistics statistics);

View File

@ -28,7 +28,7 @@ public class AbstractSessionTest {
@Test
public void getLogsAsStringTest() {
final FFprobeSession ffprobeSession = new FFprobeSession(TEST_ARGUMENTS, null, null, null);
final FFprobeSession ffprobeSession = new FFprobeSession(TEST_ARGUMENTS, null, null, null, LogRedirectionStrategy.ALWAYS_PRINT_LOGS);
String logMessage1 = "i am log one";
String logMessage2 = "i am log two";