Files
LibreMediaConverter/app/src/androidTest/java/org/libremediaconverter/ffmpeg/FFmpegEngineTest.kt
T
JMR-devandClaude Opus 5 496f1c7e73 Apply ktlintFormat
Tool output only, no hand edits, so this is safe to read with whitespace
diffing off. It is its own commit for exactly that reason: a whole-repo
reformat folded into the commit that configured the formatter would have
made both unreviewable.

What it did, mostly: trailing commas on wrapped argument lists, signatures
collapsed onto one line where they fit inside 120 columns, import order, and
four genuinely unused imports removed. Unit tests pass unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 14:10:44 -05:00

170 lines
5.9 KiB
Kotlin

package org.libremediaconverter.ffmpeg
import android.media.MediaExtractor
import android.media.MediaFormat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.libremediaconverter.model.ConversionRequest
import org.libremediaconverter.model.OutputFormat
import org.libremediaconverter.model.QualityTier
import java.io.File
/**
* Exercises the bundled FFmpeg on-device.
*
* These are the formats the app exists for that Media3 structurally cannot produce, so
* they are also the ones with no other coverage. Each test asserts against the produced
* file rather than the exit code, because FFmpeg will happily report success after
* writing something unplayable if the arguments are wrong.
*/
@RunWith(AndroidJUnit4::class)
class FFmpegEngineTest {
private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val engine: org.libremediaconverter.convert.SoftwareTranscoder = FFmpegEngine()
private lateinit var input: File
private val outputs = mutableListOf<File>()
@Before
fun setUp() {
input = File(context.cacheDir, "ffmpeg_sample.mp4")
InstrumentationRegistry.getInstrumentation().context.assets
.open("sample_h264.mp4")
.use { asset -> input.outputStream().use { asset.copyTo(it) } }
}
@After
fun tearDown() {
input.delete()
outputs.forEach { it.delete() }
}
private fun outputFor(name: String) = File(context.cacheDir, name).also {
it.delete()
outputs += it
}
private fun convert(format: OutputFormat, quality: QualityTier = QualityTier.BEST): File {
val out = outputFor("out_${format.name.lowercase()}.${format.extension}")
runBlocking {
engine.run(
request = ConversionRequest(spec = format.spec, quality = quality),
inputPath = input.absolutePath,
output = out,
durationMs = 3_000,
)
}
return out
}
private fun trackMimes(file: File): List<String> {
val extractor = MediaExtractor()
return try {
extractor.setDataSource(file.absolutePath)
(0 until extractor.trackCount).map {
extractor.getTrackFormat(it).getString(MediaFormat.KEY_MIME).orEmpty()
}
} finally {
extractor.release()
}
}
// --- the formats that justify bundling FFmpeg at all -------------------
@Test
fun encodesMp3WhichAndroidCannotDoAtAnyApiLevel() {
val out = convert(OutputFormat.MP3)
assertTrue("no MP3 produced", out.exists() && out.length() > 0)
assertTrue(
"expected an mpeg audio track, got ${trackMimes(out)}",
trackMimes(out).any { it.contains("mp") && it.startsWith("audio/") },
)
}
@Test
fun encodesGifWithAGeneratedPalette() {
val out = convert(OutputFormat.GIF)
assertTrue("no GIF produced", out.exists() && out.length() > 0)
// GIF87a / GIF89a magic. Proves a real GIF rather than a mislabelled file.
val magic = out.inputStream().use { String(it.readNBytes(6)) }
assertTrue("not a GIF: $magic", magic.startsWith("GIF"))
}
@Test
fun encodesMatroskaWhichMedia3CannotMux() {
val out = convert(OutputFormat.MKV_H264)
assertTrue("no MKV produced", out.exists() && out.length() > 0)
// EBML magic, the Matroska container header.
val magic = out.inputStream().use { it.readNBytes(4) }
assertEquals(0x1A.toByte(), magic[0])
assertEquals(0x45.toByte(), magic[1])
assertEquals(0xDF.toByte(), magic[2])
assertEquals(0xA3.toByte(), magic[3])
}
@Test
fun encodesFlacLosslessAudio() {
val out = convert(OutputFormat.FLAC)
assertTrue("no FLAC produced", out.exists() && out.length() > 0)
}
@Test
fun encodesWav() {
val out = convert(OutputFormat.WAV)
assertTrue("no WAV produced", out.exists() && out.length() > 0)
val magic = out.inputStream().use { String(it.readNBytes(4)) }
assertEquals("RIFF", magic)
}
@Test
fun encodesOpus() {
val out = convert(OutputFormat.OPUS)
assertTrue("no Opus produced", out.exists() && out.length() > 0)
}
// --- the quality tier the GPL licence was taken for --------------------
@Test
fun bestQualityProducesAPlayableH264File() {
val out = convert(OutputFormat.MP4_H264, QualityTier.BEST)
assertTrue("no MP4 produced", out.exists() && out.length() > 0)
assertTrue(
"expected an AVC track, got ${trackMimes(out)}",
trackMimes(out).any { it == MediaFormat.MIMETYPE_VIDEO_AVC },
)
}
@Test
fun bestQualityProducesAPlayableH265File() {
val out = convert(OutputFormat.MP4_H265, QualityTier.BEST)
assertTrue("no MP4 produced", out.exists() && out.length() > 0)
assertTrue(
"expected an HEVC track, got ${trackMimes(out)}",
trackMimes(out).any { it == MediaFormat.MIMETYPE_VIDEO_HEVC },
)
}
@Test
fun failureSurfacesAsAnExceptionRatherThanASilentEmptyFile() {
val out = outputFor("nope.mp4")
val failure = runCatching {
runBlocking {
engine.run(
request = ConversionRequest(spec = OutputFormat.MP4_H264.spec),
inputPath = "/does/not/exist.mp4",
output = out,
durationMs = 1_000,
)
}
}.exceptionOrNull()
assertTrue("expected an FFmpegException, got $failure", failure is FFmpegEngine.FFmpegException)
}
}