Commit Graph
4 Commits
Author SHA1 Message Date
JMR-devandClaude Opus 5 7f951baf8f Make the two codec tables answer for each other, and stop describeAudio printing a NUL
The FFprobe codec vocabulary is written out in at least four places and none of them
had a test. Two had already drifted apart. `x264`, `hev1`, `x265` and `vp09` resolved
in `CodecNames.videoFromName` and returned null from
`AndroidDeviceCodecs.mimeForCodecName`, so the app identified the codec for the source
card and for routing and then ran the device capability check blind on the same string;
`mpeg4` ran the other way and rendered as a raw name. Nothing could notice, and the
reason is structural: a `when` cannot be enumerated, so no test can ask one table what
the other one knows.

Both are maps now, for that reason alone, and `CodecVocabularyTest` walks the two key
sets. A name added to -- or removed from -- one side alone fails the build. The one
legitimate asymmetry is listed rather than implied: `mpeg4` is decodable input with no
`VideoCodec` to name it, so `CodecNames` is right not to carry it. That list is itself
checked, because otherwise it is an escape hatch -- any future divergence could be waved
through by adding the name to it, and adding `x265` to it now fails.

THIS CHANGES BEHAVIOUR for `x264`, `hev1`, `x265` and `vp09`. A null from
`mimeForCodecName` means "unknown to us: assume the platform can handle it and let a
failed export trigger the FFmpeg fallback", which is the right policy for a name nobody
recognises and the wrong one for a name recognised one file over. A device without the
matching decoder now sends those four to FFmpeg up front instead of spending a doomed
hardware attempt to discover it. 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` still passes and that is not
evidence either way -- every `canDecode` in it is a hand-written stub that never reaches
this table.

#74 is the same family one level down. `describeVideo` answered "Unrecognised" for
`InputProbe.UNPARSEABLE` and `describeAudio` had no such arm, so an unparseable audio
codec would have fallen through to `?: name` -- and the sentinel opens with a NUL, so
the source-info card would have rendered a `Text` beginning with U+0000. The two now
share one body, which is what stops the next arm being added to one side only.

Two corrections to that ticket, taken from the file rather than from the ticket, since
it warns about exactly this:

  - 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 therefore changes no answer and 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 -- so deleting the shared arm
    now reddens three tests across both sides, not one.

Mutations run, each on the full 386-test suite:

  add "avc3" to CodecNames only    -> CodecVocabularyTest red on two counts,
                                      CodecNamesTest green: 8 tests, 0 failures, which
                                      is the ticket's point about per-table arm tests
  delete the UNPARSEABLE arm       -> CodecNamesTest red on three, one of them quoting
                                      the NUL back
  add "x265" to DECODE_ONLY_NAMES  -> CodecVocabularyTest red on the escape hatch
  delete "vp09" from the MIME map  -> CodecVocabularyTest red on three, which is the
                                      state this commit is fixing

Audio is not cross-checked, and that is a gap rather than a decision: the device
capability check is video-only, so this module has no second audio table to compare
`AUDIO_ALIASES` against. `Media3Engine.audioMimeTypeFor` is the other half and belongs
to #85. `MediaProbe.shortName` (#84) is the fourth table and is untouched here for the
same reason.

Closes #87.
Closes #74.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 22:44:55 -05:00
JMR-devandClaude Opus 5 496f1c7e73 Apply ktlintFormat
Tool output only, no hand edits, so this is safe to read with whitespace
diffing off. It is its own commit for exactly that reason: a whole-repo
reformat folded into the commit that configured the formatter would have
made both unreviewable.

What it did, mostly: trailing commas on wrapped argument lists, signatures
collapsed onto one line where they fit inside 120 columns, import order, and
four genuinely unused imports removed. Unit tests pass unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 14:10:44 -05:00
JMR-devandClaude Opus 5 2e0cf2f5d6 Let the user pick a container and codecs independently, and remux without re-encoding
OutputFormat was a closed enum of twelve (container, videoCodec, audioCodec)
triples, defended on the grounds that a closed set was what made routing
decidable. Two things it could not express: changing the container while copying
the streams, and choosing codecs per track.

OutputSpec replaces it as the vocabulary; OutputFormat stays as presets over it.
Decidability moves to ContainerCapabilities, which is explicit and unit-tested
rather than implicit in whichever combinations somebody enumerated.

The matrix is indexed by (container, codec, trackType, mode), not one boolean.
"Can MP4 carry AV1" and "can this app encode AV1" have different answers, and
copy is where the difference shows: a single flag would refuse a legitimate
remux or promise an encode neither engine can deliver.

COPY is a codec value rather than a flag, so every exhaustive `when` in the
codebase had to say what it does about copying. CopyPlanner resolves it before
anything else reads the request, and inherits ConcatPlanner's rule that an
unproven match is never a copy — a needless re-encode costs time, a wrong stream
copy costs a file that will not play.

Container now drives -f, the extension and the SAF MIME type, so Matroska
without video is .mka and MP4 without video is .m4a without a preset for each.
FLAC was declared as Container.MKV with a .flac extension, inert only while
nothing read the container; it now has its own. Six containers added: MOV, MKV
audio, MPEG-TS, AVI, FLV and WMV/ASF.

Routing asks the plan, never the request. COPY belongs to none of the capability
sets, so testing the request directly sends every remux to FFmpeg on the first
check — and nothing notices, because -c copy produces a correct file, just on
the CPU. The router also learns what Media3 can *carry* as opposed to encode:
its MP4 muxer takes AAC, Opus, Vorbis and PCM but neither MP3 nor FLAC.

MediaProbe now separates "no video track" from "could not parse" and reports the
source container, which MediaExtractor cannot supply at all. FFprobe runs on
every pick for that reason, not as a fallback.

The Advanced picker shows the whole matrix and lets an impossible combination be
selected on purpose, then explains it and offers alternatives. Convert is what
blocks the job. ConversionWorker validates too, so a stale queued spec fails with
the reason rather than being coerced into something else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 07:07:52 -05:00
JMR-devandClaude Opus 5 dd2fcf0a19 Rename the project to LibreMediaConverter
Done now rather than later: the application ID is permanent once published --
Play treats a change as an entirely different app -- so this is the last
cheap moment to choose it.

  applicationId / namespace  dev.jasonmross.mediaconverter -> org.libremediaconverter
  source tree                java/dev/jasonmross/mediaconverter -> java/org/libremediaconverter
  gradle project             AndroidMediaConverter -> LibreMediaConverter
  theme                      Theme.MediaConverter -> Theme.LibreMediaConverter
  compose theme              MediaConverterTheme -> LibreMediaConverterTheme
  display name               "Media Converter" -> "LibreMediaConverter"

org.* rather than dev.jasonmross.* because "Libre" signals a project rather
than a personal app, and a project-owned namespace lets maintainership move
later without the identifier contradicting reality.

The source trees moved with git mv so history follows the files instead of
showing 42 deletions beside 42 additions.

Verified after the rename: 66 unit tests, and 40 instrumented tests on an
API 36 emulator, 0 failures. The built APK reports org.libremediaconverter,
and no stale jasonmross, AndroidMediaConverter or MediaConverterTheme
identifiers remain anywhere in the tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 18:31:43 -05:00