Supersedes #3, which GitHub auto-closed when its base branch was deleted on merging #2. Now based on main, with #2 merged in.
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 has a mode axis
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 either 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.
A matching codec is upgraded to a copy only when the container is changing. If container and codec both already match, the only reason to run the job is to re-encode it, most likely to compress — silently copying would hand back a byte-identical file and call it done. That assumption needs revisiting if bitrate or resolution controls are ever added.
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. ConversionRouterTest asserts the engine for that reason.
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, so remuxing an MP3 track into MP4 leaves the hardware path. Before remuxing existed nothing could reach that combination.
Containers became load-bearing
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 added: MOV, MKA, MPEG-TS, AVI, FLV, WMV/ASF.
Probing
MediaProbe 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. Matroska and WebM share a demuxer and report identical format names, so the codec is used to disambiguate — documented, with the residual ambiguity shown to be safe for the planner.
UI
Presets stay the one-tap path. The Advanced drawer shows the whole matrix and lets an impossible combination be selected on purpose, then explains it and offers one-tap 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.
CI on #2 established that Media3 can only write MP4 — WebmMuxer, OggMuxer, WavMuxer and AacMuxer all throw from addMetadataEntry, which MuxerWrapper calls unconditionally. So MEDIA3_MUXABLE_VIDEO/_AUDIO drop to a single MP4 entry, Reason.WEBM_CODEC_UNSUPPORTED is removed as unreachable, and the six new containers get null muxer branches. The remux behaviour is unaffected: MKV → MP4 was always the hardware direction, because Media3 reads Matroska but has never been able to write it.
Verification
:app:testDebugUnitTest — 133 tests green, up from 71 on main.
New: ContainerCapabilitiesTest, CopyPlannerTest, CodecNamesTest, MediaProbeFormatTest; extended router, builder and format tests.
New instrumented RemuxTest plus four committed fixtures (sample_h264.mkv, sample_aac.m4a, sample_vp9.webm, sample_still.png), recipes recorded in KDoc per the HardwareFallbackTest convention.
Debug and R8 release APKs both build.
Three bugs were caught by tests written for this change: suggestions repaired only the failing axis (offering VP9-in-WebM with AAC still attached); the MP3-in-MP4 muxer gap above, where a test asserted the opposite of the truth; and FFmpegCommandBuilder's else -> libx264, which would silently hand back H.264 for a VP8/AV1 request — structurally the same defect #2 fixed.
⚠️RemuxTest has never run — the emulator segfaults locally, so this PR's CI is its first execution. Unproven until it goes green: MKV→MP4 landing on MEDIA3, the MPEG-TS/AVI container writes, and InputKind classification against the real fixtures.
Supersedes #3, which GitHub auto-closed when its base branch was deleted on merging #2. Now based on `main`, with #2 merged in.
`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 has a mode axis
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 either 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.
A matching codec is upgraded to a copy **only when the container is changing**. If container and codec both already match, the only reason to run the job is to re-encode it, most likely to compress — silently copying would hand back a byte-identical file and call it done. That assumption needs revisiting if bitrate or resolution controls are ever added.
## 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. `ConversionRouterTest` asserts the **engine** for that reason.
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, so remuxing an MP3 track into MP4 leaves the hardware path. Before remuxing existed nothing could reach that combination.
## Containers became load-bearing
`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 added: MOV, MKA, MPEG-TS, AVI, FLV, WMV/ASF.
## Probing
`MediaProbe` 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. Matroska and WebM share a demuxer and report identical format names, so the codec is used to disambiguate — documented, with the residual ambiguity shown to be safe for the planner.
## UI
Presets stay the one-tap path. The Advanced drawer shows the whole matrix and **lets an impossible combination be selected on purpose**, then explains it and offers one-tap 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.
## Merged in from #2
CI on #2 established that Media3 can only write MP4 — `WebmMuxer`, `OggMuxer`, `WavMuxer` and `AacMuxer` all throw from `addMetadataEntry`, which `MuxerWrapper` calls unconditionally. So `MEDIA3_MUXABLE_VIDEO`/`_AUDIO` drop to a single MP4 entry, `Reason.WEBM_CODEC_UNSUPPORTED` is removed as unreachable, and the six new containers get null muxer branches. The remux behaviour is unaffected: MKV → MP4 was always the hardware direction, because Media3 reads Matroska but has never been able to write it.
## Verification
- `:app:testDebugUnitTest` — **133 tests green**, up from 71 on `main`.
- New: `ContainerCapabilitiesTest`, `CopyPlannerTest`, `CodecNamesTest`, `MediaProbeFormatTest`; extended router, builder and format tests.
- New instrumented `RemuxTest` plus four committed fixtures (`sample_h264.mkv`, `sample_aac.m4a`, `sample_vp9.webm`, `sample_still.png`), recipes recorded in KDoc per the `HardwareFallbackTest` convention.
- Debug and R8 release APKs both build.
Three bugs were caught by tests written for this change: suggestions repaired only the failing axis (offering VP9-in-WebM with AAC still attached); the MP3-in-MP4 muxer gap above, where a test asserted the opposite of the truth; and `FFmpegCommandBuilder`'s `else -> libx264`, which would silently hand back H.264 for a VP8/AV1 request — structurally the same defect #2 fixed.
⚠️ `RemuxTest` has never run — the emulator segfaults locally, so this PR's CI is its first execution. Unproven until it goes green: MKV→MP4 landing on `MEDIA3`, the MPEG-TS/AVI container writes, and `InputKind` classification against the real fixtures.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Supersedes #3, which GitHub auto-closed when its base branch was deleted on merging #2. Now based on
main, with #2 merged in.OutputFormatwas 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.OutputSpecreplaces it as the vocabulary;OutputFormatstays as presets over it. Decidability moves toContainerCapabilities, which is explicit and unit-tested rather than implicit in whichever combinations somebody enumerated.The matrix has a mode axis
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 either refuse a legitimate remux or promise an encode neither engine can deliver.COPYis a codec value rather than a flag, so every exhaustivewhenin the codebase had to say what it does about copying.CopyPlannerresolves it before anything else reads the request, and inheritsConcatPlanner'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.A matching codec is upgraded to a copy only when the container is changing. If container and codec both already match, the only reason to run the job is to re-encode it, most likely to compress — silently copying would hand back a byte-identical file and call it done. That assumption needs revisiting if bitrate or resolution controls are ever added.
Routing asks the plan, never the request
COPYbelongs 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 copyproduces a correct file, just on the CPU.ConversionRouterTestasserts the engine for that reason.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, so remuxing an MP3 track into MP4 leaves the hardware path. Before remuxing existed nothing could reach that combination.
Containers became load-bearing
Containernow drives-f, the extension and the SAF MIME type, so Matroska without video is.mkaand MP4 without video is.m4awithout a preset for each.FLACwas declared asContainer.MKVwith a.flacextension — inert only while nothing read the container; it now has its own. Six added: MOV, MKA, MPEG-TS, AVI, FLV, WMV/ASF.Probing
MediaProbeseparates "no video track" from "could not parse" and reports the source container, whichMediaExtractorcannot supply at all. FFprobe runs on every pick for that reason, not as a fallback. Matroska and WebM share a demuxer and report identical format names, so the codec is used to disambiguate — documented, with the residual ambiguity shown to be safe for the planner.UI
Presets stay the one-tap path. The Advanced drawer shows the whole matrix and lets an impossible combination be selected on purpose, then explains it and offers one-tap alternatives; Convert is what blocks the job.
ConversionWorkervalidates too, so a stale queued spec fails with the reason rather than being coerced into something else.Merged in from #2
CI on #2 established that Media3 can only write MP4 —
WebmMuxer,OggMuxer,WavMuxerandAacMuxerall throw fromaddMetadataEntry, whichMuxerWrappercalls unconditionally. SoMEDIA3_MUXABLE_VIDEO/_AUDIOdrop to a single MP4 entry,Reason.WEBM_CODEC_UNSUPPORTEDis removed as unreachable, and the six new containers get null muxer branches. The remux behaviour is unaffected: MKV → MP4 was always the hardware direction, because Media3 reads Matroska but has never been able to write it.Verification
:app:testDebugUnitTest— 133 tests green, up from 71 onmain.ContainerCapabilitiesTest,CopyPlannerTest,CodecNamesTest,MediaProbeFormatTest; extended router, builder and format tests.RemuxTestplus four committed fixtures (sample_h264.mkv,sample_aac.m4a,sample_vp9.webm,sample_still.png), recipes recorded in KDoc per theHardwareFallbackTestconvention.Three bugs were caught by tests written for this change: suggestions repaired only the failing axis (offering VP9-in-WebM with AAC still attached); the MP3-in-MP4 muxer gap above, where a test asserted the opposite of the truth; and
FFmpegCommandBuilder'selse -> libx264, which would silently hand back H.264 for a VP8/AV1 request — structurally the same defect #2 fixed.⚠️
RemuxTesthas never run — the emulator segfaults locally, so this PR's CI is its first execution. Unproven until it goes green: MKV→MP4 landing onMEDIA3, the MPEG-TS/AVI container writes, andInputKindclassification against the real fixtures.🤖 Generated with Claude Code