mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-02 00:17:41 +01:00
Add an instrumentation test for video transcoding.
This commit is contained in:
committed by
Cody Henthorne
parent
70dc78601a
commit
771d49bfa8
1
.gitignore
vendored
1
.gitignore
vendored
@@ -33,3 +33,4 @@ maps.key
|
||||
kls_database.db
|
||||
.kotlin
|
||||
lefthook-local.yml
|
||||
sample-videos/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user