Summary
When encoding Full HD (1920×1080) video with H.264 / AVC via WebCodecs, Chromium encoders often pad the vertical dimension to a multiple of 16 ($1088$ px, as $1080$ is not divisible by 16).
In isobmff-muxer.ts, Mediabunny computes pixelAspectRatio using codedHeight ($1088$) against displayAspectHeight ($1080$), which simplifies to 136:135 (1.0074). Mediabunny then:
Injects a pasp (Pixel Aspect Ratio) atom into the sample entry with hSpacing: 136, vSpacing: 135.
Sets trackData.info.height (and consequently the tkhd track height and avc1 sample entry height) to codedHeight ($1088$) instead of the visible height ($1080$).
While casual media players (like VLC) ignore this, professional NLEs like Adobe Premiere Pro strictly honor container metadata. Premiere interprets the footage as anamorphic non-square pixels (PAR 1.0074), causing the video to be squeezed horizontally and vertically when imported into standard 1080p square-pixel timelines unless manually overridden with "Interpret Footage -> Square Pixels (1.0)".
Root Cause Details
In src/isobmff/isobmff-muxer.ts (around line 470):
typescript
const displayAspectWidth = decoderConfig.displayAspectWidth;
const displayAspectHeight = decoderConfig.displayAspectHeight;
const pixelAspectRatio = displayAspectWidth === undefined || displayAspectHeight === undefined
? { num: 1, den: 1 }
: simplifyRational({
num: displayAspectWidth * decoderConfig.codedHeight, // 1920 * 1088 = 2,088,960
den: displayAspectHeight * decoderConfig.codedWidth, // 1080 * 1920 = 2,073,600
}); // Simplifies to 136 / 135!
And in src/isobmff/isobmff-boxes.ts:
typescript
export const pasp = (trackData: IsobmffVideoTrackData) => {
if (trackData.info.pixelAspectRatio.num === trackData.info.pixelAspectRatio.den) {
return null; // Only omitted if 1:1
}
return box('pasp', [
u32(trackData.info.pixelAspectRatio.num), // writes 136
u32(trackData.info.pixelAspectRatio.den), // writes 135
]);
};
In H.264, the extra 8 lines from $1080$ to $1088$ are macroblock padding (which is already cropped out by frame_crop_bottom_offset in the SPS NAL unit), not non-square pixels. The pixels themselves are strictly Square Pixels (1:1).
Furthermore:
typescript
info: {
width: decoderConfig.codedWidth,
height: decoderConfig.codedHeight, // Sets 1088 in tkhd and visual sample entry instead of 1080
pixelAspectRatio,
...
}
Steps to Reproduce
Encode a 1920×1080 canvas using CanvasSource + Mp4OutputFormat in Chrome/Edge on Windows with codec: 'avc'.
Inspect the resulting MP4 container (e.g. with an MP4 box parser or MediaInfo):
A pasp atom is present with 136:135.
tkhd track height is 1088.
Import the MP4 file into Adobe Premiere Pro into a standard 1080p sequence.
Result: Video appears distorted/squeezed with pillarbox borders because Premiere treats it as 1.0074 PAR.
Summary$1088$ px, as $1080$ is not divisible by 16).
When encoding Full HD (1920×1080) video with H.264 / AVC via WebCodecs, Chromium encoders often pad the vertical dimension to a multiple of 16 (
In isobmff-muxer.ts, Mediabunny computes pixelAspectRatio using codedHeight ($1088$ ) against displayAspectHeight ($1080$ ), which simplifies to 136:135 (1.0074). Mediabunny then:
Injects a pasp (Pixel Aspect Ratio) atom into the sample entry with hSpacing: 136, vSpacing: 135.$1088$ ) instead of the visible height ($1080$ ).
Sets trackData.info.height (and consequently the tkhd track height and avc1 sample entry height) to codedHeight (
While casual media players (like VLC) ignore this, professional NLEs like Adobe Premiere Pro strictly honor container metadata. Premiere interprets the footage as anamorphic non-square pixels (PAR 1.0074), causing the video to be squeezed horizontally and vertically when imported into standard 1080p square-pixel timelines unless manually overridden with "Interpret Footage -> Square Pixels (1.0)".
Root Cause Details
In src/isobmff/isobmff-muxer.ts (around line 470):
typescript
const displayAspectWidth = decoderConfig.displayAspectWidth;
const displayAspectHeight = decoderConfig.displayAspectHeight;
const pixelAspectRatio = displayAspectWidth === undefined || displayAspectHeight === undefined
? { num: 1, den: 1 }
: simplifyRational({
num: displayAspectWidth * decoderConfig.codedHeight, // 1920 * 1088 = 2,088,960
den: displayAspectHeight * decoderConfig.codedWidth, // 1080 * 1920 = 2,073,600
}); // Simplifies to 136 / 135!
And in src/isobmff/isobmff-boxes.ts:
typescript$1080$ to $1088$ are macroblock padding (which is already cropped out by frame_crop_bottom_offset in the SPS NAL unit), not non-square pixels. The pixels themselves are strictly Square Pixels (1:1).
export const pasp = (trackData: IsobmffVideoTrackData) => {
if (trackData.info.pixelAspectRatio.num === trackData.info.pixelAspectRatio.den) {
return null; // Only omitted if 1:1
}
return box('pasp', [
u32(trackData.info.pixelAspectRatio.num), // writes 136
u32(trackData.info.pixelAspectRatio.den), // writes 135
]);
};
In H.264, the extra 8 lines from
Furthermore:
typescript
info: {
width: decoderConfig.codedWidth,
height: decoderConfig.codedHeight, // Sets 1088 in tkhd and visual sample entry instead of 1080
pixelAspectRatio,
...
}
Steps to Reproduce
Encode a 1920×1080 canvas using CanvasSource + Mp4OutputFormat in Chrome/Edge on Windows with codec: 'avc'.
Inspect the resulting MP4 container (e.g. with an MP4 box parser or MediaInfo):
A pasp atom is present with 136:135.
tkhd track height is 1088.
Import the MP4 file into Adobe Premiere Pro into a standard 1080p sequence.
Result: Video appears distorted/squeezed with pillarbox borders because Premiere treats it as 1.0074 PAR.