/* * Copyright (c) 2018-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 android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Build; 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.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.StringTokenizer; 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; /** *

Configuration class of FFmpegKit library. */ public class FFmpegKitConfig { /** * The tag used for logging. */ static final String TAG = "ffmpeg-kit"; /** * Prefix of named pipes created by ffmpeg kit. */ static final String FFMPEG_KIT_NAMED_PIPE_PREFIX = "fk_pipe_"; /** * Generates ids for named ffmpeg kit pipes. */ private static final AtomicLong pipeIndexGenerator; private static Level activeLogLevel; /* Session history variables */ private static int sessionHistorySize; private static final Map sessionHistoryMap; private static final List sessionHistoryList; private static final Object sessionHistoryLock; private static int asyncConcurrencyLimit; private static ExecutorService asyncExecutorService; /* Global callbacks */ private static LogCallback globalLogCallbackFunction; private static StatisticsCallback globalStatisticsCallbackFunction; private static ExecuteCallback globalExecuteCallbackFunction; private static final SparseArray pfdMap; private static LogRedirectionStrategy globalLogRedirectionStrategy; static { Exceptions.registerRootPackage("com.arthenica"); android.util.Log.i(FFmpegKitConfig.TAG, "Loading ffmpeg-kit."); final boolean nativeFFmpegTriedAndFailed = NativeLoader.loadFFmpeg(); /* ALL FFMPEG-KIT LIBRARIES LOADED AT STARTUP */ Abi.class.getName(); FFmpegKit.class.getName(); FFprobeKit.class.getName(); NativeLoader.loadFFmpegKit(nativeFFmpegTriedAndFailed); android.util.Log.i(FFmpegKitConfig.TAG, String.format("Loaded ffmpeg-kit-%s-%s-%s-%s.", NativeLoader.loadPackageName(), NativeLoader.loadAbi(), NativeLoader.loadVersion(), NativeLoader.loadBuildDate())); pipeIndexGenerator = new AtomicLong(1); /* NATIVE LOG LEVEL IS RECEIVED ONLY ON STARTUP */ activeLogLevel = Level.from(NativeLoader.loadLogLevel()); asyncConcurrencyLimit = 10; asyncExecutorService = Executors.newFixedThreadPool(asyncConcurrencyLimit); sessionHistorySize = 10; sessionHistoryMap = new LinkedHashMap() { @Override protected boolean removeEldestEntry(Map.Entry eldest) { return (this.size() > sessionHistorySize); } }; sessionHistoryList = new LinkedList<>(); sessionHistoryLock = new Object(); globalLogCallbackFunction = null; globalStatisticsCallbackFunction = null; globalExecuteCallbackFunction = null; pfdMap = new SparseArray<>(); globalLogRedirectionStrategy = LogRedirectionStrategy.PRINT_LOGS_WHEN_NO_CALLBACKS_DEFINED; NativeLoader.enableRedirection(); } /** * Default constructor hidden. */ private FFmpegKitConfig() { } /** *

Enables log and statistics redirection. * *

When redirection is enabled FFmpeg/FFprobe logs are redirected to Logcat and sessions * collect log and statistics entries for the executions. It is possible to define global or * session specific log/statistics callbacks as well. * *

Note that redirection is enabled by default. If you do not want to use its functionality * please use {@link #disableRedirection()} to disable it. */ public static void enableRedirection() { enableNativeRedirection(); } /** *

Disables log and statistics redirection. * *

When redirection is disabled logs are printed to stderr, all logs and statistics * callbacks are disabled and FFprobe's getMediaInformation methods * do not work. */ public static void disableRedirection() { disableNativeRedirection(); } /** *

Log redirection method called by the native library. * * @param sessionId id of the session that generated this log, 0 for logs that do not belong * to a specific session * @param levelValue log level as defined in {@link Level} * @param logMessage redirected log message data */ 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); boolean globalCallbackDefined = false; boolean sessionCallbackDefined = false; LogRedirectionStrategy activeLogRedirectionStrategy = globalLogRedirectionStrategy; // AV_LOG_STDERR logs are always redirected if ((activeLogLevel == Level.AV_LOG_QUIET && levelValue != Level.AV_LOG_STDERR.getValue()) || levelValue > activeLogLevel.getValue()) { // LOG NEITHER PRINTED NOR FORWARDED return; } final Session session = getSession(sessionId); if (session != null) { activeLogRedirectionStrategy = session.getLogRedirectionStrategy(); session.addLog(log); if (session.getLogCallback() != null) { sessionCallbackDefined = true; try { // NOTIFY SESSION CALLBACK 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 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))); } } // 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; } } case ALWAYS_PRINT_LOGS: { } } // 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; } } /** *

Statistics redirection method called by the native library. * * @param sessionId id of the session that generated this statistics, 0 by default * @param videoFrameNumber frame number for videos * @param videoFps frames per second value for videos * @param videoQuality quality of the video stream * @param size size in bytes * @param time processed duration in milliseconds * @param bitrate output bit rate in kbits/s * @param speed processing speed = processed duration / operation duration */ 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 statistics = new Statistics(sessionId, videoFrameNumber, videoFps, videoQuality, size, time, bitrate, speed); final Session session = getSession(sessionId); if (session != null && session.isFFmpeg()) { FFmpegSession ffmpegSession = (FFmpegSession) session; ffmpegSession.addStatistics(statistics); if (ffmpegSession.getStatisticsCallback() != null) { try { // NOTIFY SESSION CALLBACK IF DEFINED ffmpegSession.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))); } } } final StatisticsCallback globalStatisticsCallbackFunction = FFmpegKitConfig.globalStatisticsCallbackFunction; if (globalStatisticsCallbackFunction != null) { try { // NOTIFY GLOBAL CALLBACK IF DEFINED 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))); } } } /** *

Sets and overrides fontconfig configuration directory. * * @param path directory that contains fontconfig configuration (fonts.conf) * @return zero on success, non-zero on error */ public static int setFontconfigConfigurationPath(final String path) { return setNativeEnvironmentVariable("FONTCONFIG_PATH", path); } /** *

Registers the 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 be able to use * fonts in FFmpeg. * * @param context application context to access application data * @param fontDirectoryPath directory that contains fonts (.ttf and .otf files) * @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) { setFontDirectoryList(context, Collections.singletonList(fontDirectoryPath), fontNameMapping); } /** *

Registers the fonts inside the given list of font directories, 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 be able to use * fonts in FFmpeg. * * @param context application context to access application data * @param fontDirectoryList list of directories that contain fonts (.ttf and .otf files) * @param fontNameMapping custom font name mappings, useful to access your fonts with more * friendly names */ public static void setFontDirectoryList(final Context context, final List fontDirectoryList, final Map fontNameMapping) { final File cacheDir = context.getCacheDir(); int validFontNameMappingCount = 0; final File tempConfigurationDirectory = new File(cacheDir, "fontconfig"); if (!tempConfigurationDirectory.exists()) { boolean tempFontConfDirectoryCreated = tempConfigurationDirectory.mkdirs(); 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(); android.util.Log.d(TAG, String.format("Deleted old temporary font configuration: %s.", fontConfigurationDeleted)); } /* PROCESS MAPPINGS FIRST */ final StringBuilder fontNameMappingBlock = new StringBuilder(""); if (fontNameMapping != null && (fontNameMapping.size() > 0)) { fontNameMapping.entrySet(); for (Map.Entry mapping : fontNameMapping.entrySet()) { String fontName = mapping.getKey(); String mappedFontName = mapping.getValue(); if ((fontName != null) && (mappedFontName != null) && (fontName.trim().length() > 0) && (mappedFontName.trim().length() > 0)) { fontNameMappingBlock.append(" \n"); fontNameMappingBlock.append(" \n"); fontNameMappingBlock.append(String.format(" %s\n", fontName)); fontNameMappingBlock.append(" \n"); fontNameMappingBlock.append(" \n"); fontNameMappingBlock.append(String.format(" %s\n", mappedFontName)); fontNameMappingBlock.append(" \n"); fontNameMappingBlock.append(" \n"); validFontNameMappingCount++; } } } final StringBuilder fontConfigBuilder = new StringBuilder(); fontConfigBuilder.append("\n"); fontConfigBuilder.append("\n"); fontConfigBuilder.append("\n"); fontConfigBuilder.append("

.\n"); for (String fontDirectoryPath : fontDirectoryList) { fontConfigBuilder.append(" "); fontConfigBuilder.append(fontDirectoryPath); fontConfigBuilder.append("\n"); } fontConfigBuilder.append(fontNameMappingBlock); fontConfigBuilder.append(""); final AtomicReference reference = new AtomicReference<>(); try { final FileOutputStream outputStream = new FileOutputStream(fontConfiguration); reference.set(outputStream); outputStream.write(fontConfigBuilder.toString().getBytes()); outputStream.flush(); android.util.Log.d(TAG, String.format("Saved new temporary font configuration with %d font name mappings.", validFontNameMappingCount)); setFontconfigConfigurationPath(tempConfigurationDirectory.getAbsolutePath()); for (String fontDirectoryPath : fontDirectoryList) { android.util.Log.d(TAG, String.format("Font directory %s registered successfully.", fontDirectoryPath)); } } catch (final IOException e) { android.util.Log.e(TAG, String.format("Failed to set font directory: %s.%s", Arrays.toString(fontDirectoryList.toArray()), Exceptions.getStackTraceString(e))); } finally { if (reference.get() != null) { try { reference.get().close(); } catch (IOException e) { // DO NOT PRINT THIS ERROR } } } } /** *

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

Please note that creator is responsible of closing created pipes. * * @param context application context * @return the full path of the named pipe */ public static String registerNewFFmpegPipe(final Context context) { // PIPES ARE CREATED UNDER THE PIPES DIRECTORY final File cacheDir = context.getCacheDir(); final File pipesDir = new File(cacheDir, "pipes"); if (!pipesDir.exists()) { final boolean pipesDirCreated = pipesDir.mkdirs(); if (!pipesDirCreated) { android.util.Log.e(TAG, String.format("Failed to create pipes directory: %s.", pipesDir.getAbsolutePath())); return null; } } final String newFFmpegPipePath = MessageFormat.format("{0}{1}{2}{3}", pipesDir, File.separator, FFMPEG_KIT_NAMED_PIPE_PREFIX, pipeIndexGenerator.getAndIncrement()); // FIRST CLOSE OLD PIPES WITH THE SAME NAME closeFFmpegPipe(newFFmpegPipePath); int rc = registerNewNativeFFmpegPipe(newFFmpegPipePath); if (rc == 0) { return newFFmpegPipePath; } else { android.util.Log.e(TAG, String.format("Failed to register new FFmpeg pipe %s. Operation failed with rc=%d.", newFFmpegPipePath, rc)); return null; } } /** *

Closes a previously created FFmpeg pipe. * * @param ffmpegPipePath full path of the FFmpeg pipe */ public static void closeFFmpegPipe(final String ffmpegPipePath) { final File file = new File(ffmpegPipePath); if (file.exists()) { file.delete(); } } /** * Returns the list of camera ids supported. These devices can be used in FFmpeg * commands. * *

Note that this method requires API Level ≥ 24. On older API levels it returns an empty * list. * * @param context application context * @return 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<>(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { detectedCameraIdList.addAll(CameraSupport.extractSupportedCameraIds(context)); } return detectedCameraIdList; } /** *

Returns the version of FFmpeg bundled within FFmpegKit library. * * @return the version of FFmpeg */ public static String getFFmpegVersion() { return getNativeFFmpegVersion(); } /** *

Returns FFmpegKit library version. * * @return FFmpegKit version */ public static String getVersion() { if (isLTSBuild()) { return String.format("%s-lts", getNativeVersion()); } else { return getNativeVersion(); } } /** *

Returns whether FFmpegKit release is a Long Term Release or not. * * @return true/yes or false/no */ public static boolean isLTSBuild() { return AbiDetect.isNativeLTSBuild(); } /** *

Returns FFmpegKit library build date. * * @return FFmpegKit library build date */ public static String getBuildDate() { return getNativeBuildDate(); } /** *

Prints the given string to Logcat using the given priority. If string provided is bigger * than the Logcat buffer, the string is printed in multiple lines. * * @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} * @param string string to be printed */ public static void printToLogcat(final int logPriority, final String string) { final int LOGGER_ENTRY_MAX_LEN = 4 * 1000; String remainingString = string; do { if (remainingString.length() <= LOGGER_ENTRY_MAX_LEN) { android.util.Log.println(logPriority, FFmpegKitConfig.TAG, remainingString); remainingString = ""; } else { final int index = remainingString.substring(0, LOGGER_ENTRY_MAX_LEN).lastIndexOf('\n'); if (index < 0) { android.util.Log.println(logPriority, FFmpegKitConfig.TAG, remainingString.substring(0, LOGGER_ENTRY_MAX_LEN)); remainingString = remainingString.substring(LOGGER_ENTRY_MAX_LEN); } else { android.util.Log.println(logPriority, FFmpegKitConfig.TAG, remainingString.substring(0, index)); remainingString = remainingString.substring(index); } } } while (remainingString.length() > 0); } /** *

Sets an environment variable. * * @param variableName environment variable name * @param variableValue environment variable value * @return zero on success, non-zero on error */ public static int setEnvironmentVariable(final String variableName, final String variableValue) { return setNativeEnvironmentVariable(variableName, variableValue); } /** *

Registers a new ignored signal. Ignored signals are not handled by FFmpegKit * library. * * @param signal signal to be ignored */ public static void ignoreSignal(final Signal signal) { ignoreNativeSignal(signal.getValue()); } /** *

Synchronously executes the FFmpeg session provided. * * @param ffmpegSession FFmpeg session which includes command options/arguments */ public static void ffmpegExecute(final FFmpegSession ffmpegSession) { ffmpegSession.startRunning(); try { final int returnCode = nativeFFmpegExecute(ffmpegSession.getSessionId(), ffmpegSession.getArguments()); ffmpegSession.complete(new ReturnCode(returnCode)); } catch (final Exception e) { ffmpegSession.fail(e); android.util.Log.w(FFmpegKitConfig.TAG, String.format("FFmpeg execute failed: %s.%s", FFmpegKitConfig.argumentsToString(ffmpegSession.getArguments()), Exceptions.getStackTraceString(e))); } } /** *

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

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 */ public static void getMediaInformationExecute(final MediaInformationSession mediaInformationSession, final int waitTimeout) { mediaInformationSession.startRunning(); try { final int returnCodeValue = nativeFFprobeExecute(mediaInformationSession.getSessionId(), mediaInformationSession.getArguments()); final ReturnCode returnCode = new ReturnCode(returnCodeValue); mediaInformationSession.complete(returnCode); if (returnCode.isSuccess()) { MediaInformation mediaInformation = MediaInformationJsonParser.fromWithError(mediaInformationSession.getAllLogsAsString(waitTimeout)); 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", FFmpegKitConfig.argumentsToString(mediaInformationSession.getArguments()), Exceptions.getStackTraceString(e))); } } /** *

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

Asynchronously executes the FFmpeg session provided. * * @param ffmpegSession FFmpeg session which includes command options/arguments * @param executorService executor service that will be used to run this asynchronous operation */ public static void asyncFFmpegExecute(final FFmpegSession ffmpegSession, final ExecutorService executorService) { AsyncFFmpegExecuteTask asyncFFmpegExecuteTask = new AsyncFFmpegExecuteTask(ffmpegSession); Future future = executorService.submit(asyncFFmpegExecuteTask); ffmpegSession.setFuture(future); } /** *

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

Asynchronously executes the FFprobe session provided. * * @param ffprobeSession FFprobe session which includes command options/arguments * @param executorService executor service that will be used to run this asynchronous operation */ public static void asyncFFprobeExecute(final FFprobeSession ffprobeSession, final ExecutorService executorService) { AsyncFFprobeExecuteTask asyncFFmpegExecuteTask = new AsyncFFprobeExecuteTask(ffprobeSession); Future future = executorService.submit(asyncFFmpegExecuteTask); ffprobeSession.setFuture(future); } /** *

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 */ public static void asyncGetMediaInformationExecute(final MediaInformationSession mediaInformationSession, final int waitTimeout) { AsyncGetMediaInformationTask asyncGetMediaInformationTask = new AsyncGetMediaInformationTask(mediaInformationSession, waitTimeout); Future future = asyncExecutorService.submit(asyncGetMediaInformationTask); mediaInformationSession.setFuture(future); } /** *

Asynchronously executes the media information session provided. * * @param mediaInformationSession media information session which includes command options/arguments * @param executorService executor service that will be used to run this asynchronous operation * @param waitTimeout max time to wait until media information is transmitted */ public static void asyncGetMediaInformationExecute(final MediaInformationSession mediaInformationSession, final ExecutorService executorService, final int waitTimeout) { AsyncGetMediaInformationTask asyncGetMediaInformationTask = new AsyncGetMediaInformationTask(mediaInformationSession, waitTimeout); Future future = executorService.submit(asyncGetMediaInformationTask); mediaInformationSession.setFuture(future); } /** * Returns the maximum number of async sessions that will be executed in parallel. * * @return maximum number of async sessions that will be executed in parallel */ public static int getAsyncConcurrencyLimit() { return asyncConcurrencyLimit; } /** * Sets the maximum number of async sessions that will be executed in parallel. If more * sessions 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 logCallback log callback function or null to disable a previously defined * callback */ public static void enableLogCallback(final LogCallback logCallback) { globalLogCallbackFunction = logCallback; } /** *

Sets a global callback function to redirect FFmpeg statistics. * * @param statisticsCallback 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 execute callback function or null to disable a previously * defined callback */ public static void enableExecuteCallback(final ExecuteCallback executeCallback) { globalExecuteCallbackFunction = executeCallback; } /** *

Returns the global execute callback function. * * @return global execute callback function */ static ExecuteCallback getExecuteCallback() { 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()); } } static String extractExtensionFromSafDisplayName(final String safDisplayName) { String rawExtension = safDisplayName; if (safDisplayName.lastIndexOf(".") >= 0) { rawExtension = safDisplayName.substring(safDisplayName.lastIndexOf(".")); } try { // workaround for https://issuetracker.google.com/issues/162440528: ANDROID_CREATE_DOCUMENT generating file names like "transcode.mp3 (2)" return new StringTokenizer(rawExtension, " .").nextToken(); } catch (final Exception e) { android.util.Log.w(TAG, String.format("Failed to extract extension from saf display name: %s.%s", safDisplayName, Exceptions.getStackTraceString(e))); return "raw"; } } /** *

Converts the given Structured Access Framework Uri ("content:…") into an * input/output url that can be used in FFmpeg and FFprobe commands. * *

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()) { 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))); } return "saf:" + fd + "." + FFmpegKitConfig.extractExtensionFromSafDisplayName(displayName); } /** *

Converts the given Structured Access Framework Uri ("content:…") into an * input url that can be used in FFmpeg and FFprobe commands. * *

Requires API Level ≥ 19. On older API levels it returns an empty url. * * @param context application context * @param uri saf uri * @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 FFmpeg and FFprobe commands. * *

Requires API Level ≥ 19. On older API levels it returns an empty url. * * @param context application context * @param uri saf uri * @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 native library 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 session history size, should be smaller than 1000 */ 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!"); } else if (sessionHistorySize > 0) { FFmpegKitConfig.sessionHistorySize = sessionHistorySize; } } /** * Adds a session to the session history. * * @param session new session */ static void addSession(final Session session) { synchronized (sessionHistoryLock) { /* * ASYNC SESSIONS CALL THIS METHOD TWICE * THIS CHECK PREVENTS ADDING THE SAME SESSION TWICE */ final boolean sessionAlreadyAdded = sessionHistoryMap.containsKey(session.getSessionId()); if (!sessionAlreadyAdded) { sessionHistoryMap.put(session.getSessionId(), session); sessionHistoryList.add(session); if (sessionHistoryList.size() > sessionHistorySize) { try { sessionHistoryList.remove(0); } catch (final IndexOutOfBoundsException ignored) { } } } } } /** * 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 created from the session history. * * @return the last session created or null if session history is empty */ public static Session getLastSession() { synchronized (sessionHistoryLock) { if (sessionHistoryList.size() > 0) { return sessionHistoryList.get(sessionHistoryList.size() - 1); } } return null; } /** * Returns the last session completed from the session history. * * @return the last session completed. If there are no completed sessions in the history this * method will return null */ public static Session getLastCompletedSession() { synchronized (sessionHistoryLock) { for (int i = sessionHistoryList.size() - 1; i >= 0; i--) { final Session session = sessionHistoryList.get(i); if (session.getState() == SessionState.COMPLETED) { return session; } } } return null; } /** *

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

Clears all, including ongoing, sessions in the session history. *

Note that callbacks cannot be triggered for deleted sessions. */ public static void clearSessions() { synchronized (sessionHistoryLock) { sessionHistoryList.clear(); sessionHistoryMap.clear(); } } /** *

Returns all FFmpeg sessions in the session history. * * @return all FFmpeg sessions in the session history */ static List getFFmpegSessions() { final LinkedList list = new LinkedList<>(); synchronized (sessionHistoryLock) { for (Session session : sessionHistoryList) { if (session.isFFmpeg()) { list.add((FFmpegSession) session); } } } return list; } /** *

Returns all FFprobe sessions in the session history. * * @return all FFprobe sessions in the session history */ static List getFFprobeSessions() { final LinkedList list = new LinkedList<>(); synchronized (sessionHistoryLock) { for (Session session : sessionHistoryList) { if (session.isFFprobe()) { list.add((FFprobeSession) session); } } } return list; } /** *

Returns sessions that have the given state. * * @param state session state * @return sessions that have the given state from the session history */ public static List getSessionsByState(final SessionState state) { final LinkedList list = new LinkedList<>(); synchronized (sessionHistoryLock) { for (Session session : sessionHistoryList) { if (session.getState() == state) { list.add(session); } } } return list; } /** * Returns the active log redirection strategy. * * @return log redirection strategy */ public static LogRedirectionStrategy getLogRedirectionStrategy() { return globalLogRedirectionStrategy; } /** *

Sets the log redirection strategy * * @param logRedirectionStrategy log redirection strategy */ public static void setLogRedirectionStrategy(final LogRedirectionStrategy logRedirectionStrategy) { FFmpegKitConfig.globalLogRedirectionStrategy = logRedirectionStrategy; } /** *

Parses the given command into arguments. Uses space character to split the arguments. * Supports single and double quote characters. * * @param command string command * @return array of arguments */ public static String[] parseArguments(final String command) { final List argumentList = new ArrayList<>(); StringBuilder currentArgument = new StringBuilder(); boolean singleQuoteStarted = false; boolean doubleQuoteStarted = false; for (int i = 0; i < command.length(); i++) { final Character previousChar; if (i > 0) { previousChar = command.charAt(i - 1); } else { previousChar = null; } final char currentChar = command.charAt(i); if (currentChar == ' ') { if (singleQuoteStarted || doubleQuoteStarted) { currentArgument.append(currentChar); } else if (currentArgument.length() > 0) { argumentList.add(currentArgument.toString()); currentArgument = new StringBuilder(); } } else if (currentChar == '\'' && (previousChar == null || previousChar != '\\')) { if (singleQuoteStarted) { singleQuoteStarted = false; } else if (doubleQuoteStarted) { currentArgument.append(currentChar); } else { singleQuoteStarted = true; } } else if (currentChar == '\"' && (previousChar == null || previousChar != '\\')) { if (doubleQuoteStarted) { doubleQuoteStarted = false; } else if (singleQuoteStarted) { currentArgument.append(currentChar); } else { doubleQuoteStarted = true; } } else { currentArgument.append(currentChar); } } if (currentArgument.length() > 0) { argumentList.add(currentArgument.toString()); } return argumentList.toArray(new String[0]); } /** *

Concatenates arguments into a string adding a space character between two arguments. * * @param arguments arguments * @return concatenated string containing all arguments */ public static String argumentsToString(final String[] arguments) { if (arguments == null) { return "null"; } StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < arguments.length; i++) { if (i > 0) { stringBuilder.append(" "); } stringBuilder.append(arguments[i]); } return stringBuilder.toString(); } /** *

Enables redirection natively. */ private static native void enableNativeRedirection(); /** *

Disables redirection natively. */ private static native void disableNativeRedirection(); /** * Returns native log level. * * @return log level */ 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. * * @return FFmpeg version */ private native static String getNativeFFmpegVersion(); /** *

Returns FFmpegKit library version natively. * * @return FFmpegKit version */ private native static String getNativeVersion(); /** *

Synchronously executes FFmpeg natively. * * @param sessionId id of the session * @param arguments FFmpeg command options/arguments as string array * @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); /** *

Synchronously executes FFprobe natively. * * @param sessionId id of the session * @param arguments FFprobe command options/arguments as string array * @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); /** *

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

Returns the number of native messages that are not transmitted to the Java callbacks for * this session natively. * * @param sessionId id of the session * @return number of native messages that are not transmitted to the Java callbacks for * this session natively */ public native static int messagesInTransmit(final long sessionId); /** *

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

Please note that creator is responsible of closing created pipes. * * @param ffmpegPipePath full path of ffmpeg pipe * @return zero on successful creation, non-zero on error */ private native static int registerNewNativeFFmpegPipe(final String ffmpegPipePath); /** *

Returns FFmpegKit library build date natively. * * @return FFmpegKit library build date */ private native static String getNativeBuildDate(); /** *

Sets an environment variable natively. * * @param variableName environment variable name * @param variableValue environment variable value * @return zero on success, non-zero on error */ private native static int setNativeEnvironmentVariable(final String variableName, final String variableValue); /** *

Registers a new ignored signal natively. Ignored signals are not handled by * FFmpegKit library. * * @param signum signal number */ private native static void ignoreNativeSignal(final int signum); }