Make the two codec tables answer for each other, and stop describeAudio printing a NUL #90

Merged
JMR-dev merged 2 commits from fix/codec-vocabulary-drift into main 2026-08-25 04:32:22 +00:00
JMR-dev commented 2026-08-25 03:45:36 +00:00 (Migrated from github.com)

Closes #87. Closes #74.

One defect family, two tickets, two files. The FFprobe codec vocabulary is written out in at
least four places and none of them had a test; two of the four had already stopped agreeing.

What was wrong

CodecNames.videoFromName resolved x264, hev1, x265 and vp09;
AndroidDeviceCodecs.mimeForCodecName returned null for all four. So the app identified the
codec for the source card and for routing, then ran the device capability check blind on the
same string and took the "unknown, assume the platform copes" fallthrough — a wasted hardware
attempt on exactly the inputs it had already recognised. mpeg4 ran the other way.

One level down, describeVideo answered "Unrecognised" for InputProbe.UNPARSEABLE and
describeAudio had no such arm. The sentinel opens with a NUL, so an unparseable audio codec
would have put U+0000 into a Text on the source-info card (#74).

The shape

Both tables are maps rather than when expressions, and that is the load-bearing change: a
when cannot be enumerated, so no test could ever ask one table what the other one knows. The
new CodecVocabularyTest walks both key sets and both meanings, so a name added to — or removed
from — one side alone fails the build. describeVideo/describeAudio now share one body, so
the next arm cannot be added to one side only.

The single legitimate asymmetry is listed rather than implied: DECODE_ONLY_NAMES = {"mpeg4"},
decodable input with no VideoCodec to name it. That list is itself checked in both directions,
because otherwise it is an escape hatch for the next divergence.

No fifth table. MediaProbe.kt and Media3Engine.kt are untouched — #84 and #85 own those, and
the join point for them is CodecVocabularyTest iterating CodecNames.VIDEO_ALIASES.keys.

Behaviour change, stated plainly

x264, hev1, x265, vp09 now resolve in the device capability check. Null there means
"unknown to us: assume the platform can handle it and let a failed export trigger the FFmpeg
fallback" — the right policy for a name nobody recognises, the wrong one for a name recognised
one file over. A device without the matching decoder now routes those inputs to FFmpeg up
front instead of spending a doomed hardware attempt. No input loses hardware it could have used:
each alias resolves to the MIME its canonical spelling already resolved to, so a device that has
the decoder still answers true. ConversionRouterTest passes, and that is not evidence either
way — every canDecode in it is a hand-written stub that never reaches this table.

Mutations, each on the full 386-test suite

#87's acceptance bite — add "avc3" to CodecNames.VIDEO_ALIASES only. 386 tests, 2 failed;
CodecNamesTest stayed green at 8 tests, 0 failures, which is exactly the ticket's point that
per-table arm tests would encode the disagreement:

CodecVocabularyTest > no video codec name resolves for display without also resolving for the device check FAILED
    java.lang.AssertionError: resolve in CodecNames but return null from mimeForCodecName, so the device check runs blind expected:<[]> but was:<[avc3]>
CodecVocabularyTest > the two tables agree on what each name means, not merely that they know it FAILED
    java.lang.AssertionError: avc3 is H264 in CodecNames expected:<video/avc> but was:<null>

#74 — delete the UNPARSEABLE arm again. 386 tests, 3 failed. The ? in the messages below
is the report renderer, not the value: the JUnit XML holds zero NUL bytes and substitutes ?,
because XML 1.0 cannot encode U+0000. The NUL is genuinely there at runtime, and the third
failure is the proof — it is a contains('\u0000') assertion on the returned string:

CodecNamesTest > audio descriptions degrade exactly the way video ones do FAILED
    org.junit.ComparisonFailure: expected:<[Unrecognised]> but was:<[?unparseable]>
CodecNamesTest > descriptions stay readable for unknown and missing codecs FAILED
    org.junit.ComparisonFailure: expected:<[Unrecognised]> but was:<[?unparseable]>
CodecNamesTest > no description can put a control character on the card FAILED
    java.lang.AssertionError: ?unparseable leaks the sentinel

The escape hatch — add "x265" to DECODE_ONLY_NAMES. 386 tests, 1 failed:

CodecVocabularyTest > the decode-only names are genuinely decode-only FAILED
    java.lang.AssertionError: x265 is listed as decode-only, but CodecNames does resolve it — that is a divergence being waved through rather than a documented exception expected null, but was:<H265>

The pre-fix state — delete "vp09" from the MIME map only. 3 failed, including the two
membership and meaning checks and the by-name pin of the five names #87 measured.

Two corrections to #74, from the file rather than the ticket

  • It quotes audioFromName as opening with null, InputProbe.UNPARSEABLE -> null. It did not —
    it opened with null -> null and the sentinel reached else. Naming the sentinel in the
    shared lookup changes no answer; it is documentation, not the fix.
  • It says describeVideo's arm has no test of its own. It did: descriptions stay readable for unknown and missing codecs asserts it, which is why deleting the shared arm reddens three
    tests rather than one.

Not covered

Audio has no cross-check, and that is a gap rather than a decision: the device capability check
is video-only, so this module holds no second audio table to compare AUDIO_ALIASES against.
Media3Engine.audioMimeTypeFor is the other half and belongs to #85.

🤖 Generated with Claude Code

Closes #87. Closes #74. One defect family, two tickets, two files. The FFprobe codec vocabulary is written out in at least four places and none of them had a test; two of the four had already stopped agreeing. ## What was wrong `CodecNames.videoFromName` resolved `x264`, `hev1`, `x265` and `vp09`; `AndroidDeviceCodecs.mimeForCodecName` returned null for all four. So the app identified the codec for the source card and for routing, then ran the device capability check blind on the same string and took the "unknown, assume the platform copes" fallthrough — a wasted hardware attempt on exactly the inputs it had already recognised. `mpeg4` ran the other way. One level down, `describeVideo` answered `"Unrecognised"` for `InputProbe.UNPARSEABLE` and `describeAudio` had no such arm. The sentinel opens with a NUL, so an unparseable audio codec would have put U+0000 into a `Text` on the source-info card (#74). ## The shape Both tables are **maps rather than `when` expressions**, and that is the load-bearing change: a `when` cannot be enumerated, so no test could ever ask one table what the other one knows. The new `CodecVocabularyTest` walks both key sets and both meanings, so a name added to — or removed from — one side alone fails the build. `describeVideo`/`describeAudio` now share one body, so the next arm cannot be added to one side only. The single legitimate asymmetry is listed rather than implied: `DECODE_ONLY_NAMES = {"mpeg4"}`, decodable input with no `VideoCodec` to name it. That list is itself checked in both directions, because otherwise it is an escape hatch for the next divergence. No fifth table. `MediaProbe.kt` and `Media3Engine.kt` are untouched — #84 and #85 own those, and the join point for them is `CodecVocabularyTest` iterating `CodecNames.VIDEO_ALIASES.keys`. ## Behaviour change, stated plainly `x264`, `hev1`, `x265`, `vp09` now resolve in the device capability check. Null there means "unknown to us: assume the platform can handle it and let a failed export trigger the FFmpeg fallback" — the right policy for a name nobody recognises, the wrong one for a name recognised one file over. A device **without** the matching decoder now routes those inputs to FFmpeg up front instead of spending a doomed hardware attempt. No input loses hardware it could have used: each alias resolves to the MIME its canonical spelling already resolved to, so a device that has the decoder still answers true. `ConversionRouterTest` passes, and that is not evidence either way — every `canDecode` in it is a hand-written stub that never reaches this table. ## Mutations, each on the full 386-test suite **#87's acceptance bite — add `"avc3"` to `CodecNames.VIDEO_ALIASES` only.** 386 tests, 2 failed; `CodecNamesTest` stayed green at 8 tests, 0 failures, which is exactly the ticket's point that per-table arm tests would encode the disagreement: ``` CodecVocabularyTest > no video codec name resolves for display without also resolving for the device check FAILED java.lang.AssertionError: resolve in CodecNames but return null from mimeForCodecName, so the device check runs blind expected:<[]> but was:<[avc3]> CodecVocabularyTest > the two tables agree on what each name means, not merely that they know it FAILED java.lang.AssertionError: avc3 is H264 in CodecNames expected:<video/avc> but was:<null> ``` **#74 — delete the `UNPARSEABLE` arm again.** 386 tests, 3 failed. The `?` in the messages below is the report renderer, not the value: the JUnit XML holds zero NUL bytes and substitutes `?`, because XML 1.0 cannot encode U+0000. The NUL is genuinely there at runtime, and the third failure is the proof — it is a `contains('\u0000')` assertion on the returned string: ``` CodecNamesTest > audio descriptions degrade exactly the way video ones do FAILED org.junit.ComparisonFailure: expected:<[Unrecognised]> but was:<[?unparseable]> CodecNamesTest > descriptions stay readable for unknown and missing codecs FAILED org.junit.ComparisonFailure: expected:<[Unrecognised]> but was:<[?unparseable]> CodecNamesTest > no description can put a control character on the card FAILED java.lang.AssertionError: ?unparseable leaks the sentinel ``` **The escape hatch — add `"x265"` to `DECODE_ONLY_NAMES`.** 386 tests, 1 failed: ``` CodecVocabularyTest > the decode-only names are genuinely decode-only FAILED java.lang.AssertionError: x265 is listed as decode-only, but CodecNames does resolve it — that is a divergence being waved through rather than a documented exception expected null, but was:<H265> ``` **The pre-fix state — delete `"vp09"` from the MIME map only.** 3 failed, including the two membership and meaning checks and the by-name pin of the five names #87 measured. ## Two corrections to #74, from the file rather than the ticket - It quotes `audioFromName` as opening with `null, InputProbe.UNPARSEABLE -> null`. It did not — it opened with `null -> null` and the sentinel reached `else`. Naming the sentinel in the shared lookup changes no answer; it is documentation, not the fix. - It says `describeVideo`'s arm has no test of its own. It did: `descriptions stay readable for unknown and missing codecs` asserts it, which is why deleting the shared arm reddens three tests rather than one. ## Not covered Audio has no cross-check, and that is a gap rather than a decision: the device capability check is video-only, so this module holds no second audio table to compare `AUDIO_ALIASES` against. `Media3Engine.audioMimeTypeFor` is the other half and belongs to #85. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.