camera: per-platform capture-orientation extension + macOS sensor=0
macOS preview was stretching (aspect wrong) and macOS photo capture
was rotating the landscape sensor 90° because the shared
PhotoOutput / CameraInstance code was setting
`AVCaptureConnection.videoOrientation` from the orientation snapshot
unconditionally. iOS needs that to rotate sample buffers to portrait;
macOS desktop cams are physically landscape and any rotation just
skews the result.
Moved the rotation call behind a per-platform extension on
`AVCaptureConnection`:
- `ios/Classes/Camera/AVCaptureConnection+iOS.swift` applies the
snapshot orientation (current behavior).
- `macos/Classes/Camera/AVCaptureConnection+macOS.swift` is a
no-op. macOS-flavoured photos / preview frames now flow at
native landscape orientation.
`CaptureDevice` reports sensorOrientation=0 on macOS (was hardcoded
90 for iOS); on macOS the page's `normalizeCameraCapture` math then
collapses to identity and the saved JPEG stays the landscape the
sensor produced. iOS keeps sensorOrientation=90 (matches
camera_avfoundation's reported value and the existing capture-
transform math).
Photo and video paths now both produce upright content on macOS
(video already worked because VideoRecorder's transform table maps
the always-portraitUp macOS snapshot to `.identity`).
This commit is contained in:
@@ -425,10 +425,9 @@ final class CameraInstance {
|
|||||||
// front camera — raw sensor feed at capture, mirror as a
|
// front camera — raw sensor feed at capture, mirror as a
|
||||||
// playback decision.
|
// playback decision.
|
||||||
if let videoConn = videoDataOutput?.connection(with: .video) {
|
if let videoConn = videoDataOutput?.connection(with: .video) {
|
||||||
if videoConn.isVideoOrientationSupported {
|
videoConn.applyUxCaptureOrientation(
|
||||||
videoConn.videoOrientation = lockedOrientation?.avVideoOrientation
|
lockedOrientation ?? orientation.current
|
||||||
?? orientation.current.avVideoOrientation
|
)
|
||||||
}
|
|
||||||
if videoConn.isVideoMirroringSupported {
|
if videoConn.isVideoMirroringSupported {
|
||||||
videoConn.automaticallyAdjustsVideoMirroring = false
|
videoConn.automaticallyAdjustsVideoMirroring = false
|
||||||
videoConn.isVideoMirrored = false
|
videoConn.isVideoMirrored = false
|
||||||
@@ -457,11 +456,8 @@ final class CameraInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func applyVideoOrientationOnPreview(_ next: DeviceOrientationFlutter) {
|
private func applyVideoOrientationOnPreview(_ next: DeviceOrientationFlutter) {
|
||||||
guard let conn = videoDataOutput?.connection(with: .video),
|
videoDataOutput?.connection(with: .video)?
|
||||||
conn.isVideoOrientationSupported else {
|
.applyUxCaptureOrientation(next)
|
||||||
return
|
|
||||||
}
|
|
||||||
conn.videoOrientation = next.avVideoOrientation
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func emit(_ extras: [String: Any]) {
|
private func emit(_ extras: [String: Any]) {
|
||||||
|
|||||||
@@ -28,18 +28,22 @@ enum CaptureDevice {
|
|||||||
DiscoveredCamera(
|
DiscoveredCamera(
|
||||||
device: device,
|
device: device,
|
||||||
lens: lensName(for: device.position),
|
lens: lensName(for: device.position),
|
||||||
// iOS doesn't expose sensor orientation directly;
|
sensorOrientation: defaultSensorOrientation
|
||||||
// 90° matches what `camera_avfoundation` reports
|
|
||||||
// and what banlu's `normalizeCameraCapture` math
|
|
||||||
// assumes for iOS sensors. macOS desktop cameras
|
|
||||||
// are already landscape — 0 is the right answer
|
|
||||||
// there but the value is unused on macOS (no
|
|
||||||
// recording rotation, no preview rotation).
|
|
||||||
sensorOrientation: 90
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hardcoded per platform: 90° on iOS (matches the value
|
||||||
|
/// `camera_avfoundation` reports and that banlu's
|
||||||
|
/// `normalizeCameraCapture` math assumes), 0° on macOS (desktop
|
||||||
|
/// cams are already physically landscape — any non-zero value
|
||||||
|
/// would make `normalizeCameraCapture` rotate the saved JPEG).
|
||||||
|
#if os(iOS)
|
||||||
|
private static let defaultSensorOrientation = 90
|
||||||
|
#else
|
||||||
|
private static let defaultSensorOrientation = 0
|
||||||
|
#endif
|
||||||
|
|
||||||
/// Per-platform set of device types the discovery session asks
|
/// Per-platform set of device types the discovery session asks
|
||||||
/// for. iOS only has the built-in cameras; macOS additionally
|
/// for. iOS only has the built-in cameras; macOS additionally
|
||||||
/// surfaces externals + Continuity Camera (iPhone-as-webcam).
|
/// surfaces externals + Continuity Camera (iPhone-as-webcam).
|
||||||
|
|||||||
@@ -34,9 +34,12 @@ final class PhotoOutput {
|
|||||||
)))
|
)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if connection.isVideoOrientationSupported {
|
// Rotation handled per-platform: iOS applies the snapshot
|
||||||
connection.videoOrientation = orientation.avVideoOrientation
|
// orientation to the connection (which rotates the captured
|
||||||
}
|
// JPEG); macOS is a no-op (desktop cams are physically
|
||||||
|
// landscape, any rotation skews the photo). See
|
||||||
|
// `AVCaptureConnection+iOS.swift` / `…+macOS.swift`.
|
||||||
|
connection.applyUxCaptureOrientation(orientation)
|
||||||
// The recorded photo carries no mirror; mirroring is a
|
// The recorded photo carries no mirror; mirroring is a
|
||||||
// preview-only concern.
|
// preview-only concern.
|
||||||
if connection.isVideoMirroringSupported {
|
if connection.isVideoMirroringSupported {
|
||||||
@@ -57,12 +60,12 @@ final class PhotoOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let delegate = PhotoCaptureDelegate { [weak self] result in
|
let delegate = PhotoCaptureDelegate { [weak self] result in
|
||||||
// Reset orientation on the photo connection so a future
|
// Reset orientation back to portraitUp on the photo
|
||||||
// capture without a snapshot defaults to portrait.
|
// connection so a follow-up shot without an explicit
|
||||||
if let conn = self?.avOutput.connection(with: .video),
|
// snapshot defaults cleanly. No-op on macOS (the
|
||||||
conn.isVideoOrientationSupported {
|
// extension method is empty there).
|
||||||
conn.videoOrientation = .portrait
|
self?.avOutput.connection(with: .video)?
|
||||||
}
|
.applyUxCaptureOrientation(.portraitUp)
|
||||||
self?.inFlight = nil
|
self?.inFlight = nil
|
||||||
DispatchQueue.main.async { completion(result) }
|
DispatchQueue.main.async { completion(result) }
|
||||||
}
|
}
|
||||||
|
|||||||
16
ios/Classes/Camera/AVCaptureConnection+iOS.swift
Normal file
16
ios/Classes/Camera/AVCaptureConnection+iOS.swift
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
/// Per-platform shim for applying capture-time rotation to an
|
||||||
|
/// `AVCaptureConnection`. On iOS the connection's `videoOrientation`
|
||||||
|
/// genuinely rotates the sample buffers / captured photo; on macOS
|
||||||
|
/// it appears to rotate stills but not preview/data-output, AND
|
||||||
|
/// desktop cameras are physically landscape so any rotation skews
|
||||||
|
/// the result. The macOS counterpart in
|
||||||
|
/// `macos/Classes/Camera/AVCaptureConnection+macOS.swift` is a no-op.
|
||||||
|
extension AVCaptureConnection {
|
||||||
|
func applyUxCaptureOrientation(_ orientation: DeviceOrientationFlutter) {
|
||||||
|
if isVideoOrientationSupported {
|
||||||
|
videoOrientation = orientation.avVideoOrientation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
macos/Classes/Camera/AVCaptureConnection+macOS.swift
Normal file
17
macos/Classes/Camera/AVCaptureConnection+macOS.swift
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
/// macOS counterpart of `AVCaptureConnection+iOS.swift`.
|
||||||
|
///
|
||||||
|
/// macOS desktop cameras are physically fixed landscape — applying any
|
||||||
|
/// `AVCaptureVideoOrientation` would skew the captured photo (and on
|
||||||
|
/// some macOS versions the preview's data-output buffers) by 90°.
|
||||||
|
/// The orientation snapshot from Flutter (always `portraitUp` on
|
||||||
|
/// macOS — desktops don't rotate) is therefore ignored at the
|
||||||
|
/// connection layer; the recorded video's track transform is still
|
||||||
|
/// `.identity` from VideoRecorder's existing mapping, so video stays
|
||||||
|
/// landscape too.
|
||||||
|
extension AVCaptureConnection {
|
||||||
|
func applyUxCaptureOrientation(_ orientation: DeviceOrientationFlutter) {
|
||||||
|
// Intentionally empty.
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user