An audio-only input plus Container=MP4, Video=H.265, Audio=None took the process
down. CopyPlanner drops the video track because the input has none and the
audio because the spec says so; ContainerCapabilities.validate said Valid
because it read only the spec, which names a video codec; the router said
MEDIA3; and EditedMediaItem.Builder refused the resulting composition on the
media3-transformer HandlerThread, where the throw had no caller to land on.
spec=H265+NONE plan=(Drop, Drop) validation=Valid engine=MEDIA3 reason=HARDWARE_CAPABLE
spec=H264+NONE plan=(Drop, Drop) validation=Valid engine=MEDIA3 reason=HARDWARE_CAPABLE
spec=COPY+NONE plan=(Drop, Drop) validation=Invalid(message=This file has no video track to copy.,
suggestions=[OutputSpec(container=MP4, videoCodec=NONE, audioCodec=NONE)])
engine=MEDIA3 reason=HARDWARE_CAPABLE
Matches the reported measurement exactly. One thing it adds: the COPY form was
refused, but its only suggestion — MP4/None/None — is refused by the very next
branch down, so that "one-tap fix" was a dead end.
The real defect: ContainerCapabilities.validate
The empty-file rule now asks the probe as well as the spec. The equivalence is
exact rather than approximate: CopyPlanner drops video for NONE or for an
input with no video track, and audio for NONE, so "plans to (Drop, Drop)" and
the new condition are the same set. A sweep over every non-image container ×
codec × codec against both probes asserts that, so a new container or codec
cannot reopen the gap on an axis nobody wrote a case for.
This newly refuses a combination the Advanced picker accepts today — which is
the point, since today it crashes. enabled = validation.isValid disables
Convert, and ConversionWorker re-checks before routing, so both entry points
are closed.
What it suggests now. For MP4/H.265/None on an MP3: [OutputSpec(container=MP4, videoCodec=NONE, audioCodec=COPY)] — an .m4a with
the MP3 audio copied across, which itself validates. All four faces of the rule
(H.265, H.264, Copy, None) now go through the shared repair-and-filter path and
get that same offer, replacing the None + None dead end the Copy face used to
hand back.
repairVideo is also stopped from naming a video codec for a file with no video
track. It used to fall through to "the first codec this container can encode",
so the repair offered for an MP3 was H.264 — a codec CopyPlanner then drops,
making the offer a fiction that happened to validate.
The backstop: Media3Engine
The two narrow runCatching blocks become one around the whole posted body,
with the export moved into startExport, whose contract is what makes one guard
enough: returning normally means the export is running and the listener owns the
continuation; throwing means it never started and the caller does. Cancellation
is still registered before start. Behaviour-neutral on success.
ConversionRouterTest records why validation does not make this redundant:
routing still answers MEDIA3 for a (Drop, Drop) plan, so a request that skips
validation — a job queued before the settings changed, or a direct ConversionWorker.request(...) — still arrives at the engine carrying it.
Mutations
Revert the validation change (drop the || !probe.hasVideo term):
--- a video codec named for a file with no video track and no audio is refused
java.lang.AssertionError: MP4/H.264/None on an audio-only input plans to (Drop, Drop) and would produce an empty file; it must be refused. Got Valid
--- refusing an empty output still offers a way to keep the audio
java.lang.AssertionError: expected MP4/H.265/None to be rejected
--- no valid non-image spec plans to remove both tracks
java.lang.AssertionError: OutputSpec(container=MP4, videoCodec=H264, audioCodec=NONE) on InputProbe(videoCodec=null, audioCodec=mp3, hasVideo=false, durationMs=0, kind=AUDIO_ONLY, container=MP3, width=0, height=0) plans to (Drop, Drop) — an empty file, and the composition Media3 cannot build — so it must not validate
--- every suggestion is itself valid
java.lang.AssertionError: expected OutputSpec(container=MP4, videoCodec=H265, audioCodec=NONE) to be rejected
--- a repair for a file with no video track never names a video codec
java.lang.ClassCastException: class org.libremediaconverter.model.Validation$Valid cannot be cast to class org.libremediaconverter.model.Validation$Invalid
Reverting only the repairVideo line, to check that half separately:
--- a repair for a file with no video track never names a video codec
java.lang.AssertionError: offering H.264 for a file with no video track is a fiction; CopyPlanner drops it. Suggested OutputSpec(container=MP4, videoCodec=H264, audioCodec=COPY) for OutputSpec(container=MP4, videoCodec=H265, audioCodec=NONE) expected:<NONE> but was:<H264>
Revert the backstop (restore the two narrow guards):
--- a plan that removes both tracks fails the job instead of escaping the handler thread
java.lang.AssertionError: the continuation was never resumed — the failure escaped instead of being reported: kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 10000 ms
That mutation earned its keep twice. The first version of both engine tests
asserted only failure is IllegalStateException, and the mutation passed — withTimeout raises TimeoutCancellationException, and java.util.concurrent.CancellationExceptionextendsIllegalStateException,
so the assertion called an unresumed continuation a pass. Both now assert failure !is CancellationException first.
Where the tests run
The backstop is covered on the JVM under Robolectric, which runs the real HandlerThread and the real Media3 builders — the engine really posts, really
builds, and really throws. It cannot reproduce the consequence of an escaped
throw (a JVM background thread dying is not process death), so it asserts the
half that is observable and is the half the user feels: the suspension resolves
with a reason instead of hanging.
Media3EngineTest.aPlanThatRemovesBothTracksFailsInsteadOfKillingTheProcess is
the same case against the real framework. It is deliberately not @FailsOnEmulatorApi37 — nothing here decodes or encodes, so no emulator codec
is involved. It compiles locally but has never executed: no emulator can boot on
this workstation at the moment (a VirtualBox VM holds VT-x, so every AVD dies
with KVM: entry failed, hardware error 0x0). CI is its first run.
Closes #14.
An audio-only input plus Container=MP4, Video=H.265, Audio=None took the process
down. `CopyPlanner` drops the video track because the *input* has none and the
audio because the spec says so; `ContainerCapabilities.validate` said Valid
because it read only the spec, which names a video codec; the router said
MEDIA3; and `EditedMediaItem.Builder` refused the resulting composition on the
media3-transformer HandlerThread, where the throw had no caller to land on.
## Reproduced first, on the JVM
Probe: `InputProbe(videoCodec = null, audioCodec = "mp3", hasVideo = false, kind = AUDIO_ONLY, container = MP3)`.
```
spec=H265+NONE plan=(Drop, Drop) validation=Valid engine=MEDIA3 reason=HARDWARE_CAPABLE
spec=H264+NONE plan=(Drop, Drop) validation=Valid engine=MEDIA3 reason=HARDWARE_CAPABLE
spec=COPY+NONE plan=(Drop, Drop) validation=Invalid(message=This file has no video track to copy.,
suggestions=[OutputSpec(container=MP4, videoCodec=NONE, audioCodec=NONE)])
engine=MEDIA3 reason=HARDWARE_CAPABLE
```
Matches the reported measurement exactly. One thing it adds: the COPY form was
refused, but its only suggestion — `MP4/None/None` — is refused by the very next
branch down, so that "one-tap fix" was a dead end.
## The real defect: `ContainerCapabilities.validate`
The empty-file rule now asks the probe as well as the spec. The equivalence is
exact rather than approximate: `CopyPlanner` drops video for `NONE` or for an
input with no video track, and audio for `NONE`, so "plans to (Drop, Drop)" and
the new condition are the same set. A sweep over every non-image container ×
codec × codec against both probes asserts that, so a new container or codec
cannot reopen the gap on an axis nobody wrote a case for.
This newly refuses a combination the Advanced picker accepts today — which is
the point, since today it crashes. `enabled = validation.isValid` disables
Convert, and `ConversionWorker` re-checks before routing, so both entry points
are closed.
**What it suggests now.** For `MP4/H.265/None` on an MP3:
`[OutputSpec(container=MP4, videoCodec=NONE, audioCodec=COPY)]` — an `.m4a` with
the MP3 audio copied across, which itself validates. All four faces of the rule
(H.265, H.264, Copy, None) now go through the shared repair-and-filter path and
get that same offer, replacing the `None + None` dead end the Copy face used to
hand back.
`repairVideo` is also stopped from naming a video codec for a file with no video
track. It used to fall through to "the first codec this container can encode",
so the repair offered for an MP3 was `H.264` — a codec `CopyPlanner` then drops,
making the offer a fiction that happened to validate.
## The backstop: `Media3Engine`
The two narrow `runCatching` blocks become one around the whole posted body,
with the export moved into `startExport`, whose contract is what makes one guard
enough: returning normally means the export is running and the listener owns the
continuation; throwing means it never started and the caller does. Cancellation
is still registered before `start`. Behaviour-neutral on success.
`ConversionRouterTest` records why validation does not make this redundant:
routing still answers MEDIA3 for a (Drop, Drop) plan, so a request that skips
validation — a job queued before the settings changed, or a direct
`ConversionWorker.request(...)` — still arrives at the engine carrying it.
## Mutations
**Revert the validation change** (drop the `|| !probe.hasVideo` term):
```
--- a video codec named for a file with no video track and no audio is refused
java.lang.AssertionError: MP4/H.264/None on an audio-only input plans to (Drop, Drop) and would produce an empty file; it must be refused. Got Valid
--- refusing an empty output still offers a way to keep the audio
java.lang.AssertionError: expected MP4/H.265/None to be rejected
--- no valid non-image spec plans to remove both tracks
java.lang.AssertionError: OutputSpec(container=MP4, videoCodec=H264, audioCodec=NONE) on InputProbe(videoCodec=null, audioCodec=mp3, hasVideo=false, durationMs=0, kind=AUDIO_ONLY, container=MP3, width=0, height=0) plans to (Drop, Drop) — an empty file, and the composition Media3 cannot build — so it must not validate
--- every suggestion is itself valid
java.lang.AssertionError: expected OutputSpec(container=MP4, videoCodec=H265, audioCodec=NONE) to be rejected
--- a repair for a file with no video track never names a video codec
java.lang.ClassCastException: class org.libremediaconverter.model.Validation$Valid cannot be cast to class org.libremediaconverter.model.Validation$Invalid
```
Reverting only the `repairVideo` line, to check that half separately:
```
--- a repair for a file with no video track never names a video codec
java.lang.AssertionError: offering H.264 for a file with no video track is a fiction; CopyPlanner drops it. Suggested OutputSpec(container=MP4, videoCodec=H264, audioCodec=COPY) for OutputSpec(container=MP4, videoCodec=H265, audioCodec=NONE) expected:<NONE> but was:<H264>
```
**Revert the backstop** (restore the two narrow guards):
```
--- a plan that removes both tracks fails the job instead of escaping the handler thread
java.lang.AssertionError: the continuation was never resumed — the failure escaped instead of being reported: kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 10000 ms
```
That mutation earned its keep twice. The first version of both engine tests
asserted only `failure is IllegalStateException`, and the mutation **passed** —
`withTimeout` raises `TimeoutCancellationException`, and
`java.util.concurrent.CancellationException` *extends* `IllegalStateException`,
so the assertion called an unresumed continuation a pass. Both now assert
`failure !is CancellationException` first.
## Where the tests run
The backstop is covered on the JVM under Robolectric, which runs the real
`HandlerThread` and the real Media3 builders — the engine really posts, really
builds, and really throws. It cannot reproduce the *consequence* of an escaped
throw (a JVM background thread dying is not process death), so it asserts the
half that is observable and is the half the user feels: the suspension resolves
with a reason instead of hanging.
`Media3EngineTest.aPlanThatRemovesBothTracksFailsInsteadOfKillingTheProcess` is
the same case against the real framework. It is deliberately **not**
`@FailsOnEmulatorApi37` — nothing here decodes or encodes, so no emulator codec
is involved. It compiles locally but has never executed: no emulator can boot on
this workstation at the moment (a VirtualBox VM holds VT-x, so every AVD dies
with `KVM: entry failed, hardware error 0x0`). CI is its first run.
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.
Closes #14.
An audio-only input plus Container=MP4, Video=H.265, Audio=None took the process
down.
CopyPlannerdrops the video track because the input has none and theaudio because the spec says so;
ContainerCapabilities.validatesaid Validbecause it read only the spec, which names a video codec; the router said
MEDIA3; and
EditedMediaItem.Builderrefused the resulting composition on themedia3-transformer HandlerThread, where the throw had no caller to land on.
Reproduced first, on the JVM
Probe:
InputProbe(videoCodec = null, audioCodec = "mp3", hasVideo = false, kind = AUDIO_ONLY, container = MP3).Matches the reported measurement exactly. One thing it adds: the COPY form was
refused, but its only suggestion —
MP4/None/None— is refused by the very nextbranch down, so that "one-tap fix" was a dead end.
The real defect:
ContainerCapabilities.validateThe empty-file rule now asks the probe as well as the spec. The equivalence is
exact rather than approximate:
CopyPlannerdrops video forNONEor for aninput with no video track, and audio for
NONE, so "plans to (Drop, Drop)" andthe new condition are the same set. A sweep over every non-image container ×
codec × codec against both probes asserts that, so a new container or codec
cannot reopen the gap on an axis nobody wrote a case for.
This newly refuses a combination the Advanced picker accepts today — which is
the point, since today it crashes.
enabled = validation.isValiddisablesConvert, and
ConversionWorkerre-checks before routing, so both entry pointsare closed.
What it suggests now. For
MP4/H.265/Noneon an MP3:[OutputSpec(container=MP4, videoCodec=NONE, audioCodec=COPY)]— an.m4awiththe MP3 audio copied across, which itself validates. All four faces of the rule
(H.265, H.264, Copy, None) now go through the shared repair-and-filter path and
get that same offer, replacing the
None + Nonedead end the Copy face used tohand back.
repairVideois also stopped from naming a video codec for a file with no videotrack. It used to fall through to "the first codec this container can encode",
so the repair offered for an MP3 was
H.264— a codecCopyPlannerthen drops,making the offer a fiction that happened to validate.
The backstop:
Media3EngineThe two narrow
runCatchingblocks become one around the whole posted body,with the export moved into
startExport, whose contract is what makes one guardenough: returning normally means the export is running and the listener owns the
continuation; throwing means it never started and the caller does. Cancellation
is still registered before
start. Behaviour-neutral on success.ConversionRouterTestrecords why validation does not make this redundant:routing still answers MEDIA3 for a (Drop, Drop) plan, so a request that skips
validation — a job queued before the settings changed, or a direct
ConversionWorker.request(...)— still arrives at the engine carrying it.Mutations
Revert the validation change (drop the
|| !probe.hasVideoterm):Reverting only the
repairVideoline, to check that half separately:Revert the backstop (restore the two narrow guards):
That mutation earned its keep twice. The first version of both engine tests
asserted only
failure is IllegalStateException, and the mutation passed —withTimeoutraisesTimeoutCancellationException, andjava.util.concurrent.CancellationExceptionextendsIllegalStateException,so the assertion called an unresumed continuation a pass. Both now assert
failure !is CancellationExceptionfirst.Where the tests run
The backstop is covered on the JVM under Robolectric, which runs the real
HandlerThreadand the real Media3 builders — the engine really posts, reallybuilds, and really throws. It cannot reproduce the consequence of an escaped
throw (a JVM background thread dying is not process death), so it asserts the
half that is observable and is the half the user feels: the suspension resolves
with a reason instead of hanging.
Media3EngineTest.aPlanThatRemovesBothTracksFailsInsteadOfKillingTheProcessisthe same case against the real framework. It is deliberately not
@FailsOnEmulatorApi37— nothing here decodes or encodes, so no emulator codecis involved. It compiles locally but has never executed: no emulator can boot on
this workstation at the moment (a VirtualBox VM holds VT-x, so every AVD dies
with
KVM: entry failed, hardware error 0x0). CI is its first run.