A React Native video call pauses the camera whenever AppState leaves 'active'; why does video drop out on iOS, and which transitions should pause and resume it?
answer
- not every inactive leads to background
- Notification Center: inactive then active
- Android drawer fires blur, not change
- Android picker Activity reports background
- pause on background, resume on active
basics
~20 sOn iOS, AppState reports inactive for brief interruptions such as Notification Center, the app switcher or a permission prompt, so a check for anything but active tears video down needlessly. Pause on background, resume on active, and guard both.
solid answer
~40 sReact Native maps iOS's resign-active moments to `inactive`, and many of them, such as Notification Center, the app switcher or a permission prompt, return to `active` without reaching `background`, so a `next !== 'active'` check stops and restarts the camera on every glance. Android never reports `inactive`: pulling down the notification drawer only fires the Android-only `blur` event, while another Activity covering the app, including a system picker your app launched, reports `background`. So pause the outgoing video on `background`, resume on `active`, treat iOS `inactive` and Android `blur` as transient, keep a flag so pause and resume run only when the decision changes, apply `AppState.currentState` when subscribing, and remove the subscription in the effect cleanup.
code
tsx · 25 linesimport { useEffect, useRef } from 'react';
import { AppState, type AppStateStatus } from 'react-native';
// The app's own wrapper around its call SDK's outgoing video.
type OutgoingVideo = { setEnabled(enabled: boolean): void };
export function usePauseVideoInBackground(video: OutgoingVideo) {
const paused = useRef(false);
useEffect(() => {
const apply = (next: AppStateStatus) => {
// iOS inactive and other transient values change nothing.
if (next !== 'background' && next !== 'active') return;
const shouldPause = next === 'background';
if (shouldPause !== paused.current) {
paused.current = shouldPause;
video.setEnabled(!shouldPause);
}
};
apply(AppState.currentState);
const subscription = AppState.addEventListener('change', apply);
return () => subscription.remove();
}, [video]);
}go deeper
Know that iOS reports inactive for short interruptions, Android never does, and that background is the one value that means the app has actually left the screen on both platforms.
Explain why a not-active check misfires, map each user action to the values each platform reports, and show where Android's blur and focus events fit.
Demonstrate a handler that pauses on background, resumes on active, is idempotent, applies the current state on mount, and has been exercised on real devices with interruptions and system pickers.
Treat the lifecycle policy as a product decision shared by every media feature, so camera, microphone and playback pause for the same transitions and the rules are tested once.
## The symptom A video-call screen stops sending camera frames when the user leaves the app, which is right: peers should not see a frozen frame, and the app should not hold the camera while it is not on screen. The first version checks `next !== 'active'` in an `AppState` `change` handler and pauses the outgoing video. On iOS the video now drops out and comes back whenever the user pulls down Notification Center, opens the app switcher and returns, or sees a system permission prompt. On Android the same screen sometimes pauses when the app launches a system picker, and does nothing when the user pulls down the notification drawer. ## Why: the platforms report different things **iOS `inactive`.** React Native maps iOS's resign-active moment to `inactive`. That happens on the way to the background, but also for interruptions that end with the app still on screen: Notification Center, the multitasking view, an incoming call, a permission prompt. Those produce `active` then `inactive` then `active`, never reaching `background`. A handler that treats `inactive` as "gone" tears the camera down and rebuilds it for every glance at a notification. **Android `background`.** Android's `AppState` follows the host Activity's pause and resume. It never reports `inactive`. It reports `background` whenever another Activity covers yours, and the React Native docs note that this includes temporary system activities such as autofill credential pickers, even when your own app launched them. **Android `blur` and `focus`.** When the user pulls down the notification drawer, the Activity is not paused, so `AppState` stays `active`. What changes is window focus, which React Native exposes as the Android-only `blur` and `focus` events. | User action | iOS reports | Android reports | |---|---|---| | Switches to another app | `inactive`, then `background` | `background` | | Opens Notification Center or the notification drawer | `inactive`, then `active` | `blur`, then `focus`; state stays `active` | | A system prompt or picker appears | `inactive` for a permission prompt | `background` if it is a separate Activity | | Returns to the app | `active` | `active` | ## Which transitions should drive the camera 1. **Pause on `background`.** It is the one value both platforms report when the app is truly not in the foreground. 2. **Resume on `active`.** Both platforms report it when the user is back. 3. **Treat iOS `inactive` and Android `blur` as transient.** Keep capturing; if you want a visible cue, change the UI (dim the self-view, show "paused" to peers only after a short delay), not the device. 4. **Make pause and resume idempotent.** Keep a flag with the current decision and act only when it changes, so repeated events or a re-subscribed effect do not toggle the device again. 5. **Apply the current state when you subscribe.** Read `AppState.currentState` once when the effect runs, because the screen can mount while the app is not active. An Android `background` caused by a picker your app launched does pause the video. That is usually correct, since the call screen is not visible, but it is worth knowing so the pause is not reported as a bug. ## What AppState does not do `AppState` only observes. Subscribing does not keep the app or its camera running in the background, and it does not report whether the call screen is the focused screen inside your navigation. What the operating system does with a capture session or with the call's audio while the app is backgrounded is decided by native configuration, not by this listener. The listener's job is to keep the app's own state consistent: stop sending video, tell peers, and restart cleanly. ## Checklist for review - The handler names the states it acts on (`background`, `active`) instead of `!== 'active'`. - iOS `inactive` and Android `blur` are handled as transient, if at all. - The pause and resume calls are guarded by a flag. - The subscription is removed in the effect cleanup with `subscription.remove()`. - The behaviour has been tried on a device with Notification Center, the app switcher, a permission prompt and an Android system picker.
- On Android, how can a React Native call screen notice that the user pulled down the notification drawer, and should it pause video then?`AppState` stays `active` because the Activity is not paused; subscribe to the Android-only `blur` event, and `focus` when the drawer closes. It is a transient loss of attention like iOS `inactive`, so keep sending video; at most adjust the UI.
- Why should the pause and resume calls in a React Native AppState handler be guarded by a flag?The handler can run several times for one user action: iOS sends `inactive` then `background`, the effect re-subscribes when its dependencies change, and it also applies `AppState.currentState` on mount. Acting only when the pause decision actually changes avoids restarting the camera needlessly and keeps the device state matching the app's own state.
saying these in an interview costs you the question
- Android reports inactive when the user pulls down the notification drawer.
- On iOS, inactive always means the app is about to go to the background.
- Android reports background only when the user switches to another app.
- An AppState listener keeps the camera running while the app is backgrounded.
- Reading AppState.currentState once on mount is enough to pause and resume.