Skip to content

iOS: front camera video encoder permanently stalls shortly after goLive(), recovers only via background/foreground (AppState) cycle #2338

Description

@amamenko

Which package/packages do you use?

  • @stream-io/video-react-sdk
  • @stream-io/video-react-native-sdk
  • @stream-io/video-client

Describe the bug

When a host starts a livestream (call.join({ create: true, ... })) with the front-facing camera, the local video track publishes briefly with real frames, then the encoder permanently stalls a few seconds later: frameRate drops to 0 and totalBytesSent flatlines forever. Both the host's own local preview and remote viewers see a frozen/black frame for the rest of the stream. Audio is unaffected throughout. The back-facing camera never exhibits this issue across many repeated tests on the same device/app/network.

We confirmed via useCallStatsReport() (publisherStats.camera / publisherStats.totalBytesSent) that this is a capture/encode-side stall, not a rendering issue:

// front camera - shortly after call.join() resolves and camera starts publishing
{"frameHeight": 720, "frameRate": 14, "frameWidth": 1280}   totalBytesSent: 225104   highestFPS: 6
{"frameHeight": 720, "frameRate": 0,  "frameWidth": 1280}   totalBytesSent: 963584   highestFPS: 0
{"frameHeight": 720, "frameRate": 0,  "frameWidth": 1280}   totalBytesSent: 1135172  highestFPS: 0
// ...totalBytesSent never increases again for the remainder of the session

Critically, we've reproduced this with zero app-level camera API calls beyond the default call.join() - i.e. we removed all of our own camera.enable()/camera.selectDirection()/camera.disable() calls, and the SDK's own automatic device-config step inside join() (which selects the default camera direction, enables the camera, and publishes automatically) already reproduces the stall on its own when the default direction happens to be "front". This rules out any ordering/timing issue in our own code.

The only thing that reliably recovers a stalled stream is fully backgrounding the app (or entering iOS Picture-in-Picture) and returning to the foreground. That triggers the SDK's own built-in AppStateListener recovery path (camera.disable(true).then(() => camera.enable()), from providers/StreamCall/AppStateListener.js, whose comment literally references this same "local track shown is blank" symptom for PiP). After that cycle, the camera runs perfectly healthy indefinitely (e.g. sustained 1920x1080 @ 30fps, totalBytesSent continuously climbing):

// after backgrounding/returning from PiP - same device, same front camera
{"frameHeight": 1080, "frameRate": 18, "frameWidth": 1920}  totalBytesSent: 1178195
{"frameHeight": 1080, "frameRate": 30, "frameWidth": 1920}  totalBytesSent: 1344934
{"frameHeight": 1080, "frameRate": 30, "frameWidth": 1920}  totalBytesSent: 1721536
{"frameHeight": 1080, "frameRate": 30, "frameWidth": 1920}  totalBytesSent: 1900540

We also checked our own GetStream dashboard "Recent Sessions" - every affected test session was flagged with a Quality Score of 22-36 ("poor"), consistent with a stream that sends real bytes briefly then flatlines for the rest of its duration.

Things we already tried on our end that did not fix it:

  • Calling camera.selectDirection() before vs. after camera.enable()
  • Manually forcing a camera.disable(true)camera.enable() cycle immediately after joining (mirroring the SDK's own AppStateListener recovery, but done proactively at join time instead of waiting for a background/foreground transition)
  • Forcing a live "flip to the opposite direction, then back" cycle via camera.selectDirection() (replicating what a manual camera flip does)
  • Adding delays (300-500ms) around each of the above to rule out a pure timing race
  • Passing camera_facing via settings_override.video on join() so the SDK's automatic device-config step picks the correct direction from the start (this is blocked for us by an RBAC permission error - UpdateCallSettings - for our app's user role, so we couldn't fully test it, but since the default front-camera case already reproduces the bug with no settings_override involved at all, we don't believe this would have helped anyway)

To Reproduce

  1. On a physical iOS device (not simulator), join a livestream call as the host with create: true, with the camera direction resolving to "front" (either via our own camera.selectDirection('front') or via the call type's default camera_facing setting - both reproduce it).
  2. Call goLive().
  3. Watch the local participant's video tile (host) and/or a remote viewer's view of the stream - within a few seconds, the video freezes on a very early (often black/still-adjusting-exposure) frame.
  4. Confirm via useCallStatsReport() that publisherStats.camera.frameRate has dropped to 0 and publisherStats.totalBytesSent is no longer increasing, even though hasVideo(participant) / canShowVideo-style checks still report the track as "publishing".
  5. Background the app or enter iOS PiP, then return to the foreground - video immediately recovers and stays healthy for the rest of the session.
  6. Repeat with the back-facing camera - it does not reproduce.

Expected behavior

The front-facing camera's video track should keep encoding and sending frames continuously after goLive(), the same way the back-facing camera does, without requiring a background/foreground cycle to "unstick" it.

Screenshots

Our GetStream dashboard "Recent Sessions" view shows every affected test session scored 22-36 ("poor" quality), matching the exact sessions where we captured the stalled stats above.

Version

  • @stream-io/video-react-native-sdk: 1.40.2
  • @stream-io/video-client: 1.55.1
  • @stream-io/react-native-webrtc: 145.0.0

Env:

React Native / Expo

  • OS: iOS (physical device, not simulator) - front-facing/TrueDepth camera specifically affected; back-facing camera never reproduces this
  • Device or emulator: real iPhone 16 Pro, iOS 26.5 (physical device)
  • React Native: 0.81.5
  • Expo SDK: 54.0.36
  • New Architecture (Fabric/TurboModules): enabled
  • iOS deployment target: 15.5

Additional context

  • Audio is completely unaffected - only the video track stalls.
  • The stall is reproducible with no app-level camera API calls at all beyond what call.join() does automatically (we tested with the SDK's automatic default-direction device-config path only, removing all of our own manual camera calls, and it still reproduced whenever the resolved direction was "front").
  • We also have react-native-vision-camera (5.0.11) mounted elsewhere in the same screen (used for a separate recording flow, not for the livestream itself) and made sure it's fully deactivated (isActive={false}) before the livestream's call.join() runs, to rule out any dual-camera-session contention - the stall still reproduces with it fully inactive/unmounted.
  • Given the recovery mechanism that works is identical to the SDK's own internal AppStateListener "blank local track" workaround (camera.disable(true)camera.enable()), we suspect this may be the same underlying native capture-session issue that workaround was written for, just also triggerable at initial join time on the front/TrueDepth camera, not only on background/foreground transitions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions