Skip to content

Conversation

@misos1
Copy link
Contributor

@misos1 misos1 commented Apr 2, 2025

Handle interruptions of capture sessions. Stop receiving and writing video and audio samples during such interruptions to avoid synchronization issues between video and audio.

Handle asynchronous error notifications posted about capture sessions. This can happen for example when there is an incoming phone call and then the camera starts recording with audio. The camera controller enters an error state and can be recovered by disposing and recreating.

Use single offset for both video and audio as separate offsets can cause unsynchronized video with audio. Seems audio timestamps tend to be ignored except for the first sample (probably by the video encoder, maybe it is inherent to the video format used) so prefer audio samples for calculating offset to avoid desynchronization. Avoid errors when trying to append samples with timestamp less or equal to the previous sample by not appending them. Fix a bug when after adjusting offset the last sample time was not updated which could cause adding past offsets multiple times (this could happen for example if videoIsDisconnected is set to true too early after it was previously set to true).

Pre-Review Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

Footnotes

  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. 2 3

@misos1
Copy link
Contributor Author

misos1 commented Apr 3, 2025

Mac_arm64 ios_platform_tests_shard_2 master
Testing failed:
	Type 'AVCaptureSession' has no member 'wasInterruptedNotification'
	Command SwiftCompile failed with a nonzero exit code
	Testing cancelled because the build failed.

I am not sure what this should mean. AVCaptureSession.wasInterruptedNotification should be there since ios 4 (I managed to run tests successfully locally on real device and also on simulator).

/// @return a test sample buffer.
static func createTestSampleBuffer() -> CMSampleBuffer {
static func createTestSampleBuffer(
timestamp: CMTime = CMTime.zero, duration: CMTime = CMTimeMake(value: 1, timescale: 44100)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can infer .zero
nit2: split into multiple lines

## 0.9.18+14

* Handle video and audio interruptions and errors.
* Use a single time offset for both video and audio.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... to fix a bug where audio and video are out of sync after interrupted by phone cals.


[NSNotificationCenter.defaultCenter addObserver:self
selector:@selector(captureSessionRuntimeError:)
name:AVCaptureSessionRuntimeErrorNotification
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when will run time error happen?

Copy link
Contributor Author

@misos1 misos1 Apr 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As is in the description "for example when there is an incoming phone call and the camera starts recording with audio". When startVideoRecording is called with enabled audio while there is an incoming call (does not need to be answered in my case, ringing is enough). Audio session then stops running so no more audio can be recorded after this happens until the session is started again or FLTCam recreated (seems the only possibility to recover from the dart side for now). Seems no reasonable detection of this state is possible from the dart side for now (sending error to dart side on this error notification enables such detection).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you put some comments there?

}
return;
}
if (_videoWriterInput.readyForMoreMediaData) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is moved to top right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

@property(assign, nonatomic) CMTime recordingTimeOffset;
@property(assign, nonatomic) CMTime lastSampleEndTime;
@property(nonatomic) AVCaptureOutput *outputForOffsetAdjusting;
@property(assign, nonatomic) CMTime lastAppendedVideoSampleTime;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is lastAppendedVideoSampleTime the same as lastSampleEndTime?

Can you add some doc on these 4 properties?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lastAppendedVideoSampleTime is related to this text from the description "Avoid errors when trying to append samples with timestamp less or equal to the previous sample.". appendPixelBuffer throws an error if a sample with specified sample time (PresentationTime) is appended after another sample with higher or equal time was already appended before (and maybe this should be end time rather than start time too).

@property(assign, nonatomic) CMTime videoTimeOffset;
@property(assign, nonatomic) CMTime audioTimeOffset;
@property(assign, nonatomic) CMTime recordingTimeOffset;
@property(assign, nonatomic) CMTime lastSampleEndTime;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

am i reading it right that

(1) recordingTimeOffset covers videoTimeOffset and audioTimeOffset and

(2) lastSampleEndTime covers lastVideoSampleTime and lastAudioSampleTime?

Copy link
Contributor Author

@misos1 misos1 Apr 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, lastAudioSampleTime was actually the end time of a sample so I named it accordingly. And lastVideoSampleTime was practically end time too, according to logic for offset calculation both before and after this PR, it just happens that video samples tend to not have duration which can be interpreted as zero duration in this case so end time is same as start time. For that logic to work right this needs to be end time of sample, not start time.

@property(assign, nonatomic) CMTime audioTimeOffset;
@property(assign, nonatomic) CMTime recordingTimeOffset;
@property(assign, nonatomic) CMTime lastSampleEndTime;
@property(nonatomic) AVCaptureOutput *outputForOffsetAdjusting;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we didn't have this output. can this be just a local variable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That output is either audio or video output depending on whether audio or video output should be used for offset adjusting.

@property(assign, nonatomic) CMTime recordingTimeOffset;
@property(assign, nonatomic) CMTime lastSampleEndTime;
@property(nonatomic) AVCaptureOutput *outputForOffsetAdjusting;
@property(assign, nonatomic) CMTime lastAppendedVideoSampleTime;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is lastAppendedVideoSampleTime the same as lastSampleEndTime?

Can you add some doc on these 4 properties?

@stuartmorgan-g
Copy link
Collaborator

From triage: what's the status of this PR? It's not clear to me if it's waiting for updates, or waiting for re-review.

@misos1
Copy link
Contributor Author

misos1 commented Jun 6, 2025

It is waiting for updates.

@misos1 misos1 requested a review from vashworth as a code owner July 3, 2025 13:20
@misos1
Copy link
Contributor Author

misos1 commented Jul 3, 2025

Test testImageStreamEventFormat is failing locally with XCTAssertEqual(camera.isStreamingImages, true) XCTAssertEqual failed: ("false") is not equal to ("true"). Seems waitForQueueRoundTrip is "waiting" for the wrong queue as isStreamingImages is changed on captureSessionQueue. But even with the right queue it cannot wait on something which was not already dispatched at the time waitForQueueRoundTrip was called, and the block where that variable changes may not be. There should be a better way to wait for some variable to change in xc test.

@misos1 misos1 requested a review from hellohuanlin July 3, 2025 14:39
@property(assign, nonatomic) BOOL isRecordingDisconnected;
/// Represents sum of all recording pauses/interruptions.
@property(assign, nonatomic) CMTime recordingTimeOffset;
/// Output to use for adjusting of recording offset.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add comment on what "adjusting of recording offset" means and why we need to adjust the offset?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should have been "adjusting of recording time offset" -> recordingTimeOffset which as mentioned is sum of all pauses in recording so adjusting it of course means that if there is another pause then add duration of that pause into that offset. So after that offset is applied then we get continuous recording.

if isFirstVideoSample || !(audioWriterInput?.readyForMoreMediaData ?? false) {
return
}
outputForOffsetAdjusting = output
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add comment on why using the audio output as the outputForOffsetAdjusting

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is because seems that audio samples (except the first) cannot be really moved in time meaning that audioWriterInput.append accepts sample with presentation time but seems ignores that time (except for the first) and just puts next sample at time of end of previous sample so they are always continuous. I suspect that is because these audio samples are stored in a video file in a single big chunk with just single presentation time. So if audio times cannot really be adjusted then video times need to be flexed according to audio times. I cannot be really sure that this is how it works on every platform and device so in case individual audio samples can be adjusted somewhere then it should be irrelevant whether audio or video is used for such adjusting. Thus audio output is preferred for adjusting.

reportErrorMessage("Unable to write to audio input")
}
if !(audioWriterInput?.append(sampleBuffer) ?? false) {
reportErrorMessage("Unable to write to audio input")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i may need some help understanding this change. Can you walk me thought this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That check was moved elsewhere (line 308).

@vashworth vashworth requested review from LouiseHsu and removed request for vashworth December 4, 2025 22:37
@hellohuanlin
Copy link
Contributor

@misos1 are you still working on updating this PR? Thanks

@misos1
Copy link
Contributor Author

misos1 commented Dec 20, 2025

@hellohuanlin yes

@hellohuanlin
Copy link
Contributor

FYI this likely conflict with the ongoing swift migration, and you may have to migrate this PR too.

# Conflicts:
#	packages/camera/camera/CHANGELOG.md
#	packages/camera/camera/pubspec.yaml
#	packages/camera/camera/test/camera_test.dart
#	packages/camera/camera_avfoundation/CHANGELOG.md
#	packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift
#	packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCam.m
#	packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCaptureSession.m
#	packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCam.h
#	packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCaptureSession.h
#	packages/camera/camera_avfoundation/pubspec.yaml
@hellohuanlin
Copy link
Contributor

@misos1 from triage: thank you soooooooo much for working on this. we are making this a draft. Please re-open when it's ready for another review.

@hellohuanlin hellohuanlin marked this pull request as draft January 15, 2026 22:41
@misos1 misos1 marked this pull request as ready for review January 29, 2026 16:35
@misos1
Copy link
Contributor Author

misos1 commented Jan 29, 2026

we are making this a draft. Please re-open when it's ready for another review.

Funny, I have been resolving conflicts of this for almost a year now.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces significant improvements to the camera_avfoundation package for handling capture session interruptions and synchronizing audio/video timestamps. It refactors the timestamp offset logic to use a single offset, preventing desynchronization issues after pauses or interruptions. It also adds handling for asynchronous error notifications from the capture session. The changes are well-tested with new unit tests covering the new offset logic and interruption handling. Additionally, a fix is included in the camera package to correctly handle camera error events at the controller level.

My review identifies a potential memory leak due to un-removed notification observers and suggests an improvement for error message formatting.

Comment on lines +265 to +266
reportErrorMessage(
"\(String(describing: notification.userInfo?[AVCaptureSessionErrorKey] as? Error))")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using String(describing:) on an optional error will include "Optional(...)" in the error message, which is not user-friendly. It's better to unwrap the error and use its localizedDescription, providing a fallback message if the error is nil.

Suggested change
reportErrorMessage(
"\(String(describing: notification.userInfo?[AVCaptureSessionErrorKey] as? Error))")
let errorMessage = (notification.userInfo?[AVCaptureSessionErrorKey] as? Error)?.localizedDescription ?? "An unknown capture session runtime error occurred."
reportErrorMessage(errorMessage)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hellohuanlin I saw that "Optional(...)" string but I do not care a lot. To me a lot of error messages here look not very user-friendly. To me these errors from flutter or flutter plugins are more like debug messages for me. I would rather display something else to the actual user in a lot of cases. So should I proceed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gemini probably assumed this message is displayed to real users. But we actual send it to developers, so unlocalized message should be fine.

@stuartmorgan-g
Copy link
Collaborator

@LouiseHsu Is this on your radar for secondary review?

Copy link
Contributor

@LouiseHsu LouiseHsu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you so much for spending the time to resolve all those conflicts

@stuartmorgan-g
Copy link
Collaborator

It looks like this has the necessary approvals; let's get it landed before anything else can conflict with it :)

@stuartmorgan-g stuartmorgan-g added the autosubmit Merge PR when tree becomes green via auto submit App label Feb 3, 2026
@auto-submit auto-submit bot merged commit 5b1bea8 into flutter:main Feb 3, 2026
80 checks passed
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Feb 3, 2026
github-merge-queue bot pushed a commit to flutter/flutter that referenced this pull request Feb 3, 2026
flutter/packages@837dbbd...5b1bea8

2026-02-03 [email protected]
[camera_avfoundation] handle interruptions and use single offset
(flutter/packages#8982)
2026-02-03 [email protected] [go_router]
Add `TypedQueryParameter` annotation (flutter/packages#10792)
2026-02-02 [email protected] [local_auth_darwin][UIScene] Add
UIScene lifecycle support (flutter/packages#10836)
2026-02-02 49699333+dependabot[bot]@users.noreply.github.com
[dependabot]: Bump camerax_version from 1.5.2 to 1.5.3 in
/packages/camera/camera_android_camerax/android (flutter/packages#10946)
2026-02-02 49699333+dependabot[bot]@users.noreply.github.com
[dependabot]: Bump gradle-wrapper from 8.13 to 9.3.1 in
/packages/video_player/video_player/example/android/app
(flutter/packages#10945)
2026-02-02 49699333+dependabot[bot]@users.noreply.github.com
[dependabot]: Bump gradle-wrapper from 8.13 to 9.3.1 in
/packages/local_auth/local_auth_android/example/android/app
(flutter/packages#10941)
2026-02-02 [email protected] Roll Flutter from
9eafba4 to c305f1f (12 revisions) (flutter/packages#10956)
2026-02-02 [email protected] Roll Flutter from
1d9d6a9 to 9eafba4 (27 revisions) (flutter/packages#10938)
2026-02-02 49699333+dependabot[bot]@users.noreply.github.com
[dependabot]: Bump org.json:json from 20250517 to 20251224 in
/packages/in_app_purchase/in_app_purchase_android/android
(flutter/packages#10949)
2026-02-02 49699333+dependabot[bot]@users.noreply.github.com
[dependabot]: Bump the test-dependencies group across 2 directories with
1 update (flutter/packages#10944)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages-flutter-autoroll
Please CC [email protected] on the revert to ensure that a
human
is aware of the problem.

To file a bug in Flutter:
https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
LongCatIsLooong pushed a commit to LongCatIsLooong/flutter that referenced this pull request Feb 6, 2026
…er#181857)

flutter/packages@837dbbd...5b1bea8

2026-02-03 [email protected]
[camera_avfoundation] handle interruptions and use single offset
(flutter/packages#8982)
2026-02-03 [email protected] [go_router]
Add `TypedQueryParameter` annotation (flutter/packages#10792)
2026-02-02 [email protected] [local_auth_darwin][UIScene] Add
UIScene lifecycle support (flutter/packages#10836)
2026-02-02 49699333+dependabot[bot]@users.noreply.github.com
[dependabot]: Bump camerax_version from 1.5.2 to 1.5.3 in
/packages/camera/camera_android_camerax/android (flutter/packages#10946)
2026-02-02 49699333+dependabot[bot]@users.noreply.github.com
[dependabot]: Bump gradle-wrapper from 8.13 to 9.3.1 in
/packages/video_player/video_player/example/android/app
(flutter/packages#10945)
2026-02-02 49699333+dependabot[bot]@users.noreply.github.com
[dependabot]: Bump gradle-wrapper from 8.13 to 9.3.1 in
/packages/local_auth/local_auth_android/example/android/app
(flutter/packages#10941)
2026-02-02 [email protected] Roll Flutter from
9eafba4 to c305f1f (12 revisions) (flutter/packages#10956)
2026-02-02 [email protected] Roll Flutter from
1d9d6a9 to 9eafba4 (27 revisions) (flutter/packages#10938)
2026-02-02 49699333+dependabot[bot]@users.noreply.github.com
[dependabot]: Bump org.json:json from 20250517 to 20251224 in
/packages/in_app_purchase/in_app_purchase_android/android
(flutter/packages#10949)
2026-02-02 49699333+dependabot[bot]@users.noreply.github.com
[dependabot]: Bump the test-dependencies group across 2 directories with
1 update (flutter/packages#10944)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages-flutter-autoroll
Please CC [email protected] on the revert to ensure that a
human
is aware of the problem.

To file a bug in Flutter:
https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
flutter-zl pushed a commit to flutter-zl/flutter that referenced this pull request Feb 10, 2026
…er#181857)

flutter/packages@837dbbd...5b1bea8

2026-02-03 [email protected]
[camera_avfoundation] handle interruptions and use single offset
(flutter/packages#8982)
2026-02-03 [email protected] [go_router]
Add `TypedQueryParameter` annotation (flutter/packages#10792)
2026-02-02 [email protected] [local_auth_darwin][UIScene] Add
UIScene lifecycle support (flutter/packages#10836)
2026-02-02 49699333+dependabot[bot]@users.noreply.github.com
[dependabot]: Bump camerax_version from 1.5.2 to 1.5.3 in
/packages/camera/camera_android_camerax/android (flutter/packages#10946)
2026-02-02 49699333+dependabot[bot]@users.noreply.github.com
[dependabot]: Bump gradle-wrapper from 8.13 to 9.3.1 in
/packages/video_player/video_player/example/android/app
(flutter/packages#10945)
2026-02-02 49699333+dependabot[bot]@users.noreply.github.com
[dependabot]: Bump gradle-wrapper from 8.13 to 9.3.1 in
/packages/local_auth/local_auth_android/example/android/app
(flutter/packages#10941)
2026-02-02 [email protected] Roll Flutter from
9eafba4 to c305f1f (12 revisions) (flutter/packages#10956)
2026-02-02 [email protected] Roll Flutter from
1d9d6a9 to 9eafba4 (27 revisions) (flutter/packages#10938)
2026-02-02 49699333+dependabot[bot]@users.noreply.github.com
[dependabot]: Bump org.json:json from 20250517 to 20251224 in
/packages/in_app_purchase/in_app_purchase_android/android
(flutter/packages#10949)
2026-02-02 49699333+dependabot[bot]@users.noreply.github.com
[dependabot]: Bump the test-dependencies group across 2 directories with
1 update (flutter/packages#10944)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages-flutter-autoroll
Please CC [email protected] on the revert to ensure that a
human
is aware of the problem.

To file a bug in Flutter:
https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autosubmit Merge PR when tree becomes green via auto submit App p: camera platform-ios platform-macos triage-ios Should be looked at in iOS triage

Projects

None yet

5 participants