Merge Media3's MP4-only correction into the remux branch
CI on the parent branch proved that four of the five containers the router claimed for Media3 cannot be written by Transformer at all: WebmMuxer, OggMuxer, WavMuxer and AacMuxer each throw UnsupportedOperationException from addMetadataEntry, which MuxerWrapper calls for every metadata entry on the track format. Consequences here beyond the merge itself: - MEDIA3_MUXABLE_VIDEO and MEDIA3_MUXABLE_AUDIO drop to a single MP4 entry. Every other container is already on its way to FFmpeg before those maps are consulted. - Reason.WEBM_CODEC_UNSUPPORTED is removed. WebM now fails the container check first, so nothing could ever produce that reason, and a routing reason no code path can reach is worse than no reason at all. - Media3Muxers gains null branches for the six containers this branch adds. MOV is among them despite being MP4's own family: Mp4Muxer exposes no QuickTime file format. - The README no longer claims Media3 writes five containers. The remux behaviour this branch exists for is unaffected: MKV -> MP4 was always the hardware direction, because Media3 reads Matroska but has never been able to write it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,56 +1,59 @@
|
||||
package org.libremediaconverter.convert
|
||||
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.muxer.AacMuxer
|
||||
import androidx.media3.muxer.Muxer
|
||||
import androidx.media3.muxer.MuxerException
|
||||
import androidx.media3.muxer.OggMuxer
|
||||
import androidx.media3.muxer.SeekableMuxerOutput
|
||||
import androidx.media3.muxer.WavMuxer
|
||||
import androidx.media3.muxer.WebmMuxer
|
||||
import androidx.media3.transformer.DefaultMuxer
|
||||
import com.google.common.collect.ImmutableList
|
||||
import org.libremediaconverter.model.Container
|
||||
import java.io.FileOutputStream
|
||||
|
||||
/**
|
||||
* Muxer factories for the containers Media3 can write.
|
||||
* Which containers Media3 can actually write, and why it is only one.
|
||||
*
|
||||
* `media3-muxer` ships `Mp4Muxer`, `WebmMuxer`, `OggMuxer`, `WavMuxer` and `AacMuxer`, but
|
||||
* `media3-transformer` only wraps the MP4 ones as a [Muxer.Factory]. Everything else needs the
|
||||
* three lines of glue below, which is why the app previously wrote MP4 no matter what container
|
||||
* was asked for: [androidx.media3.transformer.Transformer.Builder] defaults to
|
||||
* `DefaultMuxer.Factory`, and nothing ever overrode it.
|
||||
* ## The four other muxers cannot be driven by Transformer
|
||||
*
|
||||
* ## The MIME lists are load-bearing
|
||||
* `media3-muxer` 1.11.0 ships `WebmMuxer`, `OggMuxer`, `WavMuxer` and `AacMuxer` alongside the MP4
|
||||
* ones, which reads like four more containers on the hardware path. It is not: **all four throw
|
||||
* `UnsupportedOperationException` from `addMetadataEntry`**, and
|
||||
* `MuxerWrapper.addTrackFormat` calls it for every metadata entry on the track format. Any real
|
||||
* recording carries some — a creation timestamp is enough — so the export dies partway through:
|
||||
*
|
||||
* `Transformer` calls [Muxer.Factory.getSupportedSampleMimeTypes] to decide whether a track can be
|
||||
* copied through or has to be re-encoded, and `Transformer.Builder.build()` validates the
|
||||
* requested MIME types against it. A list that over-claims produces a file the muxer cannot
|
||||
* actually write; one that under-claims forces a needless re-encode. The values here were read out
|
||||
* of each muxer's own `isMimeTypeSupported` check rather than assumed.
|
||||
* ```
|
||||
* Caused by: java.lang.UnsupportedOperationException
|
||||
* at androidx.media3.muxer.OggMuxer.addMetadataEntry(OggMuxer.java:123)
|
||||
* at androidx.media3.transformer.MuxerWrapper.addTrackFormat(MuxerWrapper.java:488)
|
||||
* ```
|
||||
*
|
||||
* They are standalone muxers, not Transformer-compatible ones. WAV fails a second way even before
|
||||
* that: `DefaultEncoderFactory` has no PCM encoder, so Transformer reports "No MIME type is
|
||||
* supported by both encoder and muxer" rather than passing raw samples through.
|
||||
*
|
||||
* Both were observed on a CI API 35 emulator, not inferred — the tests that found them were written
|
||||
* on the assumption these containers worked.
|
||||
*
|
||||
* So Media3 writes MP4 and nothing else. WebM, Ogg, WAV and raw AAC belong to FFmpeg, which already
|
||||
* produces all of them and has instrumented coverage asserting the produced files.
|
||||
*/
|
||||
@UnstableApi
|
||||
object Media3Muxers {
|
||||
|
||||
/**
|
||||
* The factory for [container], or null when Media3 cannot mux it at all.
|
||||
* The factory for [container], or null when Media3 cannot write it.
|
||||
*
|
||||
* A null here must agree with [org.libremediaconverter.model.ConversionRouter]'s container set
|
||||
* — if the router sends Media3 a job this cannot mux, the conversion fails and falls back to
|
||||
* FFmpeg, which is a slow way to discover a routing bug. `Media3MuxersTest` asserts they agree.
|
||||
* A null here must agree with [org.libremediaconverter.model.ConversionRouter]'s container set;
|
||||
* `Media3MuxersTest` asserts they do. They drifted once already, and expensively: the router
|
||||
* claimed five containers while the engine silently wrote MP4 for all of them.
|
||||
*/
|
||||
fun factoryFor(container: Container): Muxer.Factory? = when (container) {
|
||||
// Transformer's own default. Named explicitly so the MP4 path reads the same as the rest.
|
||||
// Transformer's own default. Named explicitly so the engine states its container rather
|
||||
// than inheriting one, which is how the MP4-for-everything bug went unnoticed.
|
||||
Container.MP4 -> DefaultMuxer.Factory()
|
||||
Container.WEBM -> WebmFactory
|
||||
Container.OGG -> OggFactory
|
||||
Container.WAV -> WavFactory
|
||||
Container.AAC_ADTS -> AacFactory
|
||||
// Everything else has no Media3 muxer. Matroska and the legacy containers are FFmpeg's,
|
||||
// and `Mp4Muxer` exposes no QuickTime file format, so MOV is too.
|
||||
|
||||
// The four muxers described above, plus the containers Media3 never had one for. MOV is
|
||||
// among them despite being MP4's own family: `Mp4Muxer` exposes only FILE_FORMAT_DEFAULT
|
||||
// and FILE_FORMAT_MP4_WITH_AUXILIARY_TRACKS_EXTENSION — no QuickTime.
|
||||
Container.WEBM,
|
||||
Container.OGG,
|
||||
Container.WAV,
|
||||
Container.AAC_ADTS,
|
||||
Container.MKV,
|
||||
Container.MOV,
|
||||
Container.MPEG_TS,
|
||||
@@ -63,73 +66,4 @@ object Media3Muxers {
|
||||
Container.IMAGE_SEQUENCE,
|
||||
-> null
|
||||
}
|
||||
|
||||
private object WebmFactory : Muxer.Factory {
|
||||
override fun create(path: String): Muxer = wrapFailure(path) {
|
||||
WebmMuxer.Builder(SeekableMuxerOutput.of(path)).build()
|
||||
}
|
||||
|
||||
override fun getSupportedSampleMimeTypes(trackType: Int): ImmutableList<String> =
|
||||
when (trackType) {
|
||||
C.TRACK_TYPE_VIDEO -> ImmutableList.of(MimeTypes.VIDEO_VP8, MimeTypes.VIDEO_VP9)
|
||||
C.TRACK_TYPE_AUDIO -> ImmutableList.of(MimeTypes.AUDIO_OPUS, MimeTypes.AUDIO_VORBIS)
|
||||
else -> ImmutableList.of()
|
||||
}
|
||||
}
|
||||
|
||||
private object OggFactory : Muxer.Factory {
|
||||
override fun create(path: String): Muxer = wrapFailure(path) {
|
||||
OggMuxer.Builder(FileOutputStream(path).channel).build()
|
||||
}
|
||||
|
||||
override fun getSupportedSampleMimeTypes(trackType: Int): ImmutableList<String> =
|
||||
if (trackType == C.TRACK_TYPE_AUDIO) {
|
||||
ImmutableList.of(MimeTypes.AUDIO_OPUS, MimeTypes.AUDIO_VORBIS)
|
||||
} else {
|
||||
ImmutableList.of()
|
||||
}
|
||||
}
|
||||
|
||||
private object WavFactory : Muxer.Factory {
|
||||
override fun create(path: String): Muxer = wrapFailure(path) {
|
||||
WavMuxer(SeekableMuxerOutput.of(path))
|
||||
}
|
||||
|
||||
override fun getSupportedSampleMimeTypes(trackType: Int): ImmutableList<String> =
|
||||
if (trackType == C.TRACK_TYPE_AUDIO) {
|
||||
ImmutableList.of(MimeTypes.AUDIO_RAW)
|
||||
} else {
|
||||
ImmutableList.of()
|
||||
}
|
||||
}
|
||||
|
||||
private object AacFactory : Muxer.Factory {
|
||||
override fun create(path: String): Muxer = wrapFailure(path) {
|
||||
AacMuxer(FileOutputStream(path))
|
||||
}
|
||||
|
||||
override fun getSupportedSampleMimeTypes(trackType: Int): ImmutableList<String> =
|
||||
if (trackType == C.TRACK_TYPE_AUDIO) {
|
||||
ImmutableList.of(MimeTypes.AUDIO_AAC)
|
||||
} else {
|
||||
ImmutableList.of()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns an I/O failure into a [MuxerException].
|
||||
*
|
||||
* Opening the output can throw `FileNotFoundException`, which is not what `Muxer.Factory`
|
||||
* declares. Transformer's error handling only recognises `MuxerException`, so letting the raw
|
||||
* IOException escape turns a bad output path into an unhandled crash instead of a reported
|
||||
* export failure — the case `Media3EngineTest.anUnwritableOutputPathFailsInsteadOfHanging`
|
||||
* exists to pin down.
|
||||
*/
|
||||
private inline fun wrapFailure(path: String, open: () -> Muxer): Muxer =
|
||||
try {
|
||||
open()
|
||||
} catch (e: Exception) {
|
||||
if (e is MuxerException) throw e
|
||||
throw MuxerException("Could not open $path for muxing", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,17 +25,16 @@ object ConversionRouter {
|
||||
/**
|
||||
* Containers Media3 can mux. Anything else has to go to FFmpeg.
|
||||
*
|
||||
* MP4 alone. This set used to name WebM, Ogg, WAV and AAC-ADTS as well, on the strength of
|
||||
* `media3-muxer` shipping a muxer for each — but none of those four can be driven by
|
||||
* Transformer at all, for the reasons `Media3Muxers` records. Nothing caught it because the
|
||||
* engine ignored the container entirely and wrote MP4 regardless, so the set being wrong and
|
||||
* the engine being wrong cancelled out.
|
||||
*
|
||||
* Not private: `Media3Muxers` has to supply a `Muxer.Factory` for every entry, and a test
|
||||
* asserts the two agree. They drifted once already — this set was correct while the engine
|
||||
* silently wrote MP4 for all five.
|
||||
* asserts the two agree.
|
||||
*/
|
||||
internal val MEDIA3_CONTAINERS = setOf(
|
||||
Container.MP4,
|
||||
Container.WEBM,
|
||||
Container.OGG,
|
||||
Container.WAV,
|
||||
Container.AAC_ADTS,
|
||||
)
|
||||
internal val MEDIA3_CONTAINERS = setOf(Container.MP4)
|
||||
|
||||
/**
|
||||
* What Media3's muxers can *carry*, as distinct from what Media3 can encode.
|
||||
@@ -45,25 +44,21 @@ object ConversionRouter {
|
||||
* — legal, and something FFmpeg does without complaint — has to leave the hardware path. Before
|
||||
* remuxing existed nothing could reach that combination, so nothing had to know.
|
||||
*
|
||||
* Transcribed from each `Muxer.Factory.getSupportedSampleMimeTypes` in `Media3Muxers`;
|
||||
* One entry, because MP4 is the only container Transformer can write — see `Media3Muxers` for
|
||||
* why the four other muxers in `media3-muxer` cannot be driven by it. Every other container
|
||||
* has already been sent to FFmpeg by the time these are consulted.
|
||||
*
|
||||
* Transcribed from `Muxer.Factory.getSupportedSampleMimeTypes` in `Media3Muxers`;
|
||||
* `Media3MuxersTest` asserts the transcription still matches.
|
||||
*/
|
||||
internal val MEDIA3_MUXABLE_VIDEO: Map<Container, Set<VideoCodec>> = mapOf(
|
||||
Container.MP4 to setOf(VideoCodec.H264, VideoCodec.H265, VideoCodec.VP9, VideoCodec.AV1),
|
||||
Container.WEBM to setOf(VideoCodec.VP8, VideoCodec.VP9),
|
||||
Container.OGG to emptySet(),
|
||||
Container.WAV to emptySet(),
|
||||
Container.AAC_ADTS to emptySet(),
|
||||
)
|
||||
|
||||
internal val MEDIA3_MUXABLE_AUDIO: Map<Container, Set<AudioCodec>> = mapOf(
|
||||
Container.MP4 to setOf(
|
||||
AudioCodec.AAC, AudioCodec.OPUS, AudioCodec.VORBIS, AudioCodec.PCM,
|
||||
),
|
||||
Container.WEBM to setOf(AudioCodec.OPUS, AudioCodec.VORBIS),
|
||||
Container.OGG to setOf(AudioCodec.OPUS, AudioCodec.VORBIS),
|
||||
Container.WAV to setOf(AudioCodec.PCM),
|
||||
Container.AAC_ADTS to setOf(AudioCodec.AAC),
|
||||
)
|
||||
|
||||
/** Video codecs Media3 can encode (`Transformer.setVideoMimeType`). */
|
||||
@@ -109,15 +104,10 @@ object ConversionRouter {
|
||||
|
||||
// The container is supported but its muxer is codec-restricted. What matters is what ends
|
||||
// up in the file, so a copied track is judged by its source codec rather than the request.
|
||||
// Only MP4 reaches here now — WebM is rejected by the container check above — so the
|
||||
// WebM-specific wording no longer applies.
|
||||
if (!media3CanMux(plan, request.probe)) {
|
||||
return Decision(
|
||||
Engine.FFMPEG,
|
||||
if (plan.container == Container.WEBM) {
|
||||
Reason.WEBM_CODEC_UNSUPPORTED
|
||||
} else {
|
||||
Reason.CONTAINER_CODEC_UNSUPPORTED
|
||||
},
|
||||
)
|
||||
return Decision(Engine.FFMPEG, Reason.CONTAINER_CODEC_UNSUPPORTED)
|
||||
}
|
||||
|
||||
// A file the platform extractor could not open cannot be read at all, copied or not.
|
||||
@@ -195,7 +185,6 @@ object ConversionRouter {
|
||||
HARDWARE_CAPABLE("Hardware accelerated"),
|
||||
REMUX_NO_REENCODE("Remuxed — streams copied, nothing re-encoded"),
|
||||
CONTAINER_UNSUPPORTED("This container needs FFmpeg"),
|
||||
WEBM_CODEC_UNSUPPORTED("WebM only supports VP8/VP9 with Opus or Vorbis"),
|
||||
CONTAINER_CODEC_UNSUPPORTED("That codec needs FFmpeg for this container"),
|
||||
NO_PLATFORM_ENCODER("Android has no encoder for this format"),
|
||||
NO_PLATFORM_DECODER("This device cannot decode the input in hardware"),
|
||||
|
||||
Reference in New Issue
Block a user