camera: emit previewSize from the first sample buffer's real dims

`device.activeFormat.formatDescription` reports the device's
*selected* format, but on macOS the session-preset remap means the
data output sometimes delivers a different resolution than what
the active format claims. The mismatch surfaced as a stretched
preview on macOS: camera_thumb's SizedBox was sized for a 4:3
buffer, the FittedBox(cover) was given a 16:9 texture, so the
texture stretched to fill the wrongly-shaped box.

PreviewSink now snapshots `CVPixelBufferGet{Width,Height}` from
the first sample buffer that arrives and forwards it via a
callback. CameraInstance hooks the callback to emit the existing
`previewSizeChanged` event (same shape the Android backend uses).
The Dart controller writes it into `value.previewSize`,
camera_thumb's SizedBox snaps to the real buffer aspect, and
FittedBox(cover) crops cleanly without stretching.

Identical wire shape and event name as the Android equivalent —
no Dart changes needed.
This commit is contained in:
agra
2026-05-13 19:20:00 +03:00
parent 8ab672c12a
commit de1a9fd25e
2 changed files with 35 additions and 0 deletions

View File

@@ -132,6 +132,23 @@ final class CameraInstance {
try configureSession(forDeviceUniqueID: cameraId, replacing: false)
// The active-format dims from `device.activeFormat` don't
// always match the buffer the data output actually delivers
// (notably on macOS, where the session preset gets remapped
// mid-pipeline). Forward the real first-frame size to Dart
// so camera_thumb's SizedBox aspect matches the texture's
// actual aspect without this the FittedBox(cover) stretches
// a 16:9 buffer that's been sized as 4:3.
sink.onFirstFrameSize = { [weak self] size in
self?.emit([
"event": "previewSizeChanged",
"previewSize": [
"width": size.width,
"height": size.height,
],
])
}
orientation.start { [weak self] next in
guard let self = self else { return }
self.sessionQueue.async { self.applyOrientationFollowDevice(next) }

View File

@@ -31,6 +31,16 @@ final class PreviewSink: NSObject, FlutterTexture {
)
private var latestPixelBuffer: CVPixelBuffer?
/// Fires once after the first sample buffer arrives, carrying the
/// buffer's actual `CVPixelBuffer` dimensions. The active-format
/// dims reported by `device.activeFormat` aren't always what the
/// data output actually delivers (notably on macOS where the
/// session preset gets remapped). [CameraInstance] forwards this
/// to Dart as a `previewSizeChanged` event so the camera_thumb's
/// SizedBox aspect matches the texture's real aspect.
var onFirstFrameSize: ((CGSize) -> Void)?
private var emittedFirstSize = false
func register(with registry: FlutterTextureRegistry) -> Int64 {
self.registry = registry
textureId = registry.register(self)
@@ -63,6 +73,14 @@ final class PreviewSink: NSObject, FlutterTexture {
return
}
bufferQueue.sync { latestPixelBuffer = pb }
if !emittedFirstSize {
emittedFirstSize = true
let size = CGSize(
width: CGFloat(CVPixelBufferGetWidth(pb)),
height: CGFloat(CVPixelBufferGetHeight(pb))
)
onFirstFrameSize?(size)
}
if let registry = registry {
registry.textureFrameAvailable(textureId)
}