diff --git a/app/src/main/java/com/ai/assistance/operit/core/tools/defaultTool/standard/StandardFFmpegTool.kt b/app/src/main/java/com/ai/assistance/operit/core/tools/defaultTool/standard/StandardFFmpegTool.kt index 1eff9787c6..43af2ea5dd 100644 --- a/app/src/main/java/com/ai/assistance/operit/core/tools/defaultTool/standard/StandardFFmpegTool.kt +++ b/app/src/main/java/com/ai/assistance/operit/core/tools/defaultTool/standard/StandardFFmpegTool.kt @@ -7,9 +7,8 @@ import com.ai.assistance.operit.core.tools.ToolExecutor import com.ai.assistance.operit.data.model.AITool import com.ai.assistance.operit.data.model.ToolResult import com.ai.assistance.operit.data.model.ToolValidationResult -import com.arthenica.ffmpegkit.FFmpegKit +import com.ai.assistance.operit.util.FFmpegUtil import com.arthenica.ffmpegkit.FFmpegKitConfig -import com.arthenica.ffmpegkit.FFprobeKit import com.arthenica.ffmpegkit.ReturnCode import java.io.File @@ -35,7 +34,7 @@ class StandardFFmpegToolExecutor(private val context: Context) : ToolExecutor { val startTime = System.currentTimeMillis() // 执行FFmpeg命令 - val session = FFmpegKit.execute(command) + val session = FFmpegUtil.execute(command) val returnCode = session.returnCode val output = session.output ?: "" val duration = System.currentTimeMillis() - startTime @@ -104,7 +103,7 @@ class StandardFFmpegInfoToolExecutor : ToolExecutor { info.appendLine("Build configuration: ${FFmpegKitConfig.getBuildDate()}") // 列出支持的编解码器 - val codecsSession = FFmpegKit.execute("-codecs") + val codecsSession = FFmpegUtil.execute("-codecs") val codecsOutput = codecsSession.output ?: "" val duration = System.currentTimeMillis() - startTime @@ -201,14 +200,14 @@ class StandardFFmpegConvertToolExecutor(private val context: Context) : ToolExec val startTime = System.currentTimeMillis() // 执行FFmpeg命令 - val session = FFmpegKit.execute(command) + val session = FFmpegUtil.execute(command) val returnCode = session.returnCode val output = session.output ?: "" val duration = System.currentTimeMillis() - startTime if (ReturnCode.isSuccess(returnCode)) { // 获取输出文件的媒体信息 - val mediaSession = FFprobeKit.getMediaInformation(outputPath) + val mediaSession = FFmpegUtil.getMediaInformation(outputPath) val mediaInfo = mediaSession?.mediaInformation val ffmpegResult = @@ -250,9 +249,7 @@ class StandardFFmpegConvertToolExecutor(private val context: Context) : ToolExec } .toMutableList() - // Get additional media information using FFprobe - val ffprobeSession = FFprobeKit.getMediaInformation(outputPath) - val ffprobeInfo = ffprobeSession?.mediaInformation + val ffprobeInfo = mediaInfo if (ffprobeInfo != null) { // Update stream information with FFprobe data diff --git a/app/src/main/java/com/ai/assistance/operit/util/FFmpegUtil.kt b/app/src/main/java/com/ai/assistance/operit/util/FFmpegUtil.kt index 6e189108e0..1caa62c315 100644 --- a/app/src/main/java/com/ai/assistance/operit/util/FFmpegUtil.kt +++ b/app/src/main/java/com/ai/assistance/operit/util/FFmpegUtil.kt @@ -2,15 +2,21 @@ package com.ai.assistance.operit.util import com.ai.assistance.operit.util.AppLogger import com.arthenica.ffmpegkit.FFmpegKit +import com.arthenica.ffmpegkit.FFmpegSession import com.arthenica.ffmpegkit.FFprobeKit import com.arthenica.ffmpegkit.MediaInformation +import com.arthenica.ffmpegkit.MediaInformationSession import com.arthenica.ffmpegkit.ReturnCode /** - * Utility class for FFmpeg operations + * Utility class for FFmpeg operations. + * + * FFmpegKit's bundled libavcodec is not safe for overlapping sessions. Concurrent execute/probe + * calls can destroy the same pthread mutex twice and abort the process on Android 17 FORTIFY. */ object FFmpegUtil { private const val TAG = "FFmpegUtil" + private val nativeSessionLock = Any() /** * Build a scale filter string that survives FFmpegKit argument parsing. @@ -18,13 +24,18 @@ object FFmpegUtil { */ fun scaleFilterMaxWidth(maxWidth: Int): String = "scale=min(${maxWidth}\\,iw):-2" - /** - * Execute an FFmpeg command and return if it was successful - */ + fun withNativeSession(block: () -> T): T = synchronized(nativeSessionLock) { block() } + + fun execute(command: String): FFmpegSession = + withNativeSession { FFmpegKit.execute(command) } + + fun getMediaInformation(filePath: String): MediaInformationSession = + withNativeSession { FFprobeKit.getMediaInformation(filePath) } + fun executeCommand(command: String): Boolean { try { AppLogger.d(TAG, "Executing FFmpeg command: $command") - val session = FFmpegKit.execute(command) + val session = execute(command) val returnCode = session.returnCode if (ReturnCode.isSuccess(returnCode)) { @@ -43,16 +54,12 @@ object FFmpegUtil { } } - /** - * Get media information for a file - */ fun getMediaInfo(filePath: String): MediaInformation? { return try { - val mediaInfoSession = FFprobeKit.getMediaInformation(filePath) - mediaInfoSession.mediaInformation + getMediaInformation(filePath).mediaInformation } catch (e: Exception) { AppLogger.e(TAG, "Error getting media info: ${e.message}") null } } -} +} diff --git a/app/src/test/java/com/ai/assistance/operit/util/FFmpegUtilTest.kt b/app/src/test/java/com/ai/assistance/operit/util/FFmpegUtilTest.kt new file mode 100644 index 0000000000..59d36db51e --- /dev/null +++ b/app/src/test/java/com/ai/assistance/operit/util/FFmpegUtilTest.kt @@ -0,0 +1,11 @@ +package com.ai.assistance.operit.util + +import org.junit.Assert.assertEquals +import org.junit.Test + +class FFmpegUtilTest { + @Test + fun scaleFilterEscapesCommaForFfmpegKit() { + assertEquals("scale=min(640\\,iw):-2", FFmpegUtil.scaleFilterMaxWidth(640)) + } +}