fix: request an H.264 level that matches the Android output stream - #27
Merged
hatemragab merged 1 commit intoSep 17, 2026
Merged
Conversation
Media3's DefaultEncoderFactory requests the highest H.264 level the encoder advertises when the caller does not specify one, so exports could be declared as High@L6.x and be rejected by decoders capped at Level 5.x such as iOS and Safari (androidx/media#2603). Derive the lowest level covering the output resolution and frame rate from H.264 Table A-1 and request High@<level> through VideoEncoderSettings for video/avc output on API 26+. HDR sources are left to Media3's own profile selection.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On Android the plugin does not request an H.264 profile/level, so Media3's
DefaultEncoderFactoryfills in the highest level the encoder advertises (seeadjustMediaFormatForH264EncoderSettings→EncoderUtil.findHighestSupportedEncodingLevel). This is the behaviour discussed in androidx/media#2603: many encoders write that level into the stream unchanged, so a 720p or 1080p export can end up declared asHigh@L6.0/L6.2on devices whose encoder advertises Level 6.x (e.g. recent Samsung models).Decoders that cap H.264 at Level 5.x — notably iOS/iPadOS and Safari (
<video>, AVPlayer, Photos import) — reject those files even though the actual stream would fit comfortably in Level 3.1–4.2. The level is purely a declaration, so the file is over-specified relative to its own content.Fix
Request the lowest H.264 level that covers the output stream instead of leaving the level unset:
VVideoH264Level.minimumLevel(width, height, frameRate)picks the level from ITU-T H.264 Table A-1 (MaxFS, MaxMBPS, and thesqrt(8·MaxFS)per-dimension cap). Bitrate limits are intentionally not modelled: the request is advisory and Media3 clamps the bitrate to the encoder's range.VVideoCompressionEnginereads the source track's frame rate (the export keeps it) and HDR transfer viaMediaExtractor, computes the output size the same way the existing validation does (crop plan → aspect-ratio-preserving dimensions), and passesHigh@<level>throughDefaultEncoderFactory.setRequestedVideoEncoderSettings(...).video/avcoutput and API 26+, where Media3 itself already defaults to the High profile. Below API 24 Media3 ignores profile/level, and on API 24–25 it defaults to Baseline, so behaviour there is unchanged.Examples of the requested level: 848×480@30 → 3.1, 1280×720@30 → 3.1, 1920×1080@30 → 4.0, 1920×1080@60 → 4.2, 3840×2160@30 → 5.1, 3840×2160@60 → 5.2.
Compatibility
DefaultEncoderFactoryresets the request and falls back to today's behaviour. Encoders that derive the level themselves are unaffected.Testing
cd example && flutter build apk --debugcd example/android && ./gradlew :v_video_compressor:testDebugUnitTest— newVVideoH264LevelTestcovers the Table A-1 mapping for common outputs, macroblock rounding, orientation independence, the per-dimension cap, and invalid input.avcCbox (AVCLevelIndication) or withffprobe -show_streams(level); confirmation from such devices is welcome.Refs: androidx/media#2603