Skip to content

Commit bc04e36

Browse files
[camera_avfoundation] Fix crash when streaming while recording (#9691)
In `captureOutput`, we receive both pixel and audio buffers (audio only during recording with audio enabled, when streaming alone is enabled, everything works correctly). Currently, the streaming part of this method doesn't handle non-pixel buffers properly, causing a crash because of force unwrapping of nil. This PR changes the logic to ignore non-pixel buffers for purposes of streaming. Resolves flutter/flutter#172894. ## Pre-Review Checklist **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent 70a13a8 commit bc04e36

File tree

4 files changed

+113
-72
lines changed

4 files changed

+113
-72
lines changed

packages/camera/camera_avfoundation/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.9.21
2+
3+
* Fixes crash when streaming is enabled during recording.
4+
15
## 0.9.20+7
26

37
* Updates kotlin version to 2.2.0 to enable gradle 8.11 support.

packages/camera/camera_avfoundation/example/ios/RunnerTests/StreamingTests.swift

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ final class StreamingTests: XCTestCase {
3636
DefaultCamera,
3737
AVCaptureOutput,
3838
CMSampleBuffer,
39+
CMSampleBuffer,
3940
AVCaptureConnection
4041
) {
4142
let captureSessionQueue = DispatchQueue(label: "testing")
@@ -45,13 +46,14 @@ final class StreamingTests: XCTestCase {
4546
let camera = CameraTestUtils.createTestCamera(configuration)
4647
let testAudioOutput = CameraTestUtils.createTestAudioOutput()
4748
let sampleBuffer = CameraTestUtils.createTestSampleBuffer()
49+
let audioSampleBuffer = CameraTestUtils.createTestAudioSampleBuffer()
4850
let testAudioConnection = CameraTestUtils.createTestConnection(testAudioOutput)
4951

50-
return (camera, testAudioOutput, sampleBuffer, testAudioConnection)
52+
return (camera, testAudioOutput, sampleBuffer, audioSampleBuffer, testAudioConnection)
5153
}
5254

5355
func testExceedMaxStreamingPendingFramesCount() {
54-
let (camera, testAudioOutput, sampleBuffer, testAudioConnection) = createCamera()
56+
let (camera, testAudioOutput, sampleBuffer, _, testAudioConnection) = createCamera()
5557
let handlerMock = MockImageStreamHandler()
5658

5759
let finishStartStreamExpectation = expectation(
@@ -87,7 +89,7 @@ final class StreamingTests: XCTestCase {
8789
}
8890

8991
func testReceivedImageStreamData() {
90-
let (camera, testAudioOutput, sampleBuffer, testAudioConnection) = createCamera()
92+
let (camera, testAudioOutput, sampleBuffer, _, testAudioConnection) = createCamera()
9193
let handlerMock = MockImageStreamHandler()
9294

9395
let finishStartStreamExpectation = expectation(
@@ -124,8 +126,34 @@ final class StreamingTests: XCTestCase {
124126
waitForExpectations(timeout: 30, handler: nil)
125127
}
126128

129+
func testIgnoresNonImageBuffers() {
130+
let (camera, testAudioOutput, _, audioSampleBuffer, testAudioConnection) = createCamera()
131+
let handlerMock = MockImageStreamHandler()
132+
handlerMock.eventSinkStub = { event in
133+
XCTFail()
134+
}
135+
136+
let finishStartStreamExpectation = expectation(
137+
description: "Finish startStream")
138+
139+
let messenger = MockFlutterBinaryMessenger()
140+
camera.startImageStream(
141+
with: messenger, imageStreamHandler: handlerMock,
142+
completion: {
143+
_ in
144+
finishStartStreamExpectation.fulfill()
145+
})
146+
147+
waitForExpectations(timeout: 30, handler: nil)
148+
XCTAssertEqual(camera.isStreamingImages, true)
149+
150+
camera.captureOutput(testAudioOutput, didOutput: audioSampleBuffer, from: testAudioConnection)
151+
152+
waitForQueueRoundTrip(with: DispatchQueue.main)
153+
}
154+
127155
func testImageStreamEventFormat() {
128-
let (camera, testAudioOutput, sampleBuffer, testAudioConnection) = createCamera()
156+
let (camera, testAudioOutput, sampleBuffer, _, testAudioConnection) = createCamera()
129157

130158
let expectation = expectation(description: "Received a valid event")
131159

packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift

Lines changed: 76 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -758,73 +758,7 @@ final class DefaultCamera: FLTCam, Camera {
758758
return
759759
}
760760

761-
if isStreamingImages {
762-
if let eventSink = imageStreamHandler?.eventSink,
763-
streamingPendingFramesCount < maxStreamingPendingFramesCount
764-
{
765-
streamingPendingFramesCount += 1
766-
767-
let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)!
768-
// Must lock base address before accessing the pixel data
769-
CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
770-
771-
let imageWidth = CVPixelBufferGetWidth(pixelBuffer)
772-
let imageHeight = CVPixelBufferGetHeight(pixelBuffer)
773-
774-
var planes: [[String: Any]] = []
775-
776-
let isPlanar = CVPixelBufferIsPlanar(pixelBuffer)
777-
let planeCount = isPlanar ? CVPixelBufferGetPlaneCount(pixelBuffer) : 1
778-
779-
for i in 0..<planeCount {
780-
let planeAddress: UnsafeMutableRawPointer?
781-
let bytesPerRow: Int
782-
let height: Int
783-
let width: Int
784-
785-
if isPlanar {
786-
planeAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, i)
787-
bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, i)
788-
height = CVPixelBufferGetHeightOfPlane(pixelBuffer, i)
789-
width = CVPixelBufferGetWidthOfPlane(pixelBuffer, i)
790-
} else {
791-
planeAddress = CVPixelBufferGetBaseAddress(pixelBuffer)
792-
bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
793-
height = CVPixelBufferGetHeight(pixelBuffer)
794-
width = CVPixelBufferGetWidth(pixelBuffer)
795-
}
796-
797-
let length = bytesPerRow * height
798-
let bytes = Data(bytes: planeAddress!, count: length)
799-
800-
let planeBuffer: [String: Any] = [
801-
"bytesPerRow": bytesPerRow,
802-
"width": width,
803-
"height": height,
804-
"bytes": FlutterStandardTypedData(bytes: bytes),
805-
]
806-
planes.append(planeBuffer)
807-
}
808-
809-
// Lock the base address before accessing pixel data, and unlock it afterwards.
810-
// Done accessing the `pixelBuffer` at this point.
811-
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
812-
813-
let imageBuffer: [String: Any] = [
814-
"width": imageWidth,
815-
"height": imageHeight,
816-
"format": videoFormat,
817-
"planes": planes,
818-
"lensAperture": Double(captureDevice.lensAperture()),
819-
"sensorExposureTime": Int(captureDevice.exposureDuration().seconds * 1_000_000_000),
820-
"sensorSensitivity": Double(captureDevice.iso()),
821-
]
822-
823-
DispatchQueue.main.async {
824-
eventSink(imageBuffer)
825-
}
826-
}
827-
}
761+
handleSampleBufferStreaming(sampleBuffer)
828762

829763
if isRecording && !isRecordingPaused {
830764
if videoWriter?.status == .failed, let error = videoWriter?.error {
@@ -905,6 +839,81 @@ final class DefaultCamera: FLTCam, Camera {
905839
}
906840
}
907841

842+
private func handleSampleBufferStreaming(_ sampleBuffer: CMSampleBuffer) {
843+
guard isStreamingImages,
844+
let eventSink = imageStreamHandler?.eventSink,
845+
streamingPendingFramesCount < maxStreamingPendingFramesCount
846+
else {
847+
return
848+
}
849+
850+
// Non-pixel buffer samples, such as audio samples, are ignored for streaming
851+
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
852+
return
853+
}
854+
855+
streamingPendingFramesCount += 1
856+
857+
// Must lock base address before accessing the pixel data
858+
CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
859+
860+
let imageWidth = CVPixelBufferGetWidth(pixelBuffer)
861+
let imageHeight = CVPixelBufferGetHeight(pixelBuffer)
862+
863+
var planes: [[String: Any]] = []
864+
865+
let isPlanar = CVPixelBufferIsPlanar(pixelBuffer)
866+
let planeCount = isPlanar ? CVPixelBufferGetPlaneCount(pixelBuffer) : 1
867+
868+
for i in 0..<planeCount {
869+
let planeAddress: UnsafeMutableRawPointer?
870+
let bytesPerRow: Int
871+
let height: Int
872+
let width: Int
873+
874+
if isPlanar {
875+
planeAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, i)
876+
bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, i)
877+
height = CVPixelBufferGetHeightOfPlane(pixelBuffer, i)
878+
width = CVPixelBufferGetWidthOfPlane(pixelBuffer, i)
879+
} else {
880+
planeAddress = CVPixelBufferGetBaseAddress(pixelBuffer)
881+
bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
882+
height = CVPixelBufferGetHeight(pixelBuffer)
883+
width = CVPixelBufferGetWidth(pixelBuffer)
884+
}
885+
886+
let length = bytesPerRow * height
887+
let bytes = Data(bytes: planeAddress!, count: length)
888+
889+
let planeBuffer: [String: Any] = [
890+
"bytesPerRow": bytesPerRow,
891+
"width": width,
892+
"height": height,
893+
"bytes": FlutterStandardTypedData(bytes: bytes),
894+
]
895+
planes.append(planeBuffer)
896+
}
897+
898+
// Lock the base address before accessing pixel data, and unlock it afterwards.
899+
// Done accessing the `pixelBuffer` at this point.
900+
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
901+
902+
let imageBuffer: [String: Any] = [
903+
"width": imageWidth,
904+
"height": imageHeight,
905+
"format": videoFormat,
906+
"planes": planes,
907+
"lensAperture": Double(captureDevice.lensAperture()),
908+
"sensorExposureTime": Int(captureDevice.exposureDuration().seconds * 1_000_000_000),
909+
"sensorSensitivity": Double(captureDevice.iso()),
910+
]
911+
912+
DispatchQueue.main.async {
913+
eventSink(imageBuffer)
914+
}
915+
}
916+
908917
private func copySampleBufferWithAdjustedTime(_ sample: CMSampleBuffer, by offset: CMTime)
909918
-> CMSampleBuffer?
910919
{

packages/camera/camera_avfoundation/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: camera_avfoundation
22
description: iOS implementation of the camera plugin.
33
repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_avfoundation
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
5-
version: 0.9.20+7
5+
version: 0.9.21
66

77
environment:
88
sdk: ^3.6.0

0 commit comments

Comments
 (0)