Merge branch 'main' into test/media3engine-mime-tables
This commit is contained in:
@@ -33,9 +33,12 @@ import org.robolectric.RobolectricTestRunner
|
||||
* representation survives a `Bundle` round trip. A JVM round-trip test on the
|
||||
* saver covers the representation.
|
||||
*
|
||||
* Robolectric rather than the instrumented suite, deliberately. The instrumented tests
|
||||
* cannot run on the development host at all (see CLAUDE.md), and a red test nobody can
|
||||
* execute is not a loop anyone can work in.
|
||||
* Robolectric rather than the instrumented suite, deliberately -- but not because the
|
||||
* instrumented suite is unavailable. It runs on this host for API 33-36
|
||||
* (`tools/local-emulator/run-e2e.sh`), and CI runs 33-37. The reason is cost: this test
|
||||
* needs a composition and a saved-state round trip, nothing a device supplies, and it runs
|
||||
* in the same `./gradlew` invocation as every other JVM test instead of booting an
|
||||
* emulator. A loop measured in seconds is a loop people stay inside.
|
||||
*/
|
||||
@UnstableApi
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package org.libremediaconverter.codec
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.libremediaconverter.model.CodecNames
|
||||
import org.libremediaconverter.model.VideoCodec
|
||||
|
||||
/**
|
||||
* Bites on #87: two tables read one codec vocabulary and had stopped agreeing.
|
||||
*
|
||||
* `CodecNames.VIDEO_ALIASES` answers "which enum is this FFprobe name", for the source-info card
|
||||
* and for routing. `AndroidDeviceCodecs.NAME_TO_MIME` answers "which MIME do I ask this device
|
||||
* about", for the capability check. On `ad28293` five names lived in one and not the other: `x264`,
|
||||
* `hev1`, `x265` and `vp09` were identified for display and then fell through the device check as
|
||||
* unknown, so the app attempted a hardware path it had enough information to skip; `mpeg4` ran the
|
||||
* other way and rendered as a raw name on the card.
|
||||
*
|
||||
* Per-table arm tests would have passed on both tables and encoded the disagreement, which is why
|
||||
* these walk the key sets instead. A name added to — or removed from — one side alone fails here.
|
||||
*/
|
||||
class CodecVocabularyTest {
|
||||
|
||||
private val aliases = CodecNames.VIDEO_ALIASES
|
||||
private val mimes = AndroidDeviceCodecs.NAME_TO_MIME
|
||||
private val decodeOnly = AndroidDeviceCodecs.DECODE_ONLY_NAMES
|
||||
|
||||
@Test
|
||||
fun `no video codec name resolves for display without also resolving for the device check`() {
|
||||
assertEquals(
|
||||
"resolve in CodecNames but return null from mimeForCodecName, so the device check runs blind",
|
||||
emptySet<String>(),
|
||||
aliases.keys - mimes.keys,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `no video codec name resolves for the device check without being a name the app can label`() {
|
||||
assertEquals(
|
||||
"resolve in AndroidDeviceCodecs but not in CodecNames, and are not listed as decode-only",
|
||||
emptySet<String>(),
|
||||
mimes.keys - aliases.keys - decodeOnly,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Membership is not enough: `"x265" to MIMETYPE_VIDEO_AVC` would satisfy both key sets and
|
||||
* still ask the device about the wrong codec.
|
||||
*/
|
||||
@Test
|
||||
fun `the two tables agree on what each name means, not merely that they know it`() {
|
||||
aliases.forEach { (name, codec) ->
|
||||
val expected = AndroidDeviceCodecs.mimeFor(codec)
|
||||
assertNotNull("$name maps to $codec, which has no MIME to ask about", expected)
|
||||
assertEquals("$name is $codec in CodecNames", expected, mimes[name])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The exception list is the escape hatch: any future divergence could be waved through by
|
||||
* adding the name to it. Guard both directions so it cannot be.
|
||||
*/
|
||||
@Test
|
||||
fun `the decode-only names are genuinely decode-only`() {
|
||||
decodeOnly.forEach { name ->
|
||||
assertNotNull("$name is listed as decode-only but the device check cannot resolve it", mimes[name])
|
||||
assertNull(
|
||||
"$name is listed as decode-only, but CodecNames does resolve it — that is a divergence " +
|
||||
"being waved through rather than a documented exception",
|
||||
CodecNames.videoFromName(name),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The five names #87 measured, pinned by name so the specific regression cannot come back
|
||||
* quietly even if someone rewrites the tables above.
|
||||
*/
|
||||
@Test
|
||||
fun `the names that used to resolve on one side only resolve on both`() {
|
||||
mapOf(
|
||||
"x264" to VideoCodec.H264,
|
||||
"hev1" to VideoCodec.H265,
|
||||
"x265" to VideoCodec.H265,
|
||||
"vp09" to VideoCodec.VP9,
|
||||
).forEach { (name, codec) ->
|
||||
assertEquals("$name is a name FFmpeg emits", codec, CodecNames.videoFromName(name))
|
||||
assertEquals(
|
||||
"$name has to reach the device check too, or the app identifies it and then asks blind",
|
||||
AndroidDeviceCodecs.mimeFor(codec),
|
||||
AndroidDeviceCodecs.mimeForCodecName(name),
|
||||
)
|
||||
}
|
||||
// The one that runs the other way: decodable input with no enum to name it.
|
||||
assertNull("mpeg4 is not an output the app can target", CodecNames.videoFromName("mpeg4"))
|
||||
assertNotNull("mpeg4 is still decodable input", AndroidDeviceCodecs.mimeForCodecName("mpeg4"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Without this the agreement test above could pass on two nulls.
|
||||
*
|
||||
* `MediaFormat.MIMETYPE_VIDEO_AVC` is a Java compile-time constant, so it is inlined and the
|
||||
* unit-test classpath's stubbed `android.jar` never has to supply it. If that ever stops being
|
||||
* true, every MIME comparison here would be `null == null` and green — the vacuous-mutation
|
||||
* failure this repo has counted before. Assert one literal so the stub fails loudly instead.
|
||||
*/
|
||||
@Test
|
||||
fun `the MIME constants are real strings rather than stubs`() {
|
||||
assertEquals("video/avc", AndroidDeviceCodecs.mimeForCodecName("h264"))
|
||||
assertEquals("video/hevc", AndroidDeviceCodecs.mimeForCodecName("hevc"))
|
||||
assertEquals("video/avc", AndroidDeviceCodecs.mimeFor(VideoCodec.H264))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `codec names are matched case-insensitively on both sides`() {
|
||||
assertEquals(VideoCodec.H265, CodecNames.videoFromName("HEV1"))
|
||||
assertEquals("video/hevc", AndroidDeviceCodecs.mimeForCodecName("HEV1"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a name neither table knows still resolves to nothing`() {
|
||||
assertNull(CodecNames.videoFromName("cinepak"))
|
||||
assertNull(AndroidDeviceCodecs.mimeForCodecName("cinepak"))
|
||||
}
|
||||
|
||||
/**
|
||||
* The behaviour #87 actually changes, at the seam that uses it.
|
||||
*
|
||||
* `canDecode` treats an unresolved name as "assume the platform copes". Before the alias
|
||||
* landed, a device with no HEVC decoder answered true for `x265` and Media3 was handed a job it
|
||||
* could not do; now the router sends it to FFmpeg without spending the attempt.
|
||||
*/
|
||||
@Test
|
||||
fun `a device without the decoder now says so for the aliases it used to wave through`() {
|
||||
val hevcOnly = AndroidDeviceCodecs.forTesting(encoders = emptySet(), decoders = setOf("video/hevc"))
|
||||
assertTrue("x265 is HEVC by another name", hevcOnly.canDecode("x265"))
|
||||
assertFalse("this device has no AVC decoder, and x264 is AVC", hevcOnly.canDecode("x264"))
|
||||
assertTrue("a name nobody knows keeps the permissive answer", hevcOnly.canDecode("cinepak"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package org.libremediaconverter.convert
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* The image-demuxer rule, which looks arbitrary until it is read as a suffix.
|
||||
*
|
||||
* `MediaProbe.classify` asks [MediaProbe.isImageFormat] before anything else, so this one boolean
|
||||
* overrides everything both probes found: true and the source-info card says "Image" and a size,
|
||||
* false and it says container, codec and length. Neither mistake fails loudly.
|
||||
*
|
||||
* The rule has two halves and they are not the same shape. `image2` is a whole format name —
|
||||
* FFprobe reports it for a numbered image sequence — while the piped demuxers are named one per
|
||||
* image codec, so `_pipe` has to be matched as a *suffix*: `png_pipe`, `jpeg_pipe`, `webp_pipe`
|
||||
* and some thirty more. Widening that suffix to a substring is the tempting simplification and it
|
||||
* is wrong, because `yuv4mpegpipe` is raw video.
|
||||
*
|
||||
* The image names were measured rather than recalled. `ffprobe -show_entries format=format_name`
|
||||
* reports `png_pipe` for a `.png`, `jpeg_pipe` for a `.jpg`, `yuv4mpegpipe` for a `.y4m`, and
|
||||
* `image2` only when that demuxer is named explicitly. The container names come from
|
||||
* [MediaProbeFormatTest], and the case and spacing variants are synthetic — those exercise the
|
||||
* normalisation rather than anything FFprobe emits.
|
||||
*
|
||||
* One real format name is deliberately not asserted either way. `image2pipe` gets a false answer
|
||||
* here, being neither `image2` nor a `_pipe` suffix, and that is inert rather than a latent bug:
|
||||
* FFprobe only selects it when the demuxer is named with `-f image2pipe`, while `probeWithFFprobe`
|
||||
* forces no format at all, so a picked image arrives as `png_pipe` or its own codec's equivalent.
|
||||
* Pinning today's answer for a name this app cannot receive would be a test about FFmpeg's command
|
||||
* line rather than about this rule.
|
||||
*/
|
||||
class MediaProbeImageFormatTest {
|
||||
|
||||
@Test
|
||||
fun `a numbered image sequence is an image`() {
|
||||
assertIsImage("image2")
|
||||
}
|
||||
|
||||
/** What a picked PNG or JPEG actually reports, and the reason the suffix rule exists. */
|
||||
@Test
|
||||
fun `the per-codec piped demuxers are images`() {
|
||||
assertIsImage("png_pipe")
|
||||
assertIsImage("jpeg_pipe")
|
||||
assertIsImage("webp_pipe")
|
||||
}
|
||||
|
||||
/**
|
||||
* The half that a substring match would break.
|
||||
*
|
||||
* `yuv4mpegpipe` contains `pipe` and is not an image: it is raw uncompressed video, and
|
||||
* describing it as an image would hide its codec, its size and its length from the card while
|
||||
* leaving the file perfectly convertible.
|
||||
*/
|
||||
@Test
|
||||
fun `a format that merely contains pipe is not an image`() {
|
||||
assertNotImage("yuv4mpegpipe")
|
||||
}
|
||||
|
||||
/** The ordinary media containers, which is what the false answer is mostly for. */
|
||||
@Test
|
||||
fun `a real container is not an image`() {
|
||||
assertNotImage("mov,mp4,m4a,3gp,3g2,mj2")
|
||||
assertNotImage("matroska,webm")
|
||||
assertNotImage("mp3")
|
||||
}
|
||||
|
||||
/**
|
||||
* FFprobe names every format sharing the demuxer, so the entry that matters can be anywhere in
|
||||
* the list — and the padding and case are normalised the same way [MediaProbe.containerFrom]
|
||||
* normalises them.
|
||||
*/
|
||||
@Test
|
||||
fun `an image entry is found anywhere in the list, whatever its spacing or case`() {
|
||||
assertIsImage("PNG_PIPE")
|
||||
assertIsImage(" image2 ")
|
||||
assertIsImage("something_else, tiff_pipe")
|
||||
}
|
||||
|
||||
/** Nothing to go on is not an image; the card falls back to describing an unknown container. */
|
||||
@Test
|
||||
fun `an empty format name is not an image`() {
|
||||
assertNotImage("")
|
||||
}
|
||||
|
||||
private fun assertIsImage(formatName: String) =
|
||||
assertTrue("isImageFormat(\"$formatName\")", MediaProbe.isImageFormat(formatName))
|
||||
|
||||
private fun assertNotImage(formatName: String) =
|
||||
assertFalse("isImageFormat(\"$formatName\")", MediaProbe.isImageFormat(formatName))
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.libremediaconverter.convert
|
||||
|
||||
import android.media.MediaFormat
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* The MIME -> short codec name table, which nothing downstream would notice going wrong.
|
||||
*
|
||||
* `MediaExtractor` answers in platform MIME spellings; the router, the copy planner and the
|
||||
* source-info card all speak FFmpeg's short names. [MediaProbe.shortName] is the one place those
|
||||
* two vocabularies meet, and most of its arms are translations rather than trimming — `video/avc`
|
||||
* is `h264`, `audio/mp4a-latm` is `aac`, `video/x-vnd.on2.vp9` is `vp9`.
|
||||
*
|
||||
* So a dropped or mistyped arm does not throw. It falls through to `substringAfter('/')` and
|
||||
* reports a different, entirely plausible-looking string. `CodecNames` carries alias lists that
|
||||
* happen to rescue some of those (`avc`, `av01`, `raw`) and not others (`mp4a-latm`,
|
||||
* `x-vnd.on2.vp9`), which is exactly why leaning on the rescue is not a plan: an unrecognised
|
||||
* codec is how a stream-copyable file quietly becomes a re-encode, and how the card ends up naming
|
||||
* a codec no user has heard of. This table is the only place those arms are pinned.
|
||||
*
|
||||
* A plain JVM test rather than Robolectric: `MediaFormat.MIMETYPE_*` are Java compile-time String
|
||||
* constants, so this test and `MediaProbe` alike carry the literals in their own bytecode and the
|
||||
* framework class is never loaded.
|
||||
*
|
||||
* Every case names its MIME in the failure message, because the MIME is the thing that has to be
|
||||
* looked up when one of these goes red.
|
||||
*/
|
||||
class MediaProbeMimeNamesTest {
|
||||
|
||||
@Test
|
||||
fun `an AVC track is reported as h264, which is what everything downstream calls it`() {
|
||||
assertShortName("h264", MediaFormat.MIMETYPE_VIDEO_AVC)
|
||||
}
|
||||
|
||||
/** On2's vendor MIME looks nothing like the codec name FFmpeg and the router use. */
|
||||
@Test
|
||||
fun `the VP8 and VP9 vendor MIMEs are reported without their vendor prefix`() {
|
||||
assertShortName("vp8", MediaFormat.MIMETYPE_VIDEO_VP8)
|
||||
assertShortName("vp9", MediaFormat.MIMETYPE_VIDEO_VP9)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AV1 and MPEG-4 are reported by codec name rather than by MIME spelling`() {
|
||||
assertShortName("av1", MediaFormat.MIMETYPE_VIDEO_AV1)
|
||||
assertShortName("mpeg4", MediaFormat.MIMETYPE_VIDEO_MPEG4)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an AAC track is reported as aac, not as the mp4a-latm its MIME says`() {
|
||||
assertShortName("aac", MediaFormat.MIMETYPE_AUDIO_AAC)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `uncompressed audio is reported as pcm, which is not what its MIME says either`() {
|
||||
assertShortName("pcm", MediaFormat.MIMETYPE_AUDIO_RAW)
|
||||
}
|
||||
|
||||
/**
|
||||
* Four arms produce exactly what the fallback would produce anyway.
|
||||
*
|
||||
* `video/hevc` -> `hevc`, `audio/opus` -> `opus`, `audio/flac` -> `flac`,
|
||||
* `audio/vorbis` -> `vorbis`: for these the `when` arm and `substringAfter('/')` agree, so
|
||||
* deleting the arm changes no observable behaviour and no test can catch it. That is a
|
||||
* property of the code rather than a gap here, and it is reported as such rather than dressed
|
||||
* up as coverage. The assertions still earn their place — they pin the promise the router is
|
||||
* given (`hevc`, whatever the MIME happens to spell) against a later edit that changes the
|
||||
* mapping rather than deleting it.
|
||||
*/
|
||||
@Test
|
||||
fun `the arms whose MIME subtype already is the short name still map to it`() {
|
||||
assertShortName("hevc", MediaFormat.MIMETYPE_VIDEO_HEVC)
|
||||
assertShortName("opus", MediaFormat.MIMETYPE_AUDIO_OPUS)
|
||||
assertShortName("flac", MediaFormat.MIMETYPE_AUDIO_FLAC)
|
||||
assertShortName("vorbis", MediaFormat.MIMETYPE_AUDIO_VORBIS)
|
||||
}
|
||||
|
||||
/**
|
||||
* The fallback, which is what makes an unlisted codec describable at all.
|
||||
*
|
||||
* These are real `MediaFormat` MIMEs with no arm of their own. Dropping the subtype is the
|
||||
* right guess far more often than reporting the whole MIME would be — FFprobe calls the first
|
||||
* of these `ac3` too.
|
||||
*/
|
||||
@Test
|
||||
fun `a MIME with no arm of its own falls back to its subtype`() {
|
||||
assertShortName("ac3", MediaFormat.MIMETYPE_AUDIO_AC3)
|
||||
assertShortName("mpeg2", MediaFormat.MIMETYPE_VIDEO_MPEG2)
|
||||
assertShortName("dolby-vision", MediaFormat.MIMETYPE_VIDEO_DOLBY_VISION)
|
||||
}
|
||||
|
||||
/**
|
||||
* The surprising half of `substringAfter`'s contract, pinned deliberately.
|
||||
*
|
||||
* With no `/` in the string it returns the whole input rather than the empty string. Today's
|
||||
* callers gate on a `video/` or `audio/` prefix so they cannot reach this, but "report what
|
||||
* you were given" rather than "report nothing" is what would keep a malformed MIME visible on
|
||||
* the card instead of blank.
|
||||
*/
|
||||
@Test
|
||||
fun `a MIME with no subtype separator is reported unchanged`() {
|
||||
assertShortName("weird", "weird")
|
||||
assertShortName("", "")
|
||||
}
|
||||
|
||||
private fun assertShortName(expected: String, mime: String) =
|
||||
assertEquals("shortName(\"$mime\")", expected, MediaProbe.shortName(mime))
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package org.libremediaconverter.convert
|
||||
|
||||
import android.media.MediaFormat
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
/**
|
||||
* Reading Int track properties out of a `MediaFormat`, which is a heterogeneous map.
|
||||
*
|
||||
* [MediaProbe.intOr] guards two different failures with one expression, and only one of them is
|
||||
* obvious. A key the format does not carry is the easy half. The other is a key it *does* carry
|
||||
* with a value of another type: `getInteger` casts rather than coerces, so a frame rate stored as
|
||||
* a Float answers with a `ClassCastException`. `probeForConcat` reads `KEY_FRAME_RATE`, which the
|
||||
* platform accepts either way, and its `catch` sits outside the track loop — so without the
|
||||
* `runCatching` one oddly-typed field would discard the codec and dimensions already read from
|
||||
* that file and the join would re-encode for no reason.
|
||||
*
|
||||
* Robolectric rather than a plain JVM test, unlike the two sibling `MediaProbe` helper tests: this
|
||||
* one needs a real `MediaFormat` instance, not just its compile-time String constants.
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class MediaProbeTrackFieldsTest {
|
||||
|
||||
@Test
|
||||
fun `a property the format carries as an Int is read`() {
|
||||
val format = videoFormat()
|
||||
|
||||
assertEquals(1920, with(MediaProbe) { format.intOr(MediaFormat.KEY_WIDTH) })
|
||||
assertEquals(1080, with(MediaProbe) { format.intOr(MediaFormat.KEY_HEIGHT) })
|
||||
}
|
||||
|
||||
/**
|
||||
* A track that simply does not say. `MediaExtractor` omits `KEY_FRAME_RATE` for plenty of real
|
||||
* files, and 0 is what `ConcatPlanner` reads as "cannot prove a match".
|
||||
*/
|
||||
@Test
|
||||
fun `a key the format does not carry gives the fallback`() {
|
||||
val format = videoFormat()
|
||||
|
||||
assertEquals(0, with(MediaProbe) { format.intOr(MediaFormat.KEY_FRAME_RATE) })
|
||||
assertEquals(-1, with(MediaProbe) { format.intOr(MediaFormat.KEY_FRAME_RATE, -1) })
|
||||
}
|
||||
|
||||
/**
|
||||
* The premise of the `runCatching`, pinned against the platform rather than assumed.
|
||||
*
|
||||
* If `getInteger` coerced a Float instead of throwing, the guard below would be testing
|
||||
* nothing at all — so the throw is asserted directly first.
|
||||
*/
|
||||
@Test
|
||||
fun `getInteger refuses a Float rather than coercing it`() {
|
||||
val format = videoFormat()
|
||||
format.setFloat(MediaFormat.KEY_FRAME_RATE, NON_INTEGRAL_FRAME_RATE)
|
||||
|
||||
val thrown = runCatching { format.getInteger(MediaFormat.KEY_FRAME_RATE) }.exceptionOrNull()
|
||||
|
||||
assertTrue("expected getInteger to refuse a Float, got $thrown", thrown is ClassCastException)
|
||||
}
|
||||
|
||||
/** And that refusal is answered with the fallback, not passed on to the caller. */
|
||||
@Test
|
||||
fun `a frame rate the format carries as a Float gives the fallback rather than throwing`() {
|
||||
val format = videoFormat()
|
||||
format.setFloat(MediaFormat.KEY_FRAME_RATE, NON_INTEGRAL_FRAME_RATE)
|
||||
|
||||
assertEquals(0, with(MediaProbe) { format.intOr(MediaFormat.KEY_FRAME_RATE) })
|
||||
assertEquals(-1, with(MediaProbe) { format.intOr(MediaFormat.KEY_FRAME_RATE, -1) })
|
||||
}
|
||||
|
||||
private fun videoFormat(): MediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 1920, 1080)
|
||||
|
||||
private companion object {
|
||||
/** NTSC's 30000/1001, the frame rate that cannot be stored as an Int in the first place. */
|
||||
const val NON_INTEGRAL_FRAME_RATE = 29.97f
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,10 @@ import java.util.UUID
|
||||
* the actual filesystem — the same calls `reset()` makes, without needing a ViewModel (both
|
||||
* of those construct a `WorkManager`, which is not initialised on the JVM classpath).
|
||||
*
|
||||
* The instrumented suite cannot run on the development host, so this is the only place the
|
||||
* "Start over leaks a full-size copy" defect can be caught before CI.
|
||||
* The instrumented suite could also catch the "Start over leaks a full-size copy" defect --
|
||||
* it runs on this host for API 33-36 (`tools/local-emulator/run-e2e.sh`) and on CI for
|
||||
* 33-37. Here rather than there because a real `cacheDir` is all the defect needs, and
|
||||
* finding it costs an emulator boot there and a few seconds here.
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class OutputPublisherStagingTest {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.libremediaconverter.model
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
@@ -10,6 +11,16 @@ import org.junit.Test
|
||||
* Three vocabularies meet: `MediaExtractor` MIME types, FFprobe `codec_name` strings, and the
|
||||
* enums. Stream copy depends on the round trip, so a missing alias here shows up as "we could not
|
||||
* identify the source codec" and silently costs the user a re-encode.
|
||||
*
|
||||
* Also bites on #74: `describeVideo` and `describeAudio` are one function apiece over one
|
||||
* vocabulary and had stopped matching. Only the video side special-cased
|
||||
* [InputProbe.UNPARSEABLE]; the audio side fell through to the raw name, and that sentinel opens
|
||||
* with a NUL, so the source-info card would have rendered a control character. The arms are shared
|
||||
* now, and the tests below assert both sides so the symmetric bug cannot reappear on the other one.
|
||||
*
|
||||
* The tables these read are cross-checked against the device capability check by
|
||||
* `CodecVocabularyTest` (#87). Deliberately not repeated here: this file is what each name means,
|
||||
* that one is whether the app's two copies of the vocabulary still agree.
|
||||
*/
|
||||
class CodecNamesTest {
|
||||
|
||||
@@ -48,4 +59,52 @@ class CodecNamesTest {
|
||||
// An unrecognised but real codec name is more useful shown than hidden.
|
||||
assertEquals("cinepak", CodecNames.describeVideo("cinepak"))
|
||||
}
|
||||
|
||||
/** The audio row of the same card, which had none of the above. */
|
||||
@Test
|
||||
fun `audio descriptions degrade exactly the way video ones do`() {
|
||||
assertEquals("AAC", CodecNames.describeAudio("mp4a"))
|
||||
assertEquals("Unknown", CodecNames.describeAudio(null))
|
||||
assertEquals("Unrecognised", CodecNames.describeAudio(InputProbe.UNPARSEABLE))
|
||||
assertEquals("qdm2", CodecNames.describeAudio("qdm2"))
|
||||
}
|
||||
|
||||
/**
|
||||
* #74's actual failure mode, stated as the thing the user would have seen.
|
||||
*
|
||||
* `InputProbe.UNPARSEABLE` is `"\u0000unparseable"`. Falling through to `?: name` does not
|
||||
* mislabel the track, it puts U+0000 into a `Text`.
|
||||
*/
|
||||
@Test
|
||||
fun `no description can put a control character on the card`() {
|
||||
listOf(CodecNames.describeAudio(InputProbe.UNPARSEABLE), CodecNames.describeVideo(InputProbe.UNPARSEABLE))
|
||||
.forEach { assertFalse("$it leaks the sentinel", it.contains('\u0000')) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Every alias, pinned one at a time.
|
||||
*
|
||||
* The tables became maps so `CodecVocabularyTest` could enumerate them; this is what catches a
|
||||
* key mistyped or a value pointing at the wrong enum while that rewrite happened.
|
||||
*/
|
||||
@Test
|
||||
fun `every name in the tables resolves to the codec it spells`() {
|
||||
CodecNames.VIDEO_ALIASES.forEach { (name, codec) ->
|
||||
assertEquals(name, codec, CodecNames.videoFromName(name))
|
||||
}
|
||||
CodecNames.AUDIO_ALIASES.forEach { (name, codec) ->
|
||||
assertEquals(name, codec, CodecNames.audioFromName(name))
|
||||
}
|
||||
assertEquals(VideoCodec.H264, CodecNames.videoFromName("x264"))
|
||||
assertEquals(VideoCodec.VP9, CodecNames.videoFromName("vp09"))
|
||||
assertEquals(AudioCodec.MP3, CodecNames.audioFromName("mpga"))
|
||||
assertEquals(AudioCodec.OPUS, CodecNames.audioFromName("opus"))
|
||||
}
|
||||
|
||||
/** The audio lookup reads the sentinel the same way the video one does. */
|
||||
@Test
|
||||
fun `the unparseable sentinel resolves to nothing on the audio side too`() {
|
||||
assertNull(CodecNames.audioFromName(InputProbe.UNPARSEABLE))
|
||||
assertNull(CodecNames.audioFromName(null))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user