Add an instrumentation test for video transcoding.

This commit is contained in:
Greyson Parrelli
2026-02-19 11:32:57 -05:00
committed by Cody Henthorne
parent 70dc78601a
commit 771d49bfa8
3 changed files with 191 additions and 0 deletions

1
.gitignore vendored
View File

@@ -33,3 +33,4 @@ maps.key
kls_database.db
.kotlin
lefthook-local.yml
sample-videos/

View File

@@ -3,11 +3,20 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import java.util.Properties
plugins {
id("signal-sample-app")
alias(libs.plugins.compose.compiler)
}
val localPropertiesFile = File(rootProject.projectDir, "local.properties")
val localProperties: Properties? = if (localPropertiesFile.exists()) {
Properties().apply { localPropertiesFile.inputStream().use { load(it) } }
} else {
null
}
android {
namespace = "org.thoughtcrime.video.app"
compileSdkVersion = libs.versions.compileSdk.get()
@@ -49,6 +58,16 @@ android {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
sourceSets {
val sampleVideosPath = localProperties?.getProperty("sample.videos.dir")
if (sampleVideosPath != null) {
val sampleVideosDir = File(rootProject.projectDir, sampleVideosPath)
if (sampleVideosDir.isDirectory) {
getByName("androidTest").assets.srcDir(sampleVideosDir)
}
}
}
}
dependencies {

View File

@@ -0,0 +1,171 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.video.app.transcode
import android.content.Context
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.After
import org.junit.Assert
import org.junit.Assume
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.video.StreamingTranscoder
import org.thoughtcrime.securesms.video.TranscodingPreset
import org.thoughtcrime.securesms.video.videoconverter.mediadatasource.InputStreamMediaDataSource
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
/**
* Instrumentation test that transcodes all sample video files found in the androidTest assets.
*
* To use, set `sample.videos.dir` in `local.properties` to a directory containing video files.
* Those files will be packaged as test assets and transcoded with each [TranscodingPreset].
*
* If no sample videos are configured, the tests will be skipped (not failed).
*/
@RunWith(AndroidJUnit4::class)
@LargeTest
class VideoTranscodeInstrumentationTest {
companion object {
private const val TAG = "VideoTranscodeTest"
private val VIDEO_EXTENSIONS = setOf("mp4", "mkv", "webm", "3gp", "mov")
}
private lateinit var testContext: Context
private lateinit var appContext: Context
private val tempFiles = mutableListOf<File>()
@Before
fun setUp() {
testContext = InstrumentationRegistry.getInstrumentation().context
appContext = InstrumentationRegistry.getInstrumentation().targetContext
}
@After
fun tearDown() {
tempFiles.forEach { it.delete() }
tempFiles.clear()
}
@Test
fun transcodeAllVideos_level1() {
transcodeAllVideos(TranscodingPreset.LEVEL_1)
}
@Test
fun transcodeAllVideos_level2() {
transcodeAllVideos(TranscodingPreset.LEVEL_2)
}
@Test
fun transcodeAllVideos_level3() {
transcodeAllVideos(TranscodingPreset.LEVEL_3)
}
@Test
fun transcodeAllVideos_level3H265() {
transcodeAllVideos(TranscodingPreset.LEVEL_3_H265)
}
private fun transcodeAllVideos(preset: TranscodingPreset) {
val videoFiles = getVideoFileNames()
Assume.assumeTrue(
"No sample videos found in test assets. Set 'sample.videos.dir' in local.properties to a directory containing video files.",
videoFiles.isNotEmpty()
)
Log.i(TAG, "Found ${videoFiles.size} sample video(s): $videoFiles")
val failures = mutableListOf<String>()
for (videoFileName in videoFiles) {
Log.i(TAG, "Transcoding '$videoFileName' with preset ${preset.name}...")
try {
transcodeVideo(videoFileName, preset)
Log.i(TAG, "Successfully transcoded '$videoFileName' with preset ${preset.name}")
} catch (e: Exception) {
Log.e(TAG, "Failed to transcode '$videoFileName' with preset ${preset.name}", e)
failures.add("$videoFileName: ${e::class.simpleName}: ${e.message}")
}
}
if (failures.isNotEmpty()) {
Assert.fail(
"${failures.size}/${videoFiles.size} video(s) failed transcoding with ${preset.name}:\n" +
failures.joinToString("\n")
)
}
}
private fun transcodeVideo(videoFileName: String, preset: TranscodingPreset) {
val inputFile = createTempFile("input-", "-$videoFileName")
val outputFile = createTempFile("output-${preset.name}-", "-$videoFileName")
testContext.assets.open(videoFileName).use { input ->
inputFile.outputStream().use { output ->
input.copyTo(output)
}
}
Log.i(TAG, "Copied '$videoFileName' to temp file (${inputFile.length()} bytes)")
val dataSource = FileMediaDataSource(inputFile)
val transcoder = StreamingTranscoder.createManuallyForTesting(
dataSource,
null,
preset.videoCodec,
preset.videoBitRate,
preset.audioBitRate,
preset.videoShortEdge,
true
)
outputFile.outputStream().use { outputStream ->
transcoder.transcode(
{ percent -> Log.d(TAG, " $videoFileName [${preset.name}]: $percent%") },
outputStream,
null
)
}
Assert.assertTrue(
"Transcoded output for '$videoFileName' with ${preset.name} is empty",
outputFile.length() > 0
)
Log.i(TAG, "Output for '$videoFileName' with ${preset.name}: ${outputFile.length()} bytes")
}
private fun getVideoFileNames(): List<String> {
val allFiles = testContext.assets.list("") ?: emptyArray()
return allFiles.filter { fileName ->
val ext = fileName.substringAfterLast('.', "").lowercase()
ext in VIDEO_EXTENSIONS
}.sorted()
}
private fun createTempFile(prefix: String, suffix: String): File {
val file = File.createTempFile(prefix, suffix, appContext.cacheDir)
tempFiles.add(file)
return file
}
private class FileMediaDataSource(private val file: File) : InputStreamMediaDataSource() {
override fun close() {}
override fun getSize(): Long = file.length()
override fun createInputStream(position: Long): InputStream {
val stream = FileInputStream(file)
stream.skip(position)
return stream
}
}
}