Skip to content

Commit

Permalink
feat: add ffmpeg command
Browse files Browse the repository at this point in the history
  • Loading branch information
shaksternano committed May 17, 2024
1 parent e0043c9 commit 218235c
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ suspend fun <T> Iterable<T>.parallelForEach(action: suspend (T) -> Unit) {
parallelMap(action)
}

fun <T> Iterable<T>.indicesOf(element: T): List<Int> =
mapIndexedNotNull { index, element1 ->
index.takeIf { element1 == element }
}

fun <K, V> MutableMap<K, V>.putAllKeys(keys: Iterable<K>, value: V) = keys.forEach {
put(it, value)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,13 @@ import java.nio.file.StandardOpenOption
import java.util.regex.Pattern
import kotlin.io.path.*
import kotlin.io.use
import kotlin.random.Random
import kotlin.random.nextULong

private object IOUtil

private val TEMP_DIR: Path = Path(System.getProperty("java.io.tmpdir"))

val ALLOWED_DOMAINS: Set<String> = setOf(
"raw.githubusercontent.com",
"cdn.discordapp.com",
Expand All @@ -56,6 +60,23 @@ suspend fun createTemporaryFile(filenameWithoutExtension: String, extension: Str
return path
}

suspend fun getTemporaryFile(filename: String): Path = getTemporaryFile(
filenameWithoutExtension(filename),
fileExtension(filename),
)

suspend fun getTemporaryFile(filenameWithoutExtension: String, extension: String): Path {
val extension1 = extension.ifBlank { "tmp" }
var filename = ""
var path = TEMP_DIR.resolve(filename)
while (filename.isBlank() || withContext(Dispatchers.IO) { path.exists() }) {
filename = filenameWithoutExtension + Random.nextULong() + "." + extension1
path = TEMP_DIR.resolve(filename)
}
path.toFile().deleteOnExit()
return path
}

suspend fun getResource(resourcePath: String): InputStream =
withContext(Dispatchers.IO) {
IOUtil.javaClass.classLoader.getResourceAsStream(resourcePath)
Expand Down Expand Up @@ -211,9 +232,8 @@ fun removeQueryParams(url: String): String =
url.split('?').first()

fun filename(filePath: String): String {
val noQueryParams = removeQueryParams(filePath)
val nameWithoutExtension = Files.getNameWithoutExtension(noQueryParams)
val extension = Files.getFileExtension(noQueryParams)
val nameWithoutExtension = filenameWithoutExtension(filePath)
val extension = fileExtension(filePath)
return filename(nameWithoutExtension, extension)
}

Expand Down Expand Up @@ -258,3 +278,15 @@ suspend fun InputStream.readNBytesSuspend(n: Int): ByteArray = withContext(Dispa
suspend fun InputStream.skipNBytesSuspend(n: Long) = withContext(Dispatchers.IO) {
skipNBytes(n)
}

suspend inline fun <R> DataSource.useFile(block: (FileDataSource) -> R): R {
val isInputTemp = path == null
val fileInput = getOrWriteFile()
return try {
block(fileInput)
} finally {
if (isInputTemp) {
fileInput.path.deleteSilently()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.github.shaksternano.borgar.core.io.task

import io.github.shaksternano.borgar.core.collect.indicesOf
import io.github.shaksternano.borgar.core.exception.ErrorResponseException
import io.github.shaksternano.borgar.core.io.*
import io.github.shaksternano.borgar.core.util.splitWords
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.bytedeco.ffmpeg.ffmpeg
import org.bytedeco.javacpp.Loader
import kotlin.io.path.Path
import kotlin.io.path.absolutePathString

private val FFMPEG_PATH: String = Loader.load(ffmpeg::class.java)

class FFmpegTask(
private val arguments: String,
) : MappedFileTask() {

override suspend fun process(input: DataSource): DataSource = input.useFile { fileInput ->
val inputPath = fileInput.path
val splitArguments = arguments.splitWords()
val inputIndices = splitArguments.indicesOf("-i")
val ffmpegArguments = splitArguments.filterIndexed { index, _ ->
(index !in inputIndices && index - 1 !in inputIndices)
}.ifEmpty {
listOf(inputPath.toString())
}
val output = ffmpegArguments.last()
if (fileExtension(output).isBlank()) {
throw ErrorResponseException("Output filename must have an extension!")
}
val outputFilename = Path(output).filename
val outputPath = getTemporaryFile(outputFilename)
outputPath.toFile().deleteOnExit()
val ffmpegCommand = listOf(FFMPEG_PATH, "-i", inputPath.absolutePathString()) +
ffmpegArguments.subList(0, ffmpegArguments.lastIndex) +
outputPath.absolutePathString()
val processBuilder = ProcessBuilder(ffmpegCommand)
.redirectOutput(ProcessBuilder.Redirect.DISCARD)
.redirectError(ProcessBuilder.Redirect.DISCARD)
withContext(Dispatchers.IO) {
val process = processBuilder.start()
val exitCode = process.waitFor()
if (exitCode != 0) {
throw ErrorResponseException("FFmpeg command failed!")
}
}
DataSource.fromFile(outputPath, outputFilename)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,41 +83,32 @@ suspend fun processMedia(
input: DataSource,
config: MediaProcessingConfig,
maxFileSize: Long,
): FileDataSource {
val isTempFile = input.path == null
val fileInput = input.getOrWriteFile()
val path = fileInput.path
return try {
val inputFormat = input.fileFormat().ifBlank { "mp4" }
val imageReader = createImageReader(fileInput, inputFormat)
val audioReader = createAudioReader(fileInput, inputFormat)
val supportedInputFormat =
if (isReaderFormatSupported(inputFormat) && !isWriterFormatSupported(inputFormat))
if (imageReader.frameCount == 1) STATIC_FORMAT_MAPPING[inputFormat] ?: "png"
else ANIMATED_FORMAT_MAPPING[inputFormat] ?: "mp4"
else
inputFormat
val outputFormat = config.transformOutputFormat(supportedInputFormat)
val outputName = config.outputName.ifBlank {
fileInput.filenameWithoutExtension
}
val output = processMedia(
config.transformImageReader(imageReader, outputFormat),
config.transformAudioReader(audioReader, outputFormat),
createTemporaryFile(outputName, outputFormat),
outputFormat,
maxFileSize,
)
val filename = filename(outputName, outputFormat)
DataSource.fromFile(
output,
filename,
)
} finally {
if (isTempFile) {
path.deleteSilently()
}
): FileDataSource = input.useFile { fileInput ->
val inputFormat = input.fileFormat().ifBlank { "mp4" }
val imageReader = createImageReader(fileInput, inputFormat)
val audioReader = createAudioReader(fileInput, inputFormat)
val supportedInputFormat =
if (isReaderFormatSupported(inputFormat) && !isWriterFormatSupported(inputFormat))
if (imageReader.frameCount == 1) STATIC_FORMAT_MAPPING[inputFormat] ?: "png"
else ANIMATED_FORMAT_MAPPING[inputFormat] ?: "mp4"
else
inputFormat
val outputFormat = config.transformOutputFormat(supportedInputFormat)
val outputName = config.outputName.ifBlank {
fileInput.filenameWithoutExtension
}
val output = processMedia(
config.transformImageReader(imageReader, outputFormat),
config.transformAudioReader(audioReader, outputFormat),
createTemporaryFile(outputName, outputFormat),
outputFormat,
maxFileSize,
)
val filename = filename(outputName, outputFormat)
DataSource.fromFile(
output,
filename,
)
}

private suspend fun processMedia(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ fun CharSequence.endOfWord(startIndex: Int): Int {
}

fun CharSequence.splitWords(limit: Int = 0): List<String> =
split(WHITE_SPACE_REGEX, limit)
split(WHITE_SPACE_REGEX, limit).filter { it.isNotBlank() }

fun String.splitChunks(limit: Int): List<String> = SplitUtil.split(
this,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ val COMMANDS: Map<String, Command> = registerCommands(
GuildBannerCommand,
GuildSplashCommand,
DownloadCommand,
FFmpegCommand,
CatCommand.CAT,
CatCommand.CAT_BOMB,
DerpibooruCommand.DERPIBOORU,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.github.shaksternano.borgar.messaging.command

import io.github.shaksternano.borgar.core.io.task.FFmpegTask
import io.github.shaksternano.borgar.core.io.task.FileTask
import io.github.shaksternano.borgar.messaging.event.CommandEvent

object FFmpegCommand : FileCommand(
CommandArgumentInfo(
key = "arguments",
description = "The FFmpeg arguments.",
type = CommandArgumentType.String,
required = false,
),
) {

override val name: String = "ffmpeg"
override val description: String = "Runs an FFmpeg command. The input is specified automatically."

override suspend fun createTask(arguments: CommandArguments, event: CommandEvent, maxFileSize: Long): FileTask {
val ffmpegArguments = arguments.getDefaultStringOrEmpty()
return FFmpegTask(ffmpegArguments)
}
}

0 comments on commit 218235c

Please sign in to comment.