Skip to content

fix(engine): portrait and rotated videos export upright - #215

Merged
SteliyanH merged 1 commit into
developfrom
fix/video-orientation
Nov 13, 2026
Merged

SteliyanH merged 1 commit into
developfrom
fix/video-orientation

Conversation

@SteliyanH

Copy link
Copy Markdown
Owner

Problem

Every portrait iPhone clip exported and previewed sideways, stretched to fill the frame.

A camera stores a portrait recording as landscape pixels plus a preferredTransform (the front camera may add a mirror). A layer instruction's transform replaces that tag, and kadr never read it: grep preferredTransform Sources/ had zero hits. This is the most-reported AVFoundation bug, and a first-time user hits it within minutes.

Fix

  • OrientationSchedule (new). After the clips are placed, it reads each source's preferredTransform and naturalSize from the composition's own segments. Every build path gets it the same way, and lookups are by time, because a portrait clip and a landscape clip can share one composition track.
  • Upright before fill. Each clip is turned upright (the translation normalised, mirror kept), then scaled to fill and centred. This applies in:
    • the simple path
    • transitions (dissolve, fade, slide)
    • multi-track: Tracks, free floaters, clip transforms and keyframes
    • PlaybackComposer (preview and thumbnails)
  • Simple path. It still returns a nil videoComposition when every source is upright, so nothing changes there. A rotated source gets an oriented one, so an export that can't apply it now refuses instead of writing a sideways copy (same rule as Silent passthrough fallback drops entire videoComposition for no-audio compositions (empty audio track fails HEVC compatibility check) #201).
  • ReverseProcessor copies the source transform onto the reversed file.
  • LayerTransformTimeline. Base layout and per-clip keyframes are collected and emitted once, in time order, so neither overwrites the other by call order.
    • This also fixes a previously unreported bug: on the keyframe path, a plain clip after a transformed clip inherited the previous clip's transform.

Tests

VideoOrientationTests has 13 tests. Fixtures are 128×64 four-colour clips tagged with quarter, three-quarter, half and mirrored transforms (iPhone-style, and about the origin). The ground truth is AVAssetImageGenerator with appliesPreferredTrackTransform.

Cases covered:

  • single clip
  • mixed orientations on one track
  • dissolve and slide
  • clip transform
  • plain clip after a transformed clip
  • Track over chain
  • preview thumbnail
  • filter pre-render
  • reverse

Mutation check: run against the old engine, every rotated case fails, and the upright control and the new transform-leak test behave as expected.

Full suite: 695 swift-testing + 49 XCTest pass; the generic iOS build succeeds.

Not in this PR (filed)

  • A user MultiInputCompositor still receives raw, unrotated source frames (it gets unscaled frames by design today).
  • VideoClip.metadata.resolution reports the stored size, not the display size.

A camera stores a portrait recording as landscape pixels plus a
preferredTransform, and the front camera may add a mirror. A layer
instruction's transform replaces that tag, and kadr never applied it, so
every portrait iPhone clip exported and previewed sideways.

The orientation of every source is now read from the composition's own
segments once the clips are placed, and each clip is turned upright before
it is scaled to fill. That covers plain chains, transitions (slide included),
Tracks and free-floating clips, clip transforms and keyframes, and the
preview player. Clips with different orientations on one track each keep
their own. ReverseProcessor now copies the source's transform to the
reversed file instead of dropping it.

Transforms on a layer are collected and emitted once in time order, so the
base layout and per-clip keyframes can't overwrite each other by call
order. That also fixes a plain clip inheriting the previous clip's
transform on the keyframe path.
@SteliyanH
SteliyanH merged commit b9b28e8 into develop Nov 13, 2026
2 checks passed
@SteliyanH
SteliyanH deleted the fix/video-orientation branch November 13, 2026 15:52
@github-actions github-actions Bot locked and limited conversation to collaborators Nov 13, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant