Skip to content
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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

馃悰 CameraRuntimeError: Unknown / Unknown: Error Domain=AVFoundationErrorDomain Code=-11800 "The operation could not be completed" #2632

Closed
4 of 5 tasks
ChristopherGabba opened this issue Mar 5, 2024 · 5 comments
Labels
馃悰 bug Something isn't working

Comments

@ChristopherGabba
Copy link

ChristopherGabba commented Mar 5, 2024

What's happening?

I'm using a custom countdown timer component to invoke a callback when the timer hits 0 to start the camera recording. When the timer hits 0, sometimes the camera records perfectly, other times it throws the error above. I've narrowed the cause down to my countdown component. When I start the recording with a button, it works perfectly every time.

Reproduceable Code

Take this page and copy it directly. Upon countdown completion, you will get this to replicate. If it doesn't work the first time, try it several times until it works.

CAMERA TEST PAGE:
import { observer } from "mobx-react-lite"
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "react"
import { Dimensions, View, ViewStyle } from "react-native"
import { AppStackScreenProps } from "app/navigators"
import { colors } from "../../theme"
import {
  Camera,
  CameraRuntimeError,
  useCameraDevice,
  useCameraFormat,
} from "react-native-vision-camera"
import { useIsForeground } from "app/hooks/useIsForeground"

import { useIsFocused } from "@react-navigation/native"
import Video, { IgnoreSilentSwitchType, ResizeMode } from "react-native-video"
import { Button } from "app/components"
import { PreVideoThumbnailBlur } from "./PreVideoThumbnailBlur"
import { CountdownTimer } from "./CountdownTimer"
import { TimerStatus } from "./timer.types"

const { width, height } = Dimensions.get("window")
interface CameraDebugProps extends AppStackScreenProps<"CameraDebug"> {}

export const CameraDebugScreen: FC<CameraDebugProps> = observer(function RecordingScreen(
  _props, // @demo remove-current-line
) {
  const { navigation } = _props

  // #region - initializations
  const cameraRef = useRef<Camera | null>(null)
  const [initialCountTimerStatus, setInitialCountdownTimerStatus] =
    useState<TimerStatus>("unstarted")

  const [cameraReady, setCameraReady] = useState(false)
  const [videoReady, setVideoReady] = useState(false)
  const [isVertical, setIsVertical] = useState(false)
  const isFocused = useIsFocused()
  const isForeground = useIsForeground()

  const isActive = useMemo(() => {
    return isFocused && isForeground
  }, [isFocused, isForeground])

  const device = useCameraDevice("front")
  const [targetFps] = useState(30)
  const cameraAspectRatio = 130 / 80

  const format = useCameraFormat(device, [
    { videoStabilizationMode: "standard" },
    { fps: targetFps },
    { videoAspectRatio: cameraAspectRatio },
    { videoResolution: { width: 640, height: 480 } },
    { photoAspectRatio: cameraAspectRatio },
    { photoResolution: { width: 640, height: 480 } },
  ])

  // #endregion

  useEffect(() => {
    if (cameraReady && videoReady) {
      setInitialCountdownTimerStatus('play')
    }
  }, [cameraReady, videoReady])

  /**
   * This is just a random 10 second timer to stop everything
   */
  useEffect(() => {
    setTimeout(() => stopEverything(), 10000)
  }, [])


  const [shouldPlay, setShouldPlay] = useState(false)

  const [recordedVideoUrl, setRecordedVideoUrl] = useState("")
  const [recordedDuration, setRecordedDuration] = useState<number>()

  /**
   * Begin the recording sequence and media playback
   */
  async function startEverything() {
    setShouldPlay(true)
    cameraRef.current?.startRecording({
      fileType: "mp4",
      onRecordingFinished: (video) => {
        console.log("RECORDING FINISHED", video)
        setRecordedVideoUrl(video.path)
        setRecordedDuration(video.duration)
      },
      onRecordingError: (error) => {
        console.error("Camera Runtime Error", error)
      },
    })
  }

  /**
   * Callback to tell the camera to stop recording and
   * the video to stop playing
   */

  async function stopEverything() {
    if (!!recordedVideoUrl) return
    setShouldPlay(false)
    cameraRef.current?.stopRecording()
  }

  const onCameraError = useCallback(
    (error: CameraRuntimeError) => {
      switch (error.code) {
        case "session/audio-in-use-by-other-app": {
          alert("The camera or microphone is in use by another app")
          setShouldPlay(false)
          navigation.goBack()
        }
        default:
          break
      }
      // setCameraError(error)
      console.error(error)
    },
    [setShouldPlay],
  )
 
  return (
    <View style={$mediaContainer}>
      <Video
        source={{ uri: "https://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4" }}
        volume={0.8}
        onLoad={(status) => {
          if (status) {
            setVideoReady(true)
            setIsVertical(status.naturalSize.height >= status.naturalSize.width)
          }
        }}
        resizeMode={isVertical ? ResizeMode.COVER : ResizeMode.CONTAIN}
        muted={false}
        ignoreSilentSwitch={IgnoreSilentSwitchType.IGNORE}
        paused={!shouldPlay}
        // mixWithOthers={"mix"}
        style={$mediaContainer}
        onError={(error: any) => console.log(error)}
        onEnd={() => console.log("player ended")}
        onBuffer={(e) => {
          console.log("BufferStatus:", e.isBuffering)
        }}
        onPlaybackStateChanged={(e) => {
          console.log("PlayStatus:", e.isPlaying)
        }}
      />
      <Button style={$endRecordingButton} text={"End Recording"} onPress={stopEverything} />
      <View style={$facetimeCamera}>
        {device && (
          <Camera
            ref={cameraRef}
            video={true}
            audio={true}
            lowLightBoost={device?.supportsLowLightBoost}
            photo={true}
            format={format}
            isActive={isActive}
            device={device}
            onStarted={() => console.log("CAMERA STARTED SESSION")}
            onStopped={() => console.log("CAMERA STOPPED SESSION")}
            style={$camera}
            onInitialized={() => setCameraReady(true)}
            onError={onCameraError}
          />
        )}
      </View>
        <CountdownTimer
          initialValue={3}
          style={$absolute}
          status={initialCountTimerStatus}
          setStatus={setInitialCountdownTimerStatus}
          onTimerComplete={startEverything}
        />
    </View>
  )
})

const $facetimeCamera: ViewStyle = {
  position: "absolute",
  justifyContent: "center",
  alignItems: "center",
  borderRadius: 15,
  bottom: 100,
  left: 50,
  width: 100,
  height: 150,
  backgroundColor: colors.whiteText,
}

const $endRecordingButton: ViewStyle = {
  position: "absolute",
  alignSelf: "center",
  width: 100,
  height: 50,
  bottom: 100,
}

const $mediaContainer: ViewStyle = {
  flexBasis: "100%",
  justifyContent: "center",
  alignItems: "center",
  backgroundColor: colors.background,
  width,
  height,
}

const $camera: ViewStyle = {
  flex: 1,
  backgroundColor: "white",
}

const $absolute: ViewStyle =  {
  position: 'absolute',
  justifyContent:  'center',
  alignItems: 'center'
}

COUNTDOWN TIMER COMPONENT

import { useState, useEffect, useRef, useMemo, useCallback } from "react"
import { CountdownTimerProps, TimerStatus } from "./timer.types"
import { Text } from "app/components"

/**
 * Countdown timer that is used during the recording sequencies
 * in order to alert the user how much time is left.
 */
export const CountdownTimer = (props: CountdownTimerProps) => {
  const { style, textStyle, initialValue, status, setStatus, onTimerStart, onTimerComplete } = props

  const prevStatus = useRef<TimerStatus>("unstarted")

  const [countdown, setCountdown] = useState(initialValue)

  // We do not want the timer to show when it has not been started, or after it completes.
  const shouldHideTimer = useMemo(() => {
    return status === "unstarted" || status === "stop"
  }, [status])

  const resetTimer = useCallback(() => {
    prevStatus.current = "unstarted"
    setStatus("unstarted")
    setCountdown(initialValue)
  }, [prevStatus, setStatus, setCountdown, initialValue])

  useEffect(() => {
    let timeout: NodeJS.Timeout | undefined

    switch (status) {
      case "unstarted":
        return
      case "stop":
        clearTimeout(timeout)
        resetTimer()
        return
      case "pause":
        prevStatus.current = "pause"
        return
      case "play":
        if (countdown === 1) {
          setTimeout(() => {
            onTimerComplete()
            clearTimeout(timeout)
            resetTimer()
          }, 900)
          return
        }
        if (prevStatus.current === "unstarted") {
          prevStatus.current = "play"
          !!onTimerStart && onTimerStart()
        }
    }

    timeout = setTimeout(() => {
      setCountdown((prev) => prev - 1)
    }, 1000)

    return () => clearTimeout(timeout)
  }, [countdown, status,])

  if (shouldHideTimer) return null

  return (
    <View style={[$baseStyle, style]}>
      <Text style={textStyle} preset={"countdownTimer"} text={countdown.toString()} />
    </View>
  )
}

const $baseStyle: ViewStyle = {
  justifyContent: "center",
  alignItems: "center",
}
type TimerStatus = "unstarted" | "play" | "pause" | "stop"

interface CountdownTimerProps {
  /**
   * View style override for the text container
   */
  style?: StyleProp<ViewProps>
  /**
   * Text style override for the text
   */
  textStyle?: StyleProp<TextStyle>
  /**
   * The value you want the timer to start on
   */
  initialValue: number
  /**
   * Optionally hide the actual component from view
   */
  hideTimer?: boolean
  /**
   * The status of the timer:
   * 'not_started', 'play', 'pause', and 'stop'
   */
  status: TimerStatus
  /**
   * Dispatch to set the timer component status
   */
  setStatus: Dispatch<SetStateAction<TimerStatus>>
  /**
   * Callback when the timer starts the countdown.
   */
  onTimerStart?: () => void
  /**
   * Callback when the timer stops the countdown.
   */
  onTimerComplete: () => void
}

Relevant log output

Error occurred: The operation couldn鈥檛 be completed. (OSStatus error -50.)

VisionCamera.startRecording(options:onVideoRecorded:onError:): Starting Video recording...
VisionCamera.startRecording(options:onVideoRecorded:onError:): Will record to temporary file: /private/var/mobile/Containers/Data/Application/F6A8B890-54F1-4ACB-923D-A0D52E4CEFCF/tmp/ReactNative/7D9FC98E-225A-4DD0-B636-354A7EE6922B.mp4
VisionCamera.startRecording(options:onVideoRecorded:onError:): Enabling Audio for Recording...
VisionCamera.activateAudioSession(): Activating Audio Session...
VisionCamera.updateCategory(_:mode:options:): Changing AVAudioSession category from AVAudioSessionCategoryPlayback -> AVAudioSessionCategoryPlayAndRecord
VisionCamera.initializeAudioWriter(withSettings:format:): Initializing Audio AssetWriter with settings: ["AVEncoderQualityForVBRKey": 91, "AVEncoderBitRatePerChannelKey": 96000, "AVFormatIDKey": 1633772320, "AVNumberOfChannelsKey": 1, "AVSampleRateKey": 44100, "AVEncoderBitRateStrategyKey": AVAudioBitRateStrategy_Variable]
VisionCamera.initializeAudioWriter(withSettings:format:): Initialized Audio AssetWriter.
VisionCamera.initializeVideoWriter(withSettings:): Initializing Video AssetWriter with settings: ["AVVideoCodecKey": hvc1, "AVVideoHeightKey": 960, "AVVideoWidthKey": 540, "AVVideoCompressionPropertiesKey": {
    AllowFrameReordering = 1;
    AllowOpenGOP = 1;
    AverageBitRate = 4727808;
    ExpectedFrameRate = 30;
    MaxAllowedFrameQP = 41;
    MaxKeyFrameIntervalDuration = 1;
    MinAllowedFrameQP = 15;
    MinimizeMemoryUsage = 1;
    Priority = 80;
    ProfileLevel = "HEVC_Main_AutoLevel";
    RealTime = 1;
    RelaxAverageBitRateTarget = 1;
}]
VisionCamera.initializeVideoWriter(withSettings:): Initialized Video AssetWriter.
VisionCamera.start(clock:): Starting Asset Writer(s)...
VisionCamera.updateCategory(_:mode:options:): AVAudioSession category changed!
<<<< FigSharedMemPool >>>> Fig assert: "blkHdr->useCount > 0" at  (FigSharedMemPool.c:591) - (err=0)
VisionCamera.sessionRuntimeError(notification:): Unexpected Camera Runtime Error occured!
VisionCamera.start(clock:): Asset Writer(s) started!
VisionCamera.start(clock:): Started RecordingSession at time: 408672.754171666
VisionCamera.startRecording(options:onVideoRecorded:onError:): RecordingSesssion started in 803.766375ms!
VisionCamera.onError(_:): Invoking onError(): Error Domain=AVFoundationErrorDomain Code=-11800 "The operation could not be completed" UserInfo={NSLocalizedFailureReason=An unknown error occurred (-10868), NSLocalizedDescription=The operation could not be completed, NSUnderlyingError=0x28399aa00 {Error Domain=NSOSStatusErrorDomain Code=-10868 "(null)"}}
VisionCamera.activateAudioSession(): Audio Session activated!
CameraRuntimeError: unknown/unknown
Message: Error Domain=AVFoundationErrorDomain Code=-11800 "The operation could not be completed" UserInfo={NSLocalizedFailureReason=An unknown error occurred (-10868), NSLocalizedDescription=The operation could not be completed, NSUnderlyingError=0x28399aa00 {Error Domain=NSOSStatusErrorDomain Code=-10868 "(null)"}}
Code: unknown/unknown
Func: onCameraError
Screen: Recording
Comp: ReelFeelPlayer
'reactionUrl?', false
taking thumbnail in stop everything
[I] <libMMKV.mm:301::-[MMKV onMemoryWarning]> cleaning on memory warning mmkv.default
[I] <MMKV.cpp:308::clearMemoryCache> clearMemoryCache [mmkv.default]
[I] <MemoryFile.cpp:103::close> closing fd[0x15], /var/mobile/Containers/Data/Application/F6A8B890-54F1-4ACB-923D-A0D52E4CEFCF/Documents/mmkv/mmkv.default
VisionCamera.takePhoto(options:promise:): Capturing photo...
VisionCamera.stop(clock:): Requesting stop at 408673.167640708 seconds for AssetWriter with status "writing"...
WARNING: Logging before InitGoogleLogging() is written to STDERR
I0305 14:57:14.502342 1846079488 JSIExecutor.cpp:376] Memory warning (pressure level: TRIM_MEMORY_RUNNING_CRITICAL) received by JS VM, running a GC

VisionCamera.stop(clock:): Waited 4.0 seconds but no late Frames came in, aborting capture...
VisionCamera.finish(): Stopping AssetWriter with status "writing"...
VisionCamera.startRecording(options:onVideoRecorded:onError:): RecordingSession finished with status completed.
VisionCamera.deactivateAudioSession(): Deactivating Audio Session...
VisionCamera.deactivateAudioSession(): Audio Session deactivated!
'RECORDING FINISHED', { path: 'file:///private/var/mobile/Containers/Data/Application/F6A8B890-54F1-4ACB-923D-A0D52E4CEFCF/tmp/ReactNative/7D9FC98E-225A-4DD0-B636-354A7EE6922B.mp4',
  width: 0,
  height: 0,
  duration: 0.442151875 }

Camera Device

{
  "minFocusDistance": 0,
  "formats": [],
  "minZoom": 1,
  "sensorOrientation": "landscape-right",
  "isMultiCam": false,
  "maxZoom": 128.875,
  "name": "Front Camera",
  "hasFlash": true,
  "minExposure": -8,
  "id": "com.apple.avfoundation.avcapturedevice.built-in_video:1",
  "supportsLowLightBoost": false,
  "maxExposure": 8,
  "neutralZoom": 1,
  "physicalDevices": [
    "wide-angle-camera"
  ],
  "supportsFocus": false,
  "supportsRawCapture": false,
  "hasTorch": false,
  "position": "front",
  "hardwareLevel": "full"
}

Device

iPhone 12 Physical Device

VisionCamera Version

3.9.0

Can you reproduce this issue in the VisionCamera Example app?

No, I cannot reproduce the issue in the Example app

Additional information

@ChristopherGabba ChristopherGabba added the 馃悰 bug Something isn't working label Mar 5, 2024
@mrousavy
Copy link
Owner

mrousavy commented Mar 6, 2024

What exactly is Error -10868 again? I remember seeing this somewhere

@ChristopherGabba
Copy link
Author

ChristopherGabba commented Mar 6, 2024

@mrousavy I did a bunch of googling and docs reading on it, but truthfully everyone says that "it doesn't provide any helpful info".

Hate to be that guy, but I asked GPT-4 as a last resort and the response was:

GPT Response The AVFoundationErrorDomain Code=-11800 is an error that occurs in the context of iOS development when dealing with media playback, and it generally indicates that "The operation could not be completed". It's a broad error that can be caused by various issues related to media loading and playback, often indicating that the media file could not be loaded due to issues with its format, encoding, or the way it is being accessed.

Several common reasons for this error could be:

Insecure HTTP URLs being blocked by App Transport Security in iOS, requiring HTTPS instead, or needing exceptions configured in Info.plist if HTTP must be used.
Incorrect or missing file extensions for the media resources, which can lead to the system being unable to recognize the file format.
Issues with the media file encoding or how it's being served from the backend (such as incorrect Content-Type headers or streaming configurations).
Developers have found that verifying the URL, ensuring that the file has the correct extension, and confirming that the backend is properly configured to serve media files can help to resolve this issue. If the issue occurs only in a simulator and not on a real device, it might also be linked to the simulator's limitations or bugs.

I know that it isn't coming from the react-native-video package because it's being thrown in the onCameraError package. The resolution to this issue may be just "hey fix your shit timer code" lol. But figured it may be some edge case you could protect for in the react-native-vision-camera package. I spent a good amount of time making a reproducible demo for you and adding logs this time, hopefully it doesn't take you long to dig into.

There is a line in the logs implying that some sort of state operation interrupted the camera:

VisionCamera.updateCategory(_:mode:options:): AVAudioSession category changed!
<<<< FigSharedMemPool >>>> Fig assert: "blkHdr->useCount > 0" at  (FigSharedMemPool.c:591) - (err=0)
VisionCamera.sessionRuntimeError(notification:): Unexpected Camera Runtime Error occured!

@ChristopherGabba
Copy link
Author

@mrousavy Okay so this issue has plagued me now for a few weeks and I've tried a lot of updates.

  1. I rewrote the timer logic and it's still occurring.
  2. I just changed the react-native-video package to expo-av's video package and the error is gone.

I'm going to assume it's some sort of conflict between react-native-vision-camera and react-native-video. Maybe the AVAudioSessions or something?

@mrousavy
Copy link
Owner

mrousavy commented Mar 6, 2024

Good point, yea it could be the audio sessions.

@mrousavy
Copy link
Owner

Closing this for now, as I think I am doing nothing wrong in my audio session - maybe RN Video doesn#t clean up after they are done with audio.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
馃悰 bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants