From fdf75128c7fb18b9065dd209c3b657238b2c0208 Mon Sep 17 00:00:00 2001 From: Robert Odrowaz Date: Tue, 29 Jul 2025 10:33:47 +0200 Subject: [PATCH 1/2] Ignore non image sample buffers for streaming --- .../camera/camera_avfoundation/CHANGELOG.md | 4 + .../ios/RunnerTests/StreamingTests.swift | 36 +++++- .../camera_avfoundation/DefaultCamera.swift | 113 +++++++++--------- .../camera/camera_avfoundation/pubspec.yaml | 2 +- 4 files changed, 94 insertions(+), 61 deletions(-) diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index 37f3a277038..b7c45549d10 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.21 + +* Fixes crash when streaming is enabled during recording. + ## 0.9.20+7 * Updates kotlin version to 2.2.0 to enable gradle 8.11 support. diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/StreamingTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/StreamingTests.swift index 84023d5cf2c..497923bd06f 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/StreamingTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/StreamingTests.swift @@ -36,6 +36,7 @@ final class StreamingTests: XCTestCase { DefaultCamera, AVCaptureOutput, CMSampleBuffer, + CMSampleBuffer, AVCaptureConnection ) { let captureSessionQueue = DispatchQueue(label: "testing") @@ -45,13 +46,14 @@ final class StreamingTests: XCTestCase { let camera = CameraTestUtils.createTestCamera(configuration) let testAudioOutput = CameraTestUtils.createTestAudioOutput() let sampleBuffer = CameraTestUtils.createTestSampleBuffer() + let audioSampleBuffer = CameraTestUtils.createTestAudioSampleBuffer() let testAudioConnection = CameraTestUtils.createTestConnection(testAudioOutput) - return (camera, testAudioOutput, sampleBuffer, testAudioConnection) + return (camera, testAudioOutput, sampleBuffer, audioSampleBuffer, testAudioConnection) } func testExceedMaxStreamingPendingFramesCount() { - let (camera, testAudioOutput, sampleBuffer, testAudioConnection) = createCamera() + let (camera, testAudioOutput, sampleBuffer, _, testAudioConnection) = createCamera() let handlerMock = MockImageStreamHandler() let finishStartStreamExpectation = expectation( @@ -87,7 +89,7 @@ final class StreamingTests: XCTestCase { } func testReceivedImageStreamData() { - let (camera, testAudioOutput, sampleBuffer, testAudioConnection) = createCamera() + let (camera, testAudioOutput, sampleBuffer, _, testAudioConnection) = createCamera() let handlerMock = MockImageStreamHandler() let finishStartStreamExpectation = expectation( @@ -124,8 +126,34 @@ final class StreamingTests: XCTestCase { waitForExpectations(timeout: 30, handler: nil) } + func testIgnoresNonImageBuffers() { + let (camera, testAudioOutput, _, audioSampleBuffer, testAudioConnection) = createCamera() + let handlerMock = MockImageStreamHandler() + handlerMock.eventSinkStub = { event in + XCTFail() + } + + let finishStartStreamExpectation = expectation( + description: "Finish startStream") + + let messenger = MockFlutterBinaryMessenger() + camera.startImageStream( + with: messenger, imageStreamHandler: handlerMock, + completion: { + _ in + finishStartStreamExpectation.fulfill() + }) + + waitForExpectations(timeout: 30, handler: nil) + XCTAssertEqual(camera.isStreamingImages, true) + + camera.captureOutput(testAudioOutput, didOutput: audioSampleBuffer, from: testAudioConnection) + + waitForQueueRoundTrip(with: DispatchQueue.main) + } + func testImageStreamEventFormat() { - let (camera, testAudioOutput, sampleBuffer, testAudioConnection) = createCamera() + let (camera, testAudioOutput, sampleBuffer, _, testAudioConnection) = createCamera() let expectation = expectation(description: "Received a valid event") 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 3d573b99bca..84bcf33c156 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 @@ -762,66 +762,67 @@ final class DefaultCamera: FLTCam, Camera { if let eventSink = imageStreamHandler?.eventSink, streamingPendingFramesCount < maxStreamingPendingFramesCount { - streamingPendingFramesCount += 1 - - let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)! - // Must lock base address before accessing the pixel data - CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) - - let imageWidth = CVPixelBufferGetWidth(pixelBuffer) - let imageHeight = CVPixelBufferGetHeight(pixelBuffer) - - var planes: [[String: Any]] = [] - - let isPlanar = CVPixelBufferIsPlanar(pixelBuffer) - let planeCount = isPlanar ? CVPixelBufferGetPlaneCount(pixelBuffer) : 1 - - for i in 0.. Date: Thu, 7 Aug 2025 17:09:54 +0200 Subject: [PATCH 2/2] Extract sample buffer streaming logic --- .../camera_avfoundation/DefaultCamera.swift | 144 +++++++++--------- 1 file changed, 76 insertions(+), 68 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 84bcf33c156..7672d343c6d 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 @@ -758,74 +758,7 @@ final class DefaultCamera: FLTCam, Camera { return } - if isStreamingImages { - if let eventSink = imageStreamHandler?.eventSink, - streamingPendingFramesCount < maxStreamingPendingFramesCount - { - if let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) { - streamingPendingFramesCount += 1 - - // Must lock base address before accessing the pixel data - CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) - - let imageWidth = CVPixelBufferGetWidth(pixelBuffer) - let imageHeight = CVPixelBufferGetHeight(pixelBuffer) - - var planes: [[String: Any]] = [] - - let isPlanar = CVPixelBufferIsPlanar(pixelBuffer) - let planeCount = isPlanar ? CVPixelBufferGetPlaneCount(pixelBuffer) : 1 - - for i in 0.. CMSampleBuffer? {