Use a real software encoder when the device has none in hardware

Running the device suite for the first time surfaced a defect the unit tests
could not: FFmpeg's *_mediacodec wrappers do not fail on a device without a
hardware encoder. They quietly bind to the platform's software codec
(c2.android.hevc.encoder on the test emulator) and encode far slower than
libx264 or libx265 would, while still presenting as the fast path. A three
second 320x240 clip did not finish inside a three minute timeout.

The Fast tier now checks whether a hardware encoder actually exists for the
target codec, and falls back to libx264/libx265 on -preset veryfast when one
does not. That is both quicker and honest about what it is doing, and the
distinction between Fast and Best survives the fallback: veryfast against
medium, rather than both collapsing to the same slow path.

The routing itself was already correct. The capability probe rightly rejects
c2.android.* as software-only, so the job was correctly sent to FFmpeg -- the
test asserting Media3 was encoding an assumption about the machine it ran on.
It now derives its expectation from the device, so it means the same thing on
hardware with a real encoder and on an emulator without one.

Verified on an API 37 emulator: 17 of 17 instrumented tests pass in 4.2s,
against six minutes of timeouts before. 65 unit tests still pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-20 08:21:37 -05:00
co-authored by Claude Opus 5
parent 438f21783f
commit 2fcacd199c
5 changed files with 107 additions and 10 deletions
@@ -8,9 +8,11 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.work.WorkInfo
import androidx.work.WorkManager
import dev.jasonmross.mediaconverter.codec.AndroidDeviceCodecs
import dev.jasonmross.mediaconverter.model.Engine
import dev.jasonmross.mediaconverter.model.OutputFormat
import dev.jasonmross.mediaconverter.model.QualityTier
import dev.jasonmross.mediaconverter.model.VideoCodec
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
@@ -69,10 +71,14 @@ class ConversionWorkerTest {
@Test
fun runsAConversionThroughWorkManager() = runBlocking {
// H.264 rather than the H.265 default: on a device with no hardware encoder
// this runs in software, and AVC is markedly quicker than HEVC there. The point
// of this test is the WorkManager round trip, not the codec.
val request = ConversionWorker.request(
inputUri = Uri.fromFile(input),
displayName = "worker_sample.mp4",
sizeBytes = input.length(),
format = OutputFormat.MP4_H264,
)
workManager.enqueue(request).result.get()
@@ -130,8 +136,18 @@ class ConversionWorkerTest {
assertTrue("wrong extension: ${output.name}", output.name.endsWith(".mp3"))
}
/**
* A Fast MP4 job takes the hardware path *when the device has one*.
*
* The expectation is derived from the device rather than assumed. Emulators
* typically expose only `c2.android.*` software codecs, which the capability probe
* correctly rejects as non-hardware, so the same job legitimately routes to FFmpeg
* there. Asserting MEDIA3 unconditionally tests the test machine, not the router.
*/
@Test
fun routesAFastMp4JobToMedia3() = runBlocking {
fun routesAFastMp4JobByDeviceCapability() = runBlocking {
val hasHardwareHevc = AndroidDeviceCodecs.get().canEncode(VideoCodec.H265)
val request = ConversionWorker.request(
inputUri = Uri.fromFile(input),
displayName = "worker_sample.mp4",
@@ -145,9 +161,15 @@ class ConversionWorkerTest {
workManager.getWorkInfoByIdFlow(request.id).first { it != null && it.state.isFinished }
}
assertEquals(WorkInfo.State.SUCCEEDED, terminal?.state)
assertEquals(
Engine.MEDIA3.name,
"job did not succeed: ${terminal?.outputData?.getString(ConversionWorker.KEY_ERROR)}",
WorkInfo.State.SUCCEEDED,
terminal?.state,
)
assertEquals(
if (hasHardwareHevc) "hardware HEVC present, expected the Media3 path"
else "no hardware HEVC encoder, expected the FFmpeg path",
if (hasHardwareHevc) Engine.MEDIA3.name else Engine.FFMPEG.name,
terminal?.outputData?.getString(ConversionWorker.KEY_ENGINE_USED),
)
}