Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET" />

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
Expand Down
2 changes: 1 addition & 1 deletion example/android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
buildscript {
ext {
buildToolsVersion = "34.0.0"
minSdkVersion = 23
minSdkVersion = 24
compileSdkVersion = 34
targetSdkVersion = 34
ndkVersion = "26.1.10909125"
Expand Down
5 changes: 2 additions & 3 deletions example/ios/VisionCameraExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,6 @@
LastUpgradeCheck = 1250;
TargetAttributes = {
13B07F861A680F5B00A75B9A = {
DevelopmentTeam = CJW62Q77E7;
LastSwiftMigration = 1240;
};
};
Expand Down Expand Up @@ -422,7 +421,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = CJW62Q77E7;
DEVELOPMENT_TEAM = "";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = VisionCameraExample/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Vision Camera";
Expand Down Expand Up @@ -455,7 +454,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = CJW62Q77E7;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = VisionCameraExample/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Vision Camera";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography";
Expand Down
49 changes: 44 additions & 5 deletions example/src/CameraPage.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import * as React from 'react'
import { useRef, useState, useCallback, useMemo } from 'react'
import { useRef, useState, useCallback, useMemo, useEffect } from 'react'
import type { GestureResponderEvent } from 'react-native'
import { StyleSheet, Text, View } from 'react-native'
import type { PinchGestureHandlerGestureEvent } from 'react-native-gesture-handler'
import { PinchGestureHandler, TapGestureHandler } from 'react-native-gesture-handler'
import type { CameraProps, CameraRuntimeError, PhotoFile, VideoFile } from 'react-native-vision-camera'
import {
runAtTargetFps,
useAudioInputDevices,
useCameraDevice,
useCameraFormat,
useFrameProcessor,
useLocationPermission,
useMicrophonePermission,
AudioInputLevel,
useFrameProcessor,
runAtTargetFps,
} from 'react-native-vision-camera'
import { Camera } from 'react-native-vision-camera'
import { CONTENT_SPACING, CONTROL_BUTTON_SIZE, MAX_ZOOM_FACTOR, SAFE_AREA_PADDING, SCREEN_HEIGHT, SCREEN_WIDTH } from './Constants'
import Reanimated, { Extrapolate, interpolate, useAnimatedGestureHandler, useAnimatedProps, useSharedValue } from 'react-native-reanimated'
import { useEffect } from 'react'

import { useIsForeground } from './hooks/useIsForeground'
import { StatusBarBlurBackground } from './views/StatusBarBlurBackground'
import { CaptureButton } from './views/CaptureButton'
Expand All @@ -27,6 +29,7 @@ import type { Routes } from './Routes'
import type { NativeStackScreenProps } from '@react-navigation/native-stack'
import { useIsFocused } from '@react-navigation/core'
import { usePreferredCameraDevice } from './hooks/usePreferredCameraDevice'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { examplePlugin } from './frame-processors/ExamplePlugin'
import { exampleKotlinSwiftPlugin } from './frame-processors/ExampleKotlinSwiftPlugin'

Expand All @@ -39,13 +42,15 @@ const SCALE_FULL_ZOOM = 3

type Props = NativeStackScreenProps<Routes, 'CameraPage'>
export function CameraPage({ navigation }: Props): React.ReactElement {
const audioInputDevices = useAudioInputDevices()
const { bottom } = useSafeAreaInsets()
const [selectedMic, setSelectedMic] = useState(audioInputDevices[0])
const camera = useRef<Camera>(null)
const [isCameraInitialized, setIsCameraInitialized] = useState(false)
const microphone = useMicrophonePermission()
const location = useLocationPermission()
const zoom = useSharedValue(1)
const isPressingButton = useSharedValue(false)

// check if camera page is active
const isFocussed = useIsFocused()
const isForeground = useIsForeground()
Expand Down Expand Up @@ -189,6 +194,16 @@ export function CameraPage({ navigation }: Props): React.ReactElement {
})
}, [])

useEffect(() => {
AudioInputLevel.setPreferredAudioInputDevice(selectedMic?.uid)
const subscription = AudioInputLevel.addAudioLevelChangedListener((level) => {
console.log('Current Audio device level:', level)
})
return () => {
subscription.remove()
}
}, [selectedMic?.uid])

const videoHdr = format?.supportsVideoHdr && enableHdr
const photoHdr = format?.supportsPhotoHdr && enableHdr && !videoHdr

Expand All @@ -201,6 +216,7 @@ export function CameraPage({ navigation }: Props): React.ReactElement {
<ReanimatedCamera
style={StyleSheet.absoluteFill}
device={device}
audioInputDevice={selectedMic}
isActive={isActive}
ref={camera}
onInitialized={onInitialized}
Expand Down Expand Up @@ -283,6 +299,15 @@ export function CameraPage({ navigation }: Props): React.ReactElement {
<IonIcon name="qr-code-outline" color="white" size={24} />
</PressableOpacity>
</View>
<View style={[styles.microphoneContainer, { bottom }]}>
{audioInputDevices.map((item, index) => (
<PressableOpacity key={`mic-${index}`} onPress={() => setSelectedMic(item)} style={styles.microphoneButton}>
<Text style={[styles.microphoneButtonText, item.uid === selectedMic?.uid && styles.microphoneButtonSelectedText]}>
{item.portType}
</Text>
</PressableOpacity>
))}
</View>
</View>
)
}
Expand Down Expand Up @@ -322,4 +347,18 @@ const styles = StyleSheet.create({
justifyContent: 'center',
alignItems: 'center',
},
microphoneContainer: {
position: 'absolute',
left: 12,
top: 100,
},
microphoneButton: {
height: 48,
},
microphoneButtonText: {
color: 'white',
},
microphoneButtonSelectedText: {
color: 'blue',
},
})
2 changes: 1 addition & 1 deletion package/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ android {
}

defaultConfig {
minSdkVersion safeExtGet("minSdkVersion", 21)
minSdkVersion safeExtGet("minSdkVersion", 24)
compileSdkVersion safeExtGet("compileSdkVersion", 34)
targetSdkVersion safeExtGet("targetSdkVersion", 34)
versionCode 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import com.mrousavy.camera.core.types.VideoStabilizationMode
data class CameraConfiguration(
// Input
var cameraId: String? = null,

// Outputs

var audioInputDeviceUid: String? = null,
var preview: Output<Preview> = Output.Disabled.create(),
var photo: Output<Photo> = Output.Disabled.create(),
var video: Output<Video> = Output.Disabled.create(),
Expand Down Expand Up @@ -106,10 +107,12 @@ data class CameraConfiguration(
// (outputOrientation) changed
val orientationChanged: Boolean,
// (locationChanged) changed
val locationChanged: Boolean
val locationChanged: Boolean,
// Audio Input changed (audioInputDeviceUid)
val audioDeviceChanged: Boolean,
) {
val hasChanges: Boolean
get() = deviceChanged || outputsChanged || sidePropsChanged || isActiveChanged || orientationChanged || locationChanged
get() = deviceChanged || outputsChanged || sidePropsChanged || isActiveChanged || orientationChanged || locationChanged || audioDeviceChanged
}

/**
Expand Down Expand Up @@ -148,13 +151,16 @@ data class CameraConfiguration(

val locationChanged = left?.enableLocation != right.enableLocation

val audioDeviceChanged = left?.audioInputDeviceUid != right.audioInputDeviceUid

return Difference(
deviceChanged,
outputsChanged,
sidePropsChanged,
isActiveChanged,
orientationChanged,
locationChanged
locationChanged,
audioDeviceChanged
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,4 @@ class RecordingWhileFrameProcessingUnavailable :
)

class UnknownCameraError(cause: Throwable?) : CameraError("unknown", "unknown", cause?.message ?: "An unknown camera error occured.", cause)
class MediaRecorderFailed
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.mrousavy.camera.core

import android.annotation.SuppressLint
import android.media.MediaRecorder
import android.os.Build
import androidx.annotation.OptIn
import androidx.camera.video.ExperimentalPersistentRecording
import com.mrousavy.camera.core.utils.OutputFile

// handle Audio Recording errors here
@OptIn(ExperimentalPersistentRecording::class)
@SuppressLint("MissingPermission", "RestrictedApi")
fun CameraSession.startAudioRecording(
enableAudio: Boolean,
onError: (error: CameraError) -> Unit
) {
if (enableAudio) {
checkMicrophonePermission()
}
val audioOut = OutputFile(context, context.cacheDir, ".m4a")
audioOutputFile = audioOut.file

audioRecorder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MediaRecorder(context)
} else {
// used for lower api levels
MediaRecorder()
}
audioRecorder?.setAudioSource(MediaRecorder.AudioSource.MIC)
audioRecorder?.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
audioRecorder?.setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
audioRecorder?.setAudioSamplingRate(44100)
audioRecorder?.setAudioChannels(1)
audioRecorder?.setAudioEncodingBitRate(128_000)

val outFile = audioOut.file
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
audioRecorder?.setOutputFile(outFile)
} else {
// used for lower api levels
audioRecorder?.setOutputFile(outFile.absolutePath)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
audioRecorder?.preferredDevice = audioDevice
}
audioRecorder?.prepare()
audioRecorder?.start()

}

fun CameraSession.stopAudioRecording() {
if (audioRecorder != null) {
audioRecorder?.stop()
audioRecorder?.release()
this.audioRecorder = null
}

}

fun CameraSession.cancelAudioRecording() {
isRecordingCanceled = true
stopAudioRecording()
}

fun CameraSession.pauseAudioRecording() {
val audioRecorder = audioRecorder ?: throw NoRecordingInProgressError()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
audioRecorder.pause()
}
}

fun CameraSession.resumeAudioRecording() {
val audioRecorder = audioRecorder ?: throw NoRecordingInProgressError()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
audioRecorder.resume()
}
}

Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.mrousavy.camera.core

import android.annotation.SuppressLint
import android.content.Context
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.util.Log
import androidx.annotation.OptIn
import androidx.camera.core.CameraSelector
Expand Down Expand Up @@ -41,15 +44,30 @@ private fun assertFormatRequirement(
}
}

fun getInputDevice(audioInputDeviceUid: String?, context: Context): AudioDeviceInfo {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val inputDevices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)

val inputDevice = if (audioInputDeviceUid != null) {
inputDevices.firstOrNull { device ->
device.id.toString() == audioInputDeviceUid
}
} else {
null
}
return inputDevice ?: inputDevices.first()
}

@OptIn(ExperimentalGetImage::class)
@SuppressLint("RestrictedApi")
@SuppressLint("RestrictedApi", "NewApi")
@Suppress("LiftReturnOrAssignment")
internal fun CameraSession.configureOutputs(configuration: CameraConfiguration) {
val cameraId = configuration.cameraId!!
Log.i(CameraSession.TAG, "Creating new Outputs for Camera #$cameraId...")
val fpsRange = configuration.targetFpsRange
val format = configuration.format


Log.i(CameraSession.TAG, "Using FPS Range: $fpsRange")

val photoConfig = configuration.photo as? CameraConfiguration.Output.Enabled<CameraConfiguration.Photo>
Expand Down Expand Up @@ -143,6 +161,8 @@ internal fun CameraSession.configureOutputs(configuration: CameraConfiguration)
}
}
}.build()


}

val video = VideoCapture.Builder(recorder).also { video ->
Expand Down Expand Up @@ -347,3 +367,9 @@ internal fun CameraSession.configureIsActive(config: CameraConfiguration) {
lifecycleRegistry.currentState = Lifecycle.State.CREATED
}
}


internal fun CameraSession.configureAudioDevice(configuration: CameraConfiguration, context: Context) {
audioDevice = getInputDevice(configuration.audioInputDeviceUid, context)
Log.i(CameraSession.TAG, audioDevice?.id.toString())
}
Loading