-
Notifications
You must be signed in to change notification settings - Fork 3.6k
[camera_avfoundation] handle interruptions and use single offset #8982
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
I am not sure what this should mean. |
| /// @return a test sample buffer. | ||
| static func createTestSampleBuffer() -> CMSampleBuffer { | ||
| static func createTestSampleBuffer( | ||
| timestamp: CMTime = CMTime.zero, duration: CMTime = CMTimeMake(value: 1, timescale: 44100) |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
|
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. |
|
It is waiting for updates. |
|
Test |
| @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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
|
@misos1 are you still working on updating this PR? Thanks |
|
@hellohuanlin yes |
|
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
|
@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. |
# Conflicts: # packages/camera/camera_avfoundation/CHANGELOG.md # packages/camera/camera_avfoundation/pubspec.yaml
Funny, I have been resolving conflicts of this for almost a year now. |
There was a problem hiding this 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.
.../camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift
Show resolved
Hide resolved
| reportErrorMessage( | ||
| "\(String(describing: notification.userInfo?[AVCaptureSessionErrorKey] as? Error))") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
|
@LouiseHsu Is this on your radar for secondary review? |
LouiseHsu
left a comment
There was a problem hiding this 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
|
It looks like this has the necessary approvals; let's get it landed before anything else can conflict with it :) |
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
…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
…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
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
videoIsDisconnectedis set to true too early after it was previously set to true).Pre-Review Checklist
[shared_preferences]pubspec.yamlwith an appropriate new version according to the pub versioning philosophy, or I have commented below to indicate which version change exemption this PR falls under1.CHANGELOG.mdto add a description of the change, following repository CHANGELOG style, or I have commented below to indicate which CHANGELOG exemption this PR falls under1.///).If you need help, consider asking for advice on the #hackers-new channel on Discord.
Footnotes
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