Addressing Unexpected Terminations when launching from unlocked Camera Control
Preface
Recently, I found some crashes that are tricky to address, and after a long time of investigations, I finally found out the underlying issue and came up with a workaround for it.
This article relates to the recent one that introduces some explanations on how to adopt the Capture Extension and the Camera Control on iPhone 16 series. For more information, please refer to this article and the GitHub repo.
This article will talk about the detail on how to find the “crash”(actually it’s a termination) and how to provide a workaround for it.
Device info and crash scene
I am using iPhone 16 Pro with iOS 18.2 Beta 4. After finding the reproduction path, I asked my friend to try to reproduce it in iOS 18.1 official release, and I believe this issue also exists in iOS 18.0 (but not prior to iOS 18.0, since it relates to the Camera Control and Capture Extension feature).
The crash occurs “randomly” when using my app PhotonCam. PhotonCam is a camera app with a built-in editor to let users edit photos. When users are in some fullscreen pages, like the settings page to tweak some settings, gallery pages to browse just captured photos, or image edit page to retouch photos, the camera session will be stopped, which is pretty understandable.
At those moments when the camera is not running, the crashes will happen “randomly” - and that is my first impression of the crash. After some investigations, of course, it doesn’t occur randomly.
Crash log analysis
I once managed to recreate this issue when the app is attached with Xcode Debugger, and unlike the traditional crashes, when this crash happened, Xcode just printed:
Terminated due to signal 9
This is the first clue that it’s not a crash but a termination: what obscures me in the first place is that when I was using the TestFlight version of the app, it did ask whether to share the crash log to the developer, and I could find the crash log inside the “Analytics Data”.
Before investigating the actual termination issue, let’s try to analyze the crash log.
Below is the header of the crash log:
Some information we can get from this log:
The Exception Type is EXC_CRASH (SIGKILL), meaning that the operating system terminated the process, often because a background task violated a requirement, device resources were limited, or the user force-quit the app.
The Termination Reason code and reasons are zero, which means that the actual reason is unknown, though it’s not documented.
Then the crash logs says it’s triggered by the Thread 0, which is the main thread:
The main thread is blocked in mach_msg2_trap
waiting for an event to come in. It can’t possibly ‘crash’ while it’s in that state.
And other threads were doing something particularly special and have no information about this crash.
Finally, the thread state is:
Aside from the Address Size fault, there is no other information. At least for my current knowledge, I stuck here for a while until I saw the termination log in Xcode.
Note: Implementing or using a 3rd party crash report may obscure the built-in crash report by Apple, according to this. The crash report above is the one when I turn my 3rd party crash report off, which is AppCenter from Microsoft.
Is it an OOM issue?
App crashing without useful information could be due to the out-of-memory issue. I first found this crash when I was scrolling the gallery list or using the photo editor to edit photos, which could take up much memory and if there is a memory leak it would be worse.
I started checking the memory issue using Xcode tools and no memory leak was found. The memory usage was pretty normal, and importantly, the Jetsam event reported nothing about my app.
Termination investigation
After seeing this log in the Xcode console, I started to think about the potential issues regarding the termination.
Terminated due to signal 9
I just hope that Xcode could tell me the exact reason. Well, actually we could, but not in Xcode.
I believe that the system may have more information about this termination. After attaching my device to the Console app, I managed to filter out some useful logs about the termination.
When the previously thought crash happens, there is a log saying that:
Received termination request from [osservice<com.apple.SpringBoard>:10931] on <RBSProcessPredicate <RBSProcessInstancePredicate| [app<com.juniperphoton.PhotonCam]>> with context <RBSTerminateContext| explanation:Capture Application Requirements Unmet: "Camera not actively used; AVCaptureEventInteraction not installed" reportType:CrashLog maxTerminationResistance:Interactive>
A shorter version will be:
Camera not actively used; AVCaptureEventInteraction not installed.
That’s, the SpringBoard
process acts like a watchdog that monitors whether the app uses the camera feature and registers the AVCaptureEventInteraction
. If it doesn’t, kill the app in a few seconds.
I remember that when I was implementing the LockedCameraCapture
extension, there is a sentence saying:
The app extension terminates shortly after launch if it doesn’t have an active camera view that uses AVCaptureEventInteraction
to handle events from the hardware buttons, or if access to the camera hasn’t been requested.
However, this is regarding the app extension, which is the Capture Extension or LockedCameraCapture
extension itself. The scene that the termination occurs is when the app is launched when my iPhone is unlocked and the app is running in its full-functionality state.
At this point, I can confirm that the issue would happen when:
The app is launched via your
CameraCaptureIntent
implementation. There are two ways to launch your app viaCameraCaptureIntent
: Controls in the Control Center or the physical Camera Control in iPhone 16 series.
Your device is unlocked and running the full version of your app(aka the main app/target).
No camera session is running, neither is the
AVCaptureEventInteraction
installed after the launch.
Putting PhotonCam in this case, the way to reproduce this issue would be:
Launch the app in the desktop and navigate to the photo editor.
Back to the desktop.
Launch the app using Camera Control.
When the app is back to the foreground, wait for a few seconds and it will be terminated.
This issue can also be reproduced with other third-party apps like Blackmagic’s BlackmagicCam. The key thing is to make sure the app is in the state where the camera session is stopped before going to the background.
Workaround
This is a design fault I think, since this logic should be running when the Capture Extension is running, not the main app/target. This is like the issue where launching an app from Controls in the Control Center or Lock Screen will always launch the Capture Extension, no matter whether the device is unlocked or not.
I have reported this issue to Apple, and since I don’t think this could be resolved any time soon, I must think of a solution or workaround.
The top of my head is: according to the experiment that this issue won’t occur as long as it runs the camera session for once after being launched via the Camera Control, can we solve this issue by starting the camera manually in those pages that don’t require/expect the camera to be running?
And after some experiments I come up with this solution:
Detect whether the app is launched/brought to the foreground by our implementation of
CameraCaptureIntent
.
If so, and when the app doesn’t start the camera session at least once for this launch, start the camera session manually and stop it after 2 seconds.
This is more like a workaround than a solution as it brings the important drawback:
Regardless of the resource consumption, the camera will be started unexpectedly, like when users are in the settings page.
Users may panic when the green light is on at that moment. So an explanation to users may be needed in this workaround, to explain that the app is not recording video or capturing photos.
Below, I would introduce some details when implementing this workaround. For the full example, please refer to this GitHub repo.
Tracking launch source
First, we must know whether the app is launched via the CameraCaptureIntent
or not.
Unlike launching from the Capture Extension, launching from CameraCaptureIntent
doesn’t invoke any NSUserActivity
-specific events like scene(_:continue:) or onContinueUserActivity(_:perform:) depending on your app’s lifecycle implementations.
However, the AppIntent’s perform() method will be called in the main app/target’s process. Thus, it’s okay to track the launch source accordingly.
@available(iOS 18.0, *)
struct AppCameraCaptureIntent: CameraCaptureIntent {
@Dependency
var launchSourceTracker: LaunchSourceTracker
@MainActor
func perform() async throws -> some IntentResult {
launchSourceTracker.setLaunchFrom(AppCameraCaptureIntent.self)
return .result()
}
}
To use this dependency in your app intent implementation, remember to register the dependencies using AppDependencyManager
.
Start and stop camera session when active
Next, implement the following workaround logic when the conditions mentioned above are met:
Start the camera session.
Stop the camera session after 2 seconds.
There are a few things to be noticed:
Starting
AVCaptureSession
with no input or output is not enough. You should at least add oneAVCaptureDeviceInput
andAVCaptureVideoDataOutput
as the output.
Setting up
AVCaptureVideoDataOutput’s
delegate is not needed as we are actually getting the video stream.
Setting up Camera Control is also unnecessary.
The stopping method should be called after a few seconds. Stopping right after the camera is started won’t do the trick.
AVCaptureEventInteraction
should be registered or use theonCameraCaptureEvent
on iOS 18.
Report to Apple
I have already reported this issue to Apple through the Feedback Assistant and have also posted it in the Apple Developer Forum. To ensure that Apple becomes aware of this issue, I kindly request that you upvote this post.