From 67fa6873a5562ffe65c4de1bbf812a961d6b9ce5 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Mon, 23 Feb 2026 12:20:11 -0800 Subject: [PATCH 01/12] init --- .../camera_avfoundation/CaptureOutput.swift | 3 + .../camera_avfoundation/DefaultCamera.swift | 56 ++++++++++++++++--- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CaptureOutput.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CaptureOutput.swift index f9b607b17fdb..5f6754a77824 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CaptureOutput.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CaptureOutput.swift @@ -25,6 +25,9 @@ protocol CaptureVideoDataOutput: CaptureOutput { /// Corresponds to the `alwaysDiscardsLateVideoFrames` property of `AVCaptureVideoDataOutput` var alwaysDiscardsLateVideoFrames: Bool { get set } + /// Corresponds to the `availableVideoPixelFormatTypes` property of `AVCaptureVideoDataOutput` + var availableVideoPixelFormatTypes: [FourCharCode] { get } + /// Corresponds to the `videoSettings` property of `AVCaptureVideoDataOutput` var videoSettings: [String: Any]! { get set } diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift index b1473f16147d..4b99eae25061 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift @@ -16,12 +16,13 @@ final class DefaultCamera: NSObject, Camera { var onFrameAvailable: (() -> Void)? var videoFormat: FourCharCode = kCVPixelFormatType_32BGRA { - didSet { - captureVideoOutput.videoSettings = [ - kCVPixelBufferPixelFormatTypeKey as String: videoFormat - ] + didSet { + captureVideoOutput.videoSettings = DefaultCamera.getSafeVideoSettings( + requestedFormat: videoFormat, + availableFormats: captureVideoOutput.availableVideoPixelFormatTypes + ) + } } - } private(set) var isPreviewPaused = false @@ -148,9 +149,12 @@ final class DefaultCamera: NSObject, Camera { // Setup video capture output. let captureVideoOutput = AVCaptureVideoDataOutput() - captureVideoOutput.videoSettings = [ - kCVPixelBufferPixelFormatTypeKey as String: videoFormat - ] + + captureVideoOutput.videoSettings = getSafeVideoSettings( + requestedFormat: videoFormat, + availableFormats: captureVideoOutput.availableVideoPixelFormatTypes + ) + captureVideoOutput.alwaysDiscardsLateVideoFrames = true // Setup video capture connection. @@ -351,12 +355,24 @@ final class DefaultCamera: NSObject, Camera { var maxPixelCount: UInt = 0 var isBestSubTypePreferred = false + // Filter out all compressed/lossy types that the Flutter engine can't render. + let unsupportedSubTypes: [FourCharCode] = [ + 1651798066, // hex for 'btp2', or 420YpCbCr8BiPlanarVideoRange + 1651798068, // hex for 'btp4', or 420YpCbCr8BiPlanarFullRange + 1650943852, // hex for 'bgrl', or for 32BGRA lossy + ] + for format in captureDevice.flutterFormats { + let subType = CMFormatDescriptionGetMediaSubType(format.formatDescription) + + if unsupportedSubTypes.contains(subType) { + continue + } + let resolution = videoDimensionsConverter(format) let height = UInt(resolution.height) let width = UInt(resolution.width) let pixelCount = height * width - let subType = CMFormatDescriptionGetMediaSubType(format.formatDescription) let isSubTypePreferred = subType == preferredSubType if pixelCount > maxPixelCount @@ -457,6 +473,28 @@ final class DefaultCamera: NSObject, Camera { try? AVAudioSession.sharedInstance().setCategory(finalCategory, options: finalOptions) } + private static func getSafeVideoSettings( + requestedFormat: FourCharCode, + availableFormats: [FourCharCode] + ) -> [String: Any] { + let selectedFormat: FourCharCode + + if availableFormats.contains(requestedFormat) { + selectedFormat = requestedFormat + } else if let flutterSupportedFallback = availableFormats.first(where: { + $0 == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange || + $0 == kCVPixelFormatType_32BGRA || + $0 == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange + }) { + selectedFormat = flutterSupportedFallback + } else { + // Ultimate safe fallback + selectedFormat = availableFormats.first ?? requestedFormat + } + + return [kCVPixelBufferPixelFormatTypeKey as String: selectedFormat] + } + func reportInitializationState() { // Get all the state on the current thread, not the main thread. let state = PlatformCameraState( From 49e034e517d7d82d46e80e40f66d5d83b59e2bf6 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Mon, 23 Feb 2026 12:30:20 -0800 Subject: [PATCH 02/12] test --- .../ios/RunnerTests/Mocks/MockCaptureVideoDataOutput.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureVideoDataOutput.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureVideoDataOutput.swift index 4743a56a1bee..87aba5dbe1e6 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureVideoDataOutput.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureVideoDataOutput.swift @@ -17,6 +17,7 @@ class MockCaptureVideoDataOutput: NSObject, CaptureVideoDataOutput { var avOutput = AVCaptureVideoDataOutput() var alwaysDiscardsLateVideoFrames = false var videoSettings: [String: Any]! = [:] + var availableVideoPixelFormatTypes: [FourCharCode] = [] var connectionWithMediaTypeStub: ((AVMediaType) -> CaptureConnection?)? From cd1cbbf5b73e521ad0a802b8265487b6e5b6f5f7 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Mon, 23 Feb 2026 14:07:24 -0800 Subject: [PATCH 03/12] test --- .../ios/RunnerTests/CameraSettingsTests.swift | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift index 7eea50c33f9e..a28adf00fbd4 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift @@ -301,4 +301,36 @@ final class CameraSettingsTests: XCTestCase { "Audio session should receive AVCaptureAudioDataOutput when enableAudio is true" ) } + + func testSettings_MaxResolutionShouldIgnoreLossyFormats() { + let settings = PlatformMediaSettings( + resolutionPreset: .max, + framesPerSecond: 30, + videoBitrate: testVideoBitrate, + audioBitrate: testAudioBitrate, + enableAudio: false + ) + + let configuration = CameraTestUtils.createTestCameraConfiguration() + configuration.mediaSettings = settings + + let mockDevice = configuration.videoCaptureDeviceFactory("camera") as! MockCaptureDevice + + let massiveLossyFormat = MockCaptureDeviceFormat() + massiveLossyFormat.mediaSubType = 1651798066 // The toxic format + massiveLossyFormat.dimensions = CMVideoDimensions(width: 4224, height: 3024) + + let safe4KFormat = MockCaptureDeviceFormat() + safe4KFormat.mediaSubType = 875704422 // Standard uncompressed YUV + safe4KFormat.dimensions = CMVideoDimensions(width: 3840, height: 2160) + + mockDevice.flutterFormats = [massiveLossyFormat, safe4KFormat] + + let camera = CameraTestUtils.createTestCamera(configuration) + + let selectedDimensions = camera.videoDimensionsConverter(camera.captureDevice.flutterActiveFormat) + + XCTAssertEqual(selectedDimensions.width, 3840) + XCTAssertEqual(selectedDimensions.height, 2160) + } } From dc781e38e8ad57b7cd7f5a02f1612320587a104c Mon Sep 17 00:00:00 2001 From: louisehsu Date: Tue, 24 Feb 2026 13:29:14 -0800 Subject: [PATCH 04/12] test --- .../ios/RunnerTests/CameraSettingsTests.swift | 33 +++-- .../camera_avfoundation/DefaultCamera.swift | 117 +++++++----------- 2 files changed, 73 insertions(+), 77 deletions(-) diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift index a28adf00fbd4..4b4e36706f97 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift @@ -317,20 +317,39 @@ final class CameraSettingsTests: XCTestCase { let mockDevice = configuration.videoCaptureDeviceFactory("camera") as! MockCaptureDevice let massiveLossyFormat = MockCaptureDeviceFormat() - massiveLossyFormat.mediaSubType = 1651798066 // The toxic format - massiveLossyFormat.dimensions = CMVideoDimensions(width: 4224, height: 3024) + var lossyDesc: CMVideoFormatDescription? + CMVideoFormatDescriptionCreate( + allocator: kCFAllocatorDefault, + codecType: 1651798066, // The toxic format + width: 4224, + height: 3024, + extensions: nil, + formatDescriptionOut: &lossyDesc + ) + massiveLossyFormat._formatDescription = lossyDesc let safe4KFormat = MockCaptureDeviceFormat() - safe4KFormat.mediaSubType = 875704422 // Standard uncompressed YUV - safe4KFormat.dimensions = CMVideoDimensions(width: 3840, height: 2160) + var safeDesc: CMVideoFormatDescription? + CMVideoFormatDescriptionCreate( + allocator: kCFAllocatorDefault, + codecType: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, // Standard uncompressed YUV + width: 3840, + height: 2160, + extensions: nil, + formatDescriptionOut: &safeDesc + ) + safe4KFormat._formatDescription = safeDesc mockDevice.flutterFormats = [massiveLossyFormat, safe4KFormat] + mockDevice.flutterActiveFormat = safe4KFormat + let camera = CameraTestUtils.createTestCamera(configuration) - let selectedDimensions = camera.videoDimensionsConverter(camera.captureDevice.flutterActiveFormat) + let selectedFormat = camera.captureDevice.flutterActiveFormat + let selectedDimensions = CMVideoFormatDescriptionGetDimensions(selectedFormat.formatDescription) - XCTAssertEqual(selectedDimensions.width, 3840) - XCTAssertEqual(selectedDimensions.height, 2160) + XCTAssertEqual(selectedDimensions.width, 3840, "Camera should have selected the safe 4K format, skipping the 12MP lossy format.") + XCTAssertEqual(selectedDimensions.height, 2160, "Camera should have selected the safe 4K format, skipping the 12MP lossy format.") } } diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift index 4b99eae25061..162176cd82c8 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift @@ -16,13 +16,12 @@ final class DefaultCamera: NSObject, Camera { var onFrameAvailable: (() -> Void)? var videoFormat: FourCharCode = kCVPixelFormatType_32BGRA { - didSet { - captureVideoOutput.videoSettings = DefaultCamera.getSafeVideoSettings( - requestedFormat: videoFormat, - availableFormats: captureVideoOutput.availableVideoPixelFormatTypes - ) - } + didSet { + captureVideoOutput.videoSettings = [ + kCVPixelBufferPixelFormatTypeKey as String: videoFormat + ] } + } private(set) var isPreviewPaused = false @@ -149,12 +148,9 @@ final class DefaultCamera: NSObject, Camera { // Setup video capture output. let captureVideoOutput = AVCaptureVideoDataOutput() - - captureVideoOutput.videoSettings = getSafeVideoSettings( - requestedFormat: videoFormat, - availableFormats: captureVideoOutput.availableVideoPixelFormatTypes - ) - + captureVideoOutput.videoSettings = [ + kCVPixelBufferPixelFormatTypeKey as String: videoFormat + ] captureVideoOutput.alwaysDiscardsLateVideoFrames = true // Setup video capture connection. @@ -345,47 +341,50 @@ final class DefaultCamera: NSObject, Camera { } /// Finds the highest available resolution in terms of pixel count for the given device. - /// Preferred are formats with the same subtype as current activeFormat. - private func highestResolutionFormat(forCaptureDevice captureDevice: CaptureDevice) - -> CaptureDeviceFormat? - { - let preferredSubType = CMFormatDescriptionGetMediaSubType( - captureDevice.flutterActiveFormat.formatDescription) - var bestFormat: CaptureDeviceFormat? = nil - var maxPixelCount: UInt = 0 - var isBestSubTypePreferred = false - - // Filter out all compressed/lossy types that the Flutter engine can't render. - let unsupportedSubTypes: [FourCharCode] = [ - 1651798066, // hex for 'btp2', or 420YpCbCr8BiPlanarVideoRange - 1651798068, // hex for 'btp4', or 420YpCbCr8BiPlanarFullRange - 1650943852, // hex for 'bgrl', or for 32BGRA lossy - ] + /// Preferred are formats with the same subtype as current activeFormat. + private func highestResolutionFormat(forCaptureDevice captureDevice: CaptureDevice) + -> CaptureDeviceFormat? + { + let preferredSubType = CMFormatDescriptionGetMediaSubType( + captureDevice.flutterActiveFormat.formatDescription) + var bestFormat: CaptureDeviceFormat? = nil + var maxPixelCount: UInt = 0 + var isBestSubTypePreferred = false + + // Apple's lossy compressed formats that Flutter Metal cannot render. + // Filtering these out prevents AVFoundation from crashing when it tries to + // stream 12MP video, forcing a safe fallback to uncompressed 4K. + let unsupportedSubTypes: [FourCharCode] = [ + 1651798066, // 'btp2' - 420YpCbCr8BiPlanarVideoRange (Lossy) + 1651798068, // 'btp4' - 420YpCbCr8BiPlanarFullRange (Lossy) + 1650943852, // 'bgrl' - 32BGRA (Lossy) + ] - for format in captureDevice.flutterFormats { - let subType = CMFormatDescriptionGetMediaSubType(format.formatDescription) + for format in captureDevice.flutterFormats { + let subType = CMFormatDescriptionGetMediaSubType(format.formatDescription) - if unsupportedSubTypes.contains(subType) { - continue - } + // Skip formats that will crash the Flutter Engine + if unsupportedSubTypes.contains(subType) { + continue + } - let resolution = videoDimensionsConverter(format) - let height = UInt(resolution.height) - let width = UInt(resolution.width) - let pixelCount = height * width - let isSubTypePreferred = subType == preferredSubType - - if pixelCount > maxPixelCount - || (pixelCount == maxPixelCount && isSubTypePreferred && !isBestSubTypePreferred) - { - bestFormat = format - maxPixelCount = pixelCount - isBestSubTypePreferred = isSubTypePreferred + let resolution = videoDimensionsConverter(format) + let height = UInt(resolution.height) + let width = UInt(resolution.width) + let pixelCount = height * width + let isSubTypePreferred = subType == preferredSubType + + if pixelCount > maxPixelCount + || (pixelCount == maxPixelCount && isSubTypePreferred && !isBestSubTypePreferred) + { + bestFormat = format + maxPixelCount = pixelCount + isBestSubTypePreferred = isSubTypePreferred + } } - } - return bestFormat - } + return bestFormat + } func setUpCaptureSessionForAudioIfNeeded() { // Don't setup audio twice or we will lose the audio. @@ -473,28 +472,6 @@ final class DefaultCamera: NSObject, Camera { try? AVAudioSession.sharedInstance().setCategory(finalCategory, options: finalOptions) } - private static func getSafeVideoSettings( - requestedFormat: FourCharCode, - availableFormats: [FourCharCode] - ) -> [String: Any] { - let selectedFormat: FourCharCode - - if availableFormats.contains(requestedFormat) { - selectedFormat = requestedFormat - } else if let flutterSupportedFallback = availableFormats.first(where: { - $0 == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange || - $0 == kCVPixelFormatType_32BGRA || - $0 == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange - }) { - selectedFormat = flutterSupportedFallback - } else { - // Ultimate safe fallback - selectedFormat = availableFormats.first ?? requestedFormat - } - - return [kCVPixelBufferPixelFormatTypeKey as String: selectedFormat] - } - func reportInitializationState() { // Get all the state on the current thread, not the main thread. let state = PlatformCameraState( From 11f48560f9b2035e3a9409f3fefa03a6228e8570 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Tue, 24 Feb 2026 21:01:53 -0800 Subject: [PATCH 05/12] test? --- .../ios/RunnerTests/CameraSettingsTests.swift | 88 +++++++++++-------- .../Mocks/MockCaptureDeviceFormat.swift | 8 ++ .../Mocks/MockDeviceOrientationProvider.swift | 1 + .../camera_avfoundation/DefaultCamera.swift | 15 ++-- 4 files changed, 68 insertions(+), 44 deletions(-) diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift index 4b4e36706f97..89b21bc2d178 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift @@ -302,54 +302,64 @@ final class CameraSettingsTests: XCTestCase { ) } - func testSettings_MaxResolutionShouldIgnoreLossyFormats() { - let settings = PlatformMediaSettings( - resolutionPreset: .max, - framesPerSecond: 30, - videoBitrate: testVideoBitrate, - audioBitrate: testAudioBitrate, - enableAudio: false - ) - - let configuration = CameraTestUtils.createTestCameraConfiguration() - configuration.mediaSettings = settings - - let mockDevice = configuration.videoCaptureDeviceFactory("camera") as! MockCaptureDevice - - let massiveLossyFormat = MockCaptureDeviceFormat() - var lossyDesc: CMVideoFormatDescription? - CMVideoFormatDescriptionCreate( - allocator: kCFAllocatorDefault, - codecType: 1651798066, // The toxic format + func testResolutionPresetWithMax_mustIgnoreLossyFormatsAndSquares() { + // 1. Setup the basic mock infrastructure + let videoSessionMock = MockCaptureSession() + videoSessionMock.canSetSessionPresetStub = { _ in true } + + // 2. Create our boss-fight formats + let lossyFormat = MockCaptureDeviceFormat( + codecType: 1651798066, // 'btp2' width: 4224, - height: 3024, - extensions: nil, - formatDescriptionOut: &lossyDesc + height: 3024 ) - massiveLossyFormat._formatDescription = lossyDesc - - let safe4KFormat = MockCaptureDeviceFormat() - var safeDesc: CMVideoFormatDescription? - CMVideoFormatDescriptionCreate( - allocator: kCFAllocatorDefault, - codecType: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, // Standard uncompressed YUV + let squareFormat = MockCaptureDeviceFormat( + codecType: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + width: 4032, + height: 4032 + ) + let safe4KFormat = MockCaptureDeviceFormat( + codecType: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, width: 3840, - height: 2160, - extensions: nil, - formatDescriptionOut: &safeDesc + height: 2160 ) - safe4KFormat._formatDescription = safeDesc - mockDevice.flutterFormats = [massiveLossyFormat, safe4KFormat] + let captureDeviceMock = MockCaptureDevice() + captureDeviceMock.flutterFormats = [lossyFormat, squareFormat, safe4KFormat] + captureDeviceMock.flutterActiveFormat = safe4KFormat // Baseline for preferredSubType - mockDevice.flutterActiveFormat = safe4KFormat + // 3. Configure the camera test environment exactly like the other tests in this file + let configuration = CameraTestUtils.createTestCameraConfiguration() + configuration.videoCaptureDeviceFactory = { _ in captureDeviceMock } + configuration.videoCaptureSession = videoSessionMock + + // Allow the config to read our custom width/height values + configuration.videoDimensionsConverter = { format in + return CMVideoFormatDescriptionGetDimensions(format.formatDescription) + } + + // CRITICAL: Using default settings keeps `framesPerSecond` nil. + // This stops FormatUtils from throwing away our formats due to missing mock frame rates! + configuration.mediaSettings = CameraTestUtils.createDefaultMediaSettings( + resolutionPreset: PlatformResolutionPreset.max + ) - let camera = CameraTestUtils.createTestCamera(configuration) + // 4. Trigger the initialization (this runs our patched highestResolutionFormat math) + let _ = CameraTestUtils.createTestCamera(configuration) - let selectedFormat = camera.captureDevice.flutterActiveFormat + // 5. Assert that the camera dodged both traps! + let selectedFormat = captureDeviceMock.flutterActiveFormat let selectedDimensions = CMVideoFormatDescriptionGetDimensions(selectedFormat.formatDescription) - XCTAssertEqual(selectedDimensions.width, 3840, "Camera should have selected the safe 4K format, skipping the 12MP lossy format.") - XCTAssertEqual(selectedDimensions.height, 2160, "Camera should have selected the safe 4K format, skipping the 12MP lossy format.") + XCTAssertEqual( + selectedDimensions.width, + 3840, + "Camera should have ignored the lossy and square formats, safely falling back to 4K." + ) + XCTAssertEqual( + selectedDimensions.height, + 2160, + "Camera should have ignored the lossy and square formats, safely falling back to 4K." + ) } } diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDeviceFormat.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDeviceFormat.swift index bae401095f3a..6cf66b5ad9ec 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDeviceFormat.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDeviceFormat.swift @@ -37,4 +37,12 @@ final class MockCaptureDeviceFormat: NSObject, CaptureDeviceFormat { allocator: kCFAllocatorDefault, codecType: kCVPixelFormatType_32BGRA, width: 1920, height: 1080, extensions: nil, formatDescriptionOut: &_formatDescription) } + + init(codecType: OSType, width: Int32, height: Int32) { + super.init() + + CMVideoFormatDescriptionCreate( + allocator: kCFAllocatorDefault, codecType: codecType, width: width, + height: height, extensions: nil, formatDescriptionOut: &_formatDescription) + } } diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockDeviceOrientationProvider.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockDeviceOrientationProvider.swift index 3c1ffacad5a7..33831dcaf813 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockDeviceOrientationProvider.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockDeviceOrientationProvider.swift @@ -3,6 +3,7 @@ // found in the LICENSE file. @testable import camera_avfoundation +import UIKit // Import Objective-C part of the implementation when SwiftPM is used. #if canImport(camera_avfoundation_objc) diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift index 162176cd82c8..f13ef790c63f 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift @@ -340,7 +340,7 @@ final class DefaultCamera: NSObject, Camera { audioCaptureSession.sessionPreset = videoCaptureSession.sessionPreset } - /// Finds the highest available resolution in terms of pixel count for the given device. + /// Finds the highest available resolution 16:9 in terms of pixel count for the given device. /// Preferred are formats with the same subtype as current activeFormat. private func highestResolutionFormat(forCaptureDevice captureDevice: CaptureDevice) -> CaptureDeviceFormat? @@ -351,9 +351,7 @@ final class DefaultCamera: NSObject, Camera { var maxPixelCount: UInt = 0 var isBestSubTypePreferred = false - // Apple's lossy compressed formats that Flutter Metal cannot render. - // Filtering these out prevents AVFoundation from crashing when it tries to - // stream 12MP video, forcing a safe fallback to uncompressed 4K. + // Apple's lossy compressed formats that Flutter Metal can't render. let unsupportedSubTypes: [FourCharCode] = [ 1651798066, // 'btp2' - 420YpCbCr8BiPlanarVideoRange (Lossy) 1651798068, // 'btp4' - 420YpCbCr8BiPlanarFullRange (Lossy) @@ -371,6 +369,14 @@ final class DefaultCamera: NSObject, Camera { let resolution = videoDimensionsConverter(format) let height = UInt(resolution.height) let width = UInt(resolution.width) + + let ratio = max(resolution.width, resolution.height) / min(resolution.width, resolution.height) + let is16x9 = abs(Double(ratio) - Double(16.0 / 9.0)) < 0.05 + + if !is16x9 { + continue + } + let pixelCount = height * width let isSubTypePreferred = subType == preferredSubType @@ -382,7 +388,6 @@ final class DefaultCamera: NSObject, Camera { isBestSubTypePreferred = isSubTypePreferred } } - return bestFormat } From 009c94087da76aa42d3158c7c7b4f229ab8cb253 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Tue, 24 Feb 2026 23:09:52 -0800 Subject: [PATCH 06/12] test. --- .../ios/RunnerTests/CameraSessionPresetsTests.swift | 10 +++++++--- .../ios/RunnerTests/CameraSettingsTests.swift | 13 ++++--------- .../Sources/camera_avfoundation/DefaultCamera.swift | 10 +++++----- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.swift index 69e0a828c5c5..ea3d8662b82d 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.swift @@ -21,6 +21,7 @@ final class CameraSessionPresetsTests: XCTestCase { description: "Expected lockForConfiguration called") let videoSessionMock = MockCaptureSession() + videoSessionMock.canSetSessionPresetStub = { _ in true } videoSessionMock.setSessionPresetStub = { preset in if preset == expectedPreset { presetExpectation.fulfill() @@ -29,15 +30,18 @@ final class CameraSessionPresetsTests: XCTestCase { let captureFormatMock = MockCaptureDeviceFormat() let captureDeviceMock = MockCaptureDevice() captureDeviceMock.flutterFormats = [captureFormatMock] - captureDeviceMock.flutterActiveFormat = captureFormatMock + var currentFormat: CaptureDeviceFormat = captureFormatMock + captureDeviceMock.activeFormatStub = { + return currentFormat + } captureDeviceMock.lockForConfigurationStub = { lockForConfigurationExpectation.fulfill() } let configuration = CameraTestUtils.createTestCameraConfiguration() configuration.videoCaptureDeviceFactory = { _ in captureDeviceMock } - configuration.videoDimensionsConverter = { format in - return CMVideoDimensions(width: 1, height: 1) + configuration.videoDimensionsConverter = { _ in + return CMVideoDimensions(width: 1920, height: 1080) } configuration.videoCaptureSession = videoSessionMock configuration.mediaSettings = CameraTestUtils.createDefaultMediaSettings( diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift index 89b21bc2d178..57e9ea06533d 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift @@ -303,11 +303,9 @@ final class CameraSettingsTests: XCTestCase { } func testResolutionPresetWithMax_mustIgnoreLossyFormatsAndSquares() { - // 1. Setup the basic mock infrastructure let videoSessionMock = MockCaptureSession() videoSessionMock.canSetSessionPresetStub = { _ in true } - // 2. Create our boss-fight formats let lossyFormat = MockCaptureDeviceFormat( codecType: 1651798066, // 'btp2' width: 4224, @@ -326,28 +324,25 @@ final class CameraSettingsTests: XCTestCase { let captureDeviceMock = MockCaptureDevice() captureDeviceMock.flutterFormats = [lossyFormat, squareFormat, safe4KFormat] - captureDeviceMock.flutterActiveFormat = safe4KFormat // Baseline for preferredSubType - // 3. Configure the camera test environment exactly like the other tests in this file + var currentFormat: CaptureDeviceFormat = safe4KFormat + captureDeviceMock.activeFormatStub = { currentFormat } + captureDeviceMock.setActiveFormatStub = { newFormat in currentFormat = newFormat } + let configuration = CameraTestUtils.createTestCameraConfiguration() configuration.videoCaptureDeviceFactory = { _ in captureDeviceMock } configuration.videoCaptureSession = videoSessionMock - // Allow the config to read our custom width/height values configuration.videoDimensionsConverter = { format in return CMVideoFormatDescriptionGetDimensions(format.formatDescription) } - // CRITICAL: Using default settings keeps `framesPerSecond` nil. - // This stops FormatUtils from throwing away our formats due to missing mock frame rates! configuration.mediaSettings = CameraTestUtils.createDefaultMediaSettings( resolutionPreset: PlatformResolutionPreset.max ) - // 4. Trigger the initialization (this runs our patched highestResolutionFormat math) let _ = CameraTestUtils.createTestCamera(configuration) - // 5. Assert that the camera dodged both traps! let selectedFormat = captureDeviceMock.flutterActiveFormat let selectedDimensions = CMVideoFormatDescriptionGetDimensions(selectedFormat.formatDescription) diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift index f13ef790c63f..2a398aa71787 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift @@ -369,12 +369,11 @@ final class DefaultCamera: NSObject, Camera { let resolution = videoDimensionsConverter(format) let height = UInt(resolution.height) let width = UInt(resolution.width) + let ratio = Double(max(resolution.width, resolution.height)) / Double(min(resolution.width, resolution.height)) - let ratio = max(resolution.width, resolution.height) / min(resolution.width, resolution.height) - let is16x9 = abs(Double(ratio) - Double(16.0 / 9.0)) < 0.05 - - if !is16x9 { - continue + // No squares allowed + if ratio == 1 { + continue } let pixelCount = height * width @@ -388,6 +387,7 @@ final class DefaultCamera: NSObject, Camera { isBestSubTypePreferred = isSubTypePreferred } } + print("DEBUG: Final bestFormat: \(bestFormat != nil ? "Found" : "NIL")") return bestFormat } From b910b018d2a7260c74a1cabc0b65153108a80dbb Mon Sep 17 00:00:00 2001 From: louisehsu Date: Wed, 25 Feb 2026 12:36:55 -0800 Subject: [PATCH 07/12] . --- .../Sources/camera_avfoundation/DefaultCamera.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift index 2a398aa71787..a455e07e9085 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift @@ -353,9 +353,7 @@ final class DefaultCamera: NSObject, Camera { // Apple's lossy compressed formats that Flutter Metal can't render. let unsupportedSubTypes: [FourCharCode] = [ - 1651798066, // 'btp2' - 420YpCbCr8BiPlanarVideoRange (Lossy) - 1651798068, // 'btp4' - 420YpCbCr8BiPlanarFullRange (Lossy) - 1650943852, // 'bgrl' - 32BGRA (Lossy) + 1651798066, // Hex for 'btp2', or kCVPixelFormatType_96VersatileBayerPacked12 ] for format in captureDevice.flutterFormats { @@ -387,7 +385,6 @@ final class DefaultCamera: NSObject, Camera { isBestSubTypePreferred = isSubTypePreferred } } - print("DEBUG: Final bestFormat: \(bestFormat != nil ? "Found" : "NIL")") return bestFormat } From 0f7dbf5442b04a90b5295cd3a816fad6d3edc688 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Thu, 26 Feb 2026 14:32:30 -0800 Subject: [PATCH 08/12] 4/3 --- .../Sources/camera_avfoundation/DefaultCamera.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift index a455e07e9085..2d816bda9432 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift @@ -357,6 +357,7 @@ final class DefaultCamera: NSObject, Camera { ] for format in captureDevice.flutterFormats { + print("format \(format) \n") let subType = CMFormatDescriptionGetMediaSubType(format.formatDescription) // Skip formats that will crash the Flutter Engine @@ -368,9 +369,10 @@ final class DefaultCamera: NSObject, Camera { let height = UInt(resolution.height) let width = UInt(resolution.width) let ratio = Double(max(resolution.width, resolution.height)) / Double(min(resolution.width, resolution.height)) + let is4x3 = abs(ratio - 4.0/3.0) < 0.05 - // No squares allowed - if ratio == 1 { + // Check that the ratio is 4:3 + if !is4x3 { continue } From a6afe3b7ef4767bd73196721bdac1a3084568761 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Thu, 26 Feb 2026 16:43:28 -0800 Subject: [PATCH 09/12] . --- .../Sources/camera_avfoundation/DefaultCamera.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift index 2d816bda9432..ab8e7711d566 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift @@ -371,7 +371,6 @@ final class DefaultCamera: NSObject, Camera { let ratio = Double(max(resolution.width, resolution.height)) / Double(min(resolution.width, resolution.height)) let is4x3 = abs(ratio - 4.0/3.0) < 0.05 - // Check that the ratio is 4:3 if !is4x3 { continue } From 10e20c436ae6ae7e63b90a9856be52c4d50b13a4 Mon Sep 17 00:00:00 2001 From: louisehsu Date: Thu, 26 Feb 2026 16:43:57 -0800 Subject: [PATCH 10/12] . --- .../Sources/camera_avfoundation/DefaultCamera.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift index ab8e7711d566..aac399405143 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift @@ -357,7 +357,6 @@ final class DefaultCamera: NSObject, Camera { ] for format in captureDevice.flutterFormats { - print("format \(format) \n") let subType = CMFormatDescriptionGetMediaSubType(format.formatDescription) // Skip formats that will crash the Flutter Engine From 4a0a70001f5559d5660e32b107bc347524b1169a Mon Sep 17 00:00:00 2001 From: louisehsu Date: Thu, 26 Feb 2026 16:59:59 -0800 Subject: [PATCH 11/12] format --- .../CameraSessionPresetsTests.swift | 4 +- .../ios/RunnerTests/CameraSettingsTests.swift | 102 +++++++++--------- .../Mocks/MockDeviceOrientationProvider.swift | 3 +- .../camera_avfoundation/DefaultCamera.swift | 80 +++++++------- 4 files changed, 96 insertions(+), 93 deletions(-) diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.swift index ea3d8662b82d..6ccb3366b91d 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.swift @@ -32,7 +32,7 @@ final class CameraSessionPresetsTests: XCTestCase { captureDeviceMock.flutterFormats = [captureFormatMock] var currentFormat: CaptureDeviceFormat = captureFormatMock captureDeviceMock.activeFormatStub = { - return currentFormat + return currentFormat } captureDeviceMock.lockForConfigurationStub = { lockForConfigurationExpectation.fulfill() @@ -41,7 +41,7 @@ final class CameraSessionPresetsTests: XCTestCase { let configuration = CameraTestUtils.createTestCameraConfiguration() configuration.videoCaptureDeviceFactory = { _ in captureDeviceMock } configuration.videoDimensionsConverter = { _ in - return CMVideoDimensions(width: 1920, height: 1080) + return CMVideoDimensions(width: 1920, height: 1080) } configuration.videoCaptureSession = videoSessionMock configuration.mediaSettings = CameraTestUtils.createDefaultMediaSettings( diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift index 57e9ea06533d..79f75da6efc9 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift @@ -303,58 +303,58 @@ final class CameraSettingsTests: XCTestCase { } func testResolutionPresetWithMax_mustIgnoreLossyFormatsAndSquares() { - let videoSessionMock = MockCaptureSession() - videoSessionMock.canSetSessionPresetStub = { _ in true } - - let lossyFormat = MockCaptureDeviceFormat( - codecType: 1651798066, // 'btp2' - width: 4224, - height: 3024 - ) - let squareFormat = MockCaptureDeviceFormat( - codecType: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, - width: 4032, - height: 4032 - ) - let safe4KFormat = MockCaptureDeviceFormat( - codecType: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, - width: 3840, - height: 2160 - ) - - let captureDeviceMock = MockCaptureDevice() - captureDeviceMock.flutterFormats = [lossyFormat, squareFormat, safe4KFormat] + let videoSessionMock = MockCaptureSession() + videoSessionMock.canSetSessionPresetStub = { _ in true } + + let lossyFormat = MockCaptureDeviceFormat( + codecType: 1_651_798_066, // 'btp2' + width: 4224, + height: 3024 + ) + let squareFormat = MockCaptureDeviceFormat( + codecType: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + width: 4032, + height: 4032 + ) + let safe4KFormat = MockCaptureDeviceFormat( + codecType: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + width: 3840, + height: 2160 + ) + + let captureDeviceMock = MockCaptureDevice() + captureDeviceMock.flutterFormats = [lossyFormat, squareFormat, safe4KFormat] var currentFormat: CaptureDeviceFormat = safe4KFormat - captureDeviceMock.activeFormatStub = { currentFormat } - captureDeviceMock.setActiveFormatStub = { newFormat in currentFormat = newFormat } - - let configuration = CameraTestUtils.createTestCameraConfiguration() - configuration.videoCaptureDeviceFactory = { _ in captureDeviceMock } - configuration.videoCaptureSession = videoSessionMock - - configuration.videoDimensionsConverter = { format in - return CMVideoFormatDescriptionGetDimensions(format.formatDescription) - } - - configuration.mediaSettings = CameraTestUtils.createDefaultMediaSettings( - resolutionPreset: PlatformResolutionPreset.max - ) - - let _ = CameraTestUtils.createTestCamera(configuration) - - let selectedFormat = captureDeviceMock.flutterActiveFormat - let selectedDimensions = CMVideoFormatDescriptionGetDimensions(selectedFormat.formatDescription) - - XCTAssertEqual( - selectedDimensions.width, - 3840, - "Camera should have ignored the lossy and square formats, safely falling back to 4K." - ) - XCTAssertEqual( - selectedDimensions.height, - 2160, - "Camera should have ignored the lossy and square formats, safely falling back to 4K." - ) + captureDeviceMock.activeFormatStub = { currentFormat } + captureDeviceMock.setActiveFormatStub = { newFormat in currentFormat = newFormat } + + let configuration = CameraTestUtils.createTestCameraConfiguration() + configuration.videoCaptureDeviceFactory = { _ in captureDeviceMock } + configuration.videoCaptureSession = videoSessionMock + + configuration.videoDimensionsConverter = { format in + return CMVideoFormatDescriptionGetDimensions(format.formatDescription) } + + configuration.mediaSettings = CameraTestUtils.createDefaultMediaSettings( + resolutionPreset: PlatformResolutionPreset.max + ) + + let _ = CameraTestUtils.createTestCamera(configuration) + + let selectedFormat = captureDeviceMock.flutterActiveFormat + let selectedDimensions = CMVideoFormatDescriptionGetDimensions(selectedFormat.formatDescription) + + XCTAssertEqual( + selectedDimensions.width, + 3840, + "Camera should have ignored the lossy and square formats, safely falling back to 4K." + ) + XCTAssertEqual( + selectedDimensions.height, + 2160, + "Camera should have ignored the lossy and square formats, safely falling back to 4K." + ) + } } diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockDeviceOrientationProvider.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockDeviceOrientationProvider.swift index 33831dcaf813..65023952cfe9 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockDeviceOrientationProvider.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockDeviceOrientationProvider.swift @@ -2,9 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@testable import camera_avfoundation import UIKit +@testable import camera_avfoundation + // Import Objective-C part of the implementation when SwiftPM is used. #if canImport(camera_avfoundation_objc) import camera_avfoundation_objc diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift index aac399405143..420e52731071 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift @@ -340,53 +340,55 @@ final class DefaultCamera: NSObject, Camera { audioCaptureSession.sessionPreset = videoCaptureSession.sessionPreset } - /// Finds the highest available resolution 16:9 in terms of pixel count for the given device. - /// Preferred are formats with the same subtype as current activeFormat. - private func highestResolutionFormat(forCaptureDevice captureDevice: CaptureDevice) - -> CaptureDeviceFormat? - { - let preferredSubType = CMFormatDescriptionGetMediaSubType( - captureDevice.flutterActiveFormat.formatDescription) - var bestFormat: CaptureDeviceFormat? = nil - var maxPixelCount: UInt = 0 - var isBestSubTypePreferred = false - - // Apple's lossy compressed formats that Flutter Metal can't render. - let unsupportedSubTypes: [FourCharCode] = [ - 1651798066, // Hex for 'btp2', or kCVPixelFormatType_96VersatileBayerPacked12 - ] + /// Finds the highest available resolution 16:9 in terms of pixel count for the given device. + /// Preferred are formats with the same subtype as current activeFormat. + private func highestResolutionFormat(forCaptureDevice captureDevice: CaptureDevice) + -> CaptureDeviceFormat? + { + let preferredSubType = CMFormatDescriptionGetMediaSubType( + captureDevice.flutterActiveFormat.formatDescription) + var bestFormat: CaptureDeviceFormat? = nil + var maxPixelCount: UInt = 0 + var isBestSubTypePreferred = false + + // Apple's lossy compressed formats that Flutter Metal can't render. + let unsupportedSubTypes: [FourCharCode] = [ + 1_651_798_066 // Hex for 'btp2', or kCVPixelFormatType_96VersatileBayerPacked12 + ] - for format in captureDevice.flutterFormats { - let subType = CMFormatDescriptionGetMediaSubType(format.formatDescription) + for format in captureDevice.flutterFormats { + let subType = CMFormatDescriptionGetMediaSubType(format.formatDescription) - // Skip formats that will crash the Flutter Engine - if unsupportedSubTypes.contains(subType) { - continue - } + // Skip formats that will crash the Flutter Engine + if unsupportedSubTypes.contains(subType) { + continue + } - let resolution = videoDimensionsConverter(format) - let height = UInt(resolution.height) - let width = UInt(resolution.width) - let ratio = Double(max(resolution.width, resolution.height)) / Double(min(resolution.width, resolution.height)) - let is4x3 = abs(ratio - 4.0/3.0) < 0.05 + let resolution = videoDimensionsConverter(format) + let height = UInt(resolution.height) + let width = UInt(resolution.width) + let ratio = + Double(max(resolution.width, resolution.height)) + / Double(min(resolution.width, resolution.height)) + let is4x3 = abs(ratio - 4.0 / 3.0) < 0.05 - if !is4x3 { - continue - } + if !is4x3 { + continue + } - let pixelCount = height * width - let isSubTypePreferred = subType == preferredSubType + let pixelCount = height * width + let isSubTypePreferred = subType == preferredSubType - if pixelCount > maxPixelCount - || (pixelCount == maxPixelCount && isSubTypePreferred && !isBestSubTypePreferred) - { - bestFormat = format - maxPixelCount = pixelCount - isBestSubTypePreferred = isSubTypePreferred - } + if pixelCount > maxPixelCount + || (pixelCount == maxPixelCount && isSubTypePreferred && !isBestSubTypePreferred) + { + bestFormat = format + maxPixelCount = pixelCount + isBestSubTypePreferred = isSubTypePreferred } - return bestFormat } + return bestFormat + } func setUpCaptureSessionForAudioIfNeeded() { // Don't setup audio twice or we will lose the audio. From 8829fa20163275bbe8e6118931b9a333c073faac Mon Sep 17 00:00:00 2001 From: louisehsu Date: Thu, 26 Feb 2026 18:30:20 -0800 Subject: [PATCH 12/12] gemini --- .../example/ios/RunnerTests/CameraSessionPresetsTests.swift | 2 +- .../Sources/camera_avfoundation/DefaultCamera.swift | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.swift index 6ccb3366b91d..7f9fd41626ae 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.swift @@ -41,7 +41,7 @@ final class CameraSessionPresetsTests: XCTestCase { let configuration = CameraTestUtils.createTestCameraConfiguration() configuration.videoCaptureDeviceFactory = { _ in captureDeviceMock } configuration.videoDimensionsConverter = { _ in - return CMVideoDimensions(width: 1920, height: 1080) + return CMVideoDimensions(width: 4, height: 3) } configuration.videoCaptureSession = videoSessionMock configuration.mediaSettings = CameraTestUtils.createDefaultMediaSettings( diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift index 420e52731071..f1d0e385312a 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift @@ -340,7 +340,7 @@ final class DefaultCamera: NSObject, Camera { audioCaptureSession.sessionPreset = videoCaptureSession.sessionPreset } - /// Finds the highest available resolution 16:9 in terms of pixel count for the given device. + /// Finds the highest available 4:3 resolution in terms of pixel count for the given device. /// Preferred are formats with the same subtype as current activeFormat. private func highestResolutionFormat(forCaptureDevice captureDevice: CaptureDevice) -> CaptureDeviceFormat? @@ -351,7 +351,6 @@ final class DefaultCamera: NSObject, Camera { var maxPixelCount: UInt = 0 var isBestSubTypePreferred = false - // Apple's lossy compressed formats that Flutter Metal can't render. let unsupportedSubTypes: [FourCharCode] = [ 1_651_798_066 // Hex for 'btp2', or kCVPixelFormatType_96VersatileBayerPacked12 ]