Skip to content

fix: rotate ReplayKit frames upright on landscape - #39

Merged
Timo972 merged 1 commit into
masterfrom
timo/dro-3359-ios-wda-stream-landscape-picture-stays-un-rotated-portrait
Oct 14, 2026
Merged

Timo972 merged 1 commit into
masterfrom
timo/dro-3359-ios-wda-stream-landscape-picture-stays-un-rotated-portrait

Conversation

@Timo972

@Timo972 Timo972 commented Oct 13, 2026

Copy link
Copy Markdown

Fixes DRO-3359.

Problem

On WDA-controlled phones (iOS < 27), rotating to landscape flips the stream to 1278×588, so the viewer rotates its frame. The picture inside stays un-rotated: a narrow portrait strip with the UI drawn sideways, in Chrome and Safari.

ReplayKit always hands the broadcast extension a portrait buffer and carries the orientation only in RPVideoSampleOrientationKey. Since #33 (e0d899d1), the pipeline swaps the encoder size on rotation, but VTPixelTransferSessionTransferImage only letterboxes the portrait buffer into the new landscape size and never rotates it.

Change

  • FBBroadcastFrameRotation maps the orientation tag to the turn that makes the buffer upright: 3 → 180°, 6 → 90° counter-clockwise, 8 → 90° clockwise. Unknown and mirrored values (which ReplayKit doesn't emit) map to no rotation.
  • FBExtSessionPipeline handles rotated frames in two steps, because iOS has no rotating pixel transfer (kVTPixelTransferPropertyKey_Rotation is macOS-only) and VTPixelRotationSession can't scale:
    1. scale the frame (letterboxed, as before) into an encoder-sized buffer in the un-rotated orientation;
    2. rotate that buffer into the encoder's buffer.
  • The rotation session and the extra buffer pool are created on the first rotated frame and released with the other scaler resources. Each intermediate buffer is encoder-sized (about 1 MB in NV12), well inside the extension's memory cap.
  • Upright frames take the existing path unchanged. The direct-source shortcut is skipped while a rotation is needed, so an upside-down frame (same size as portrait) is not encoded un-rotated.
  • Rotation needs iOS 16; iOS 15 keeps the previous behaviour.

The quarter-turn mapping is the opposite of what the CGImagePropertyOrientation docs suggest. I first implemented it per the docs, and both landscape directions came out upside down on hardware; the code comment records the measured mapping.

Verification

  • Unit tests: UnitTests 255 passed, 1 skipped, including two new tests for FBBroadcastFrameRotation.
  • Hardware: iPhone 15 Pro, iOS 26.6.1 ("iPhone 4" on the BYOD host). I built the runner the way Scripts/ci/build-real.sh does, then signed and installed it with mobilerun-ios setup --reinstall. A probe brought the broadcast up (state connected), opened the portal's H.265 capture (50%, 60 fps) with Safari in front, and stepped portrait → landscapeLeft → portrait → landscapeRight → portrait through CoreDevice changeOrientation. For each step I decoded a frame with ffmpeg:
    • landscapeLeft and landscapeRight: 1278×588, upright, filling the frame (before the fix: a sideways portrait strip; with the docs mapping: upside down);
    • each return to portrait: 588×1278, upright, with no reconnect.
  • Not covered: the 180° case (orientation 3). Safari doesn't rotate upside-down on an iPhone.

🤖 Generated with Claude Code

ReplayKit delivers the screen as a portrait buffer and carries the device
orientation only in RPVideoSampleOrientationKey. Since e0d899d the
pipeline swaps the encoder footprint on rotation, but the pixel transfer
only letterboxed the portrait buffer into it, so landscape streamed a
narrow portrait strip with the UI sideways.

Rotate the frame before encoding. iOS has no rotating pixel transfer and
a VTPixelRotationSession cannot scale, so a rotated frame is scaled into
an encoder-sized buffer in the un-rotated orientation and then rotated
into the encoder buffer. Upright frames keep the existing path, and the
direct-source shortcut is skipped while a rotation is needed. Rotation
needs iOS 16; iOS 15 keeps the previous behaviour.

The quarter-turn mapping was measured on an iPhone 15 Pro (iOS 26.6.1):
it reads upright opposite to the CGImagePropertyOrientation docs.

Refs DRO-3359

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Timo972
Timo972 requested a review from niko-droidrun October 14, 2026 11:09
@Timo972
Timo972 merged commit efe6842 into master Oct 14, 2026
40 of 50 checks passed
@Timo972
Timo972 deleted the timo/dro-3359-ios-wda-stream-landscape-picture-stays-un-rotated-portrait branch October 14, 2026 11:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants