18 Commits

Author SHA1 Message Date
Rui Rodrigues
b006b1e744 add duration to onVideoChunkReady data 2024-07-16 10:46:24 +01:00
Rui Rodrigues
694d9cfa8c add log warning that either onInitReady or onVideoChunkReady are not valid
- however I am not seeing the log when running from yarn
2024-07-16 10:20:13 +01:00
Rui Rodrigues
91767e71c8 default RecordVideoOptions.fileType to mp4
- mov does not support fragmented mp4
2024-07-16 09:50:44 +01:00
Rui Rodrigues
9f2c7906e5 add filePath to define recording directory
- add CaptureError.createRecordingDirectoryError
- stub RCTViewManager to be able to compile CameraViewManager
2024-07-16 09:50:21 +01:00
Rui Rodrigues
621bfe333c format identation 2024-07-16 08:17:06 +01:00
Rui Rodrigues
20f8fa2937 test onInitReady and onVideoChunkReady 2024-07-16 08:07:21 +01:00
Rui Rodrigues
b03f9ea423 add onInitReady to video-camera react native side 2024-07-16 08:04:05 +01:00
Rui Rodrigues
98d90a6442 remove zero padding from chunks file names
- rename index to chunkIndex
2024-07-16 07:59:52 +01:00
Rui Rodrigues
0a43d7a160 add onInitReady callback to send initialization segment path 2024-07-15 09:57:18 +01:00
Rui Rodrigues
a2ce4df663 connect onChunkReady from ChunkedRecorder to react native 2024-07-15 09:57:18 +01:00
Rui Rodrigues
89ecb35616 implement ChunkedRecorder
- save initialization and data chunks as individual files
- ChunkType identifies chunks as initialization or data chunks
- add onChunkReady callback to ChunkedRecorder
2024-07-15 09:57:18 +01:00
Rui Rodrigues
d9a1287b68 WIP - implement ChunkedRecorder
- configure AVAssetWriter for fragmented mp4 output
- implement ChunkedRecorder to received chunk data via AVAssetWriterDelegate
2024-07-12 16:51:09 +01:00
Rafael Bastos
23459b2635 create TestRecorder iOS test app 2024-07-12 08:53:47 +01:00
952e4a93e1 Merge pull request 'pass filePath to RecordingSession' (#3) from loewy/store-video-internally-with-video-id into main
Reviewed-on: #3
Reviewed-by: Ivan Malison <ivanmalison@gmail.com>
2024-03-12 01:21:16 -06:00
Loewy
489171f6f3 take internal storage filePath for RecordingSession, tested 2024-03-11 23:52:04 -07:00
19bf300bbe Support orientation as a parameter to startRecording 2024-02-08 11:17:09 -07:00
1312c5be53 Fix type in Camera.tsx 2024-02-03 20:47:53 -07:00
0e05fc314f Merge pull request 'Add onVideoChunkReady callback' (#2) from ivan/addOnVideoChunkReadyCallback into main
Reviewed-on: #2
2024-02-01 19:43:06 -07:00
31 changed files with 1429 additions and 440 deletions

View File

@@ -13,7 +13,7 @@ import com.mrousavy.camera.types.RecordVideoOptions
import com.mrousavy.camera.utils.makeErrorMap
import java.util.*
suspend fun CameraView.startRecording(options: RecordVideoOptions, onRecordCallback: Callback) {
suspend fun CameraView.startRecording(options: RecordVideoOptions, filePath: String, onRecordCallback: Callback) {
// check audio permission
if (audio == true) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
@@ -33,7 +33,7 @@ suspend fun CameraView.startRecording(options: RecordVideoOptions, onRecordCallb
val errorMap = makeErrorMap(error.code, error.message)
onRecordCallback(null, errorMap)
}
cameraSession.startRecording(audio == true, options, callback, onError)
cameraSession.startRecording(audio == true, options, filePath, callback, onError)
}
@SuppressLint("RestrictedApi")

View File

@@ -95,12 +95,12 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
// TODO: startRecording() cannot be awaited, because I can't have a Promise and a onRecordedCallback in the same function. Hopefully TurboModules allows that
@ReactMethod
fun startRecording(viewTag: Int, jsOptions: ReadableMap, onRecordCallback: Callback) {
fun startRecording(viewTag: Int, jsOptions: ReadableMap, filePath: String, onRecordCallback: Callback) {
coroutineScope.launch {
val view = findCameraView(viewTag)
try {
val options = RecordVideoOptions(jsOptions)
view.startRecording(options, onRecordCallback)
view.startRecording(options, filePath, onRecordCallback)
} catch (error: CameraError) {
val map = makeErrorMap("${error.domain}/${error.id}", error.message, error)
onRecordCallback(null, map)

View File

@@ -621,6 +621,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
suspend fun startRecording(
enableAudio: Boolean,
options: RecordVideoOptions,
filePath: String,
callback: (video: RecordingSession.Video) -> Unit,
onError: (error: CameraError) -> Unit
) {
@@ -640,6 +641,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
videoOutput.enableHdr,
orientation,
options,
filePath,
callback,
onError,
this.callback,

View File

@@ -27,10 +27,11 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu
bitRate: Int,
options: RecordVideoOptions,
outputDirectory: File,
iFrameInterval: Int = 3
iFrameInterval: Int = 5
): ChunkedRecordingManager {
val mimeType = options.videoCodec.toMimeType()
val orientationDegrees = cameraOrientation.toDegrees()
val cameraOrientationDegrees = cameraOrientation.toDegrees()
val recordingOrientationDegrees = (options.orientation ?: Orientation.PORTRAIT).toDegrees();
val (width, height) = if (cameraOrientation.isLandscape()) {
size.height to size.width
} else {
@@ -54,11 +55,13 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval)
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate)
Log.i(TAG, "Video Format: $format, orientation $cameraOrientation")
Log.d(TAG, "Video Format: $format, camera orientation $cameraOrientationDegrees, recordingOrientation: $recordingOrientationDegrees")
// Create a MediaCodec encoder, and configure it with our format. Get a Surface
// we can use for input and wrap it with a class that handles the EGL work.
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
return ChunkedRecordingManager(codec, outputDirectory, 0, iFrameInterval, callbacks)
return ChunkedRecordingManager(
codec, outputDirectory, recordingOrientationDegrees, iFrameInterval, callbacks
)
}
}

View File

@@ -23,6 +23,7 @@ class RecordingSession(
private val hdr: Boolean = false,
private val cameraOrientation: Orientation,
private val options: RecordVideoOptions,
private val filePath: String,
private val callback: (video: Video) -> Unit,
private val onError: (error: CameraError) -> Unit,
private val allCallbacks: CameraSession.Callback,
@@ -37,12 +38,7 @@ class RecordingSession(
data class Video(val path: String, val durationMs: Long, val size: Size)
private val outputPath = run {
val videoDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)
val sdf = SimpleDateFormat("yyyy_MM_dd_HH_mm_ss_SSS", Locale.US)
val videoFileName = "VID_${sdf.format(Date())}"
File(videoDir!!, videoFileName)
}
private val outputPath: File = File(filePath)
private val bitRate = getBitRate()
private val recorder = ChunkedRecordingManager.fromParams(

View File

@@ -8,6 +8,7 @@ class RecordVideoOptions(map: ReadableMap) {
var videoCodec = VideoCodec.H264
var videoBitRateOverride: Double? = null
var videoBitRateMultiplier: Double? = null
var orientation: Orientation? = null
init {
if (map.hasKey("fileType")) {
@@ -25,5 +26,8 @@ class RecordVideoOptions(map: ReadableMap) {
if (map.hasKey("videoBitRateMultiplier")) {
videoBitRateMultiplier = map.getDouble("videoBitRateMultiplier")
}
if (map.hasKey("orientation")) {
orientation = Orientation.fromUnionValue(map.getString("orientation"))
}
}
}

View File

@@ -11,7 +11,7 @@ import AVFoundation
// MARK: - CameraView + AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate
extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate {
func startRecording(options: NSDictionary, callback jsCallback: @escaping RCTResponseSenderBlock) {
func startRecording(options: NSDictionary, filePath: String, callback jsCallback: @escaping RCTResponseSenderBlock) {
// Type-safety
let callback = Callback(jsCallback)
@@ -21,6 +21,7 @@ extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAud
// Start Recording with success and error callbacks
cameraSession.startRecording(
options: options,
filePath: filePath,
onVideoRecorded: { video in
callback.resolve(video.toJSValue())
},

View File

@@ -62,6 +62,8 @@ public final class CameraView: UIView, CameraSessionDelegate {
@objc var onStarted: RCTDirectEventBlock?
@objc var onStopped: RCTDirectEventBlock?
@objc var onViewReady: RCTDirectEventBlock?
@objc var onInitReady: RCTDirectEventBlock?
@objc var onVideoChunkReady: RCTDirectEventBlock?
@objc var onCodeScanned: RCTDirectEventBlock?
// zoom
@objc var enableZoomGesture = false {
@@ -335,6 +337,31 @@ public final class CameraView: UIView, CameraSessionDelegate {
}
#endif
}
func onVideoChunkReady(chunk: ChunkedRecorder.Chunk) {
ReactLogger.log(level: .info, message: "Chunk ready: \(chunk)")
guard let onVideoChunkReady, let onInitReady else {
ReactLogger.log(level: .warning, message: "Either onInitReady or onVideoChunkReady are not valid!")
return
}
switch chunk.type {
case .initialization:
onInitReady([
"filepath": chunk.url.path,
])
case let .data(index: index, duration: duration):
var data: [String: Any] = [
"filepath": chunk.url.path,
"index": index,
]
if let duration {
data["duration"] = duration.seconds
}
onVideoChunkReady(data)
}
}
func onCodeScanned(codes: [CameraSession.Code], scannerFrame: CameraSession.CodeScannerFrame) {
guard let onCodeScanned = onCodeScanned else {

View File

@@ -55,6 +55,8 @@ RCT_EXPORT_VIEW_PROPERTY(onInitialized, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onStarted, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onStopped, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onViewReady, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onInitReady, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoChunkReady, RCTDirectEventBlock);
// Code Scanner
RCT_EXPORT_VIEW_PROPERTY(codeScannerOptions, NSDictionary);
RCT_EXPORT_VIEW_PROPERTY(onCodeScanned, RCTDirectEventBlock);
@@ -62,7 +64,8 @@ RCT_EXPORT_VIEW_PROPERTY(onCodeScanned, RCTDirectEventBlock);
// Camera View Functions
RCT_EXTERN_METHOD(startRecording
: (nonnull NSNumber*)node options
: (NSDictionary*)options onRecordCallback
: (NSDictionary*)options filePath
: (NSString*)filePath onRecordCallback
: (RCTResponseSenderBlock)onRecordCallback);
RCT_EXTERN_METHOD(pauseRecording
: (nonnull NSNumber*)node resolve

View File

@@ -43,9 +43,9 @@ final class CameraViewManager: RCTViewManager {
// This means that any errors that occur in this function have to be delegated through
// the callback, but I'd prefer for them to throw for the original function instead.
@objc
final func startRecording(_ node: NSNumber, options: NSDictionary, onRecordCallback: @escaping RCTResponseSenderBlock) {
final func startRecording(_ node: NSNumber, options: NSDictionary, filePath: NSString, onRecordCallback: @escaping RCTResponseSenderBlock) {
let component = getCameraView(withTag: node)
component.startRecording(options: options, callback: onRecordCallback)
component.startRecording(options: options, filePath: filePath as String, callback: onRecordCallback)
}
@objc

View File

@@ -176,6 +176,7 @@ enum CaptureError {
case noRecordingInProgress
case fileError
case createTempFileError(message: String? = nil)
case createRecordingDirectoryError(message: String? = nil)
case createRecorderError(message: String? = nil)
case videoNotEnabled
case photoNotEnabled
@@ -193,6 +194,8 @@ enum CaptureError {
return "file-io-error"
case .createTempFileError:
return "create-temp-file-error"
case .createRecordingDirectoryError:
return "create-recording-directory-error"
case .createRecorderError:
return "create-recorder-error"
case .videoNotEnabled:
@@ -218,6 +221,8 @@ enum CaptureError {
return "An unexpected File IO error occured!"
case let .createTempFileError(message: message):
return "Failed to create a temporary file! \(message ?? "(no additional message)")"
case let .createRecordingDirectoryError(message: message):
return "Failed to create a recording directory! \(message ?? "(no additional message)")"
case let .createRecorderError(message: message):
return "Failed to create the AVAssetWriter (Recorder)! \(message ?? "(no additional message)")"
case .videoNotEnabled:

View File

@@ -15,6 +15,7 @@ extension CameraSession {
Starts a video + audio recording with a custom Asset Writer.
*/
func startRecording(options: RecordVideoOptions,
filePath: String,
onVideoRecorded: @escaping (_ video: Video) -> Void,
onError: @escaping (_ error: CameraError) -> Void) {
// Run on Camera Queue
@@ -33,6 +34,14 @@ extension CameraSession {
}
let enableAudio = self.configuration?.audio != .disabled
// Callback for when new chunks are ready
let onChunkReady: (ChunkedRecorder.Chunk) -> Void = { chunk in
guard let delegate = self.delegate else {
return
}
delegate.onVideoChunkReady(chunk: chunk)
}
// Callback for when the recording ends
let onFinish = { (recordingSession: RecordingSession, status: AVAssetWriter.Status, error: Error?) in
@@ -62,7 +71,7 @@ extension CameraSession {
} else {
if status == .completed {
// Recording was successfully saved
let video = Video(path: recordingSession.url.absoluteString,
let video = Video(path: recordingSession.outputDiretory.absoluteString,
duration: recordingSession.duration,
size: recordingSession.size ?? CGSize.zero)
onVideoRecorded(video)
@@ -73,22 +82,22 @@ extension CameraSession {
}
}
// Create temporary file
let errorPointer = ErrorPointer(nilLiteral: ())
let fileExtension = options.fileType.descriptor ?? "mov"
guard let tempFilePath = RCTTempFilePath(fileExtension, errorPointer) else {
let message = errorPointer?.pointee?.description
onError(.capture(.createTempFileError(message: message)))
return
if !FileManager.default.fileExists(atPath: filePath) {
do {
try FileManager.default.createDirectory(atPath: filePath, withIntermediateDirectories: true)
} catch {
onError(.capture(.createRecordingDirectoryError(message: error.localizedDescription)))
return
}
}
ReactLogger.log(level: .info, message: "Will record to temporary file: \(tempFilePath)")
let tempURL = URL(string: "file://\(tempFilePath)")!
ReactLogger.log(level: .info, message: "Will record to temporary file: \(filePath)")
do {
// Create RecordingSession for the temp file
let recordingSession = try RecordingSession(url: tempURL,
let recordingSession = try RecordingSession(outputDiretory: filePath,
fileType: options.fileType,
onChunkReady: onChunkReady,
completion: onFinish)
// Init Audio + Activate Audio Session (optional)

View File

@@ -33,6 +33,10 @@ protocol CameraSessionDelegate: AnyObject {
Called for every frame (if video or frameProcessor is enabled)
*/
func onFrame(sampleBuffer: CMSampleBuffer)
/**
Called whenever a new video chunk is available
*/
func onVideoChunkReady(chunk: ChunkedRecorder.Chunk)
/**
Called whenever a QR/Barcode has been scanned. Only if the CodeScanner Output is enabled
*/

View File

@@ -0,0 +1,88 @@
//
// ChunkedRecorder.swift
// VisionCamera
//
// Created by Rafael Bastos on 12/07/2024.
// Copyright © 2024 mrousavy. All rights reserved.
//
import Foundation
import AVFoundation
class ChunkedRecorder: NSObject {
enum ChunkType {
case initialization
case data(index: UInt64, duration: CMTime?)
}
struct Chunk {
let url: URL
let type: ChunkType
}
let outputURL: URL
let onChunkReady: ((Chunk) -> Void)
private var chunkIndex: UInt64 = 0
init(outputURL: URL, onChunkReady: @escaping ((Chunk) -> Void)) throws {
self.outputURL = outputURL
self.onChunkReady = onChunkReady
guard FileManager.default.fileExists(atPath: outputURL.path) else {
throw CameraError.unknown(message: "output directory does not exist at: \(outputURL.path)", cause: nil)
}
}
}
extension ChunkedRecorder: AVAssetWriterDelegate {
func assetWriter(_ writer: AVAssetWriter,
didOutputSegmentData segmentData: Data,
segmentType: AVAssetSegmentType,
segmentReport: AVAssetSegmentReport?) {
switch segmentType {
case .initialization:
saveInitSegment(segmentData)
case .separable:
saveSegment(segmentData, report: segmentReport)
@unknown default:
fatalError("Unknown AVAssetSegmentType!")
}
}
private func saveInitSegment(_ data: Data) {
let url = outputURL.appendingPathComponent("init.mp4")
save(data: data, url: url)
onChunkReady(url: url, type: .initialization)
}
private func saveSegment(_ data: Data, report: AVAssetSegmentReport?) {
let name = "\(chunkIndex).mp4"
let url = outputURL.appendingPathComponent(name)
save(data: data, url: url)
let duration = report?
.trackReports
.filter { $0.mediaType == .video }
.first?
.duration
onChunkReady(url: url, type: .data(index: chunkIndex, duration: duration))
chunkIndex += 1
}
private func save(data: Data, url: URL) {
do {
try data.write(to: url)
} catch {
ReactLogger.log(level: .error, message: "Unable to write \(url): \(error.localizedDescription)")
}
}
private func onChunkReady(url: URL, type: ChunkType) {
onChunkReady(Chunk(url: url, type: type))
}
}

View File

@@ -29,6 +29,7 @@ class RecordingSession {
private let assetWriter: AVAssetWriter
private var audioWriter: AVAssetWriterInput?
private var videoWriter: AVAssetWriterInput?
private let recorder: ChunkedRecorder
private let completionHandler: (RecordingSession, AVAssetWriter.Status, Error?) -> Void
private var startTimestamp: CMTime?
@@ -48,8 +49,8 @@ class RecordingSession {
/**
Gets the file URL of the recorded video.
*/
var url: URL {
return assetWriter.outputURL
var outputDiretory: URL {
return recorder.outputURL
}
/**
@@ -70,14 +71,32 @@ class RecordingSession {
return (lastWrittenTimestamp - startTimestamp).seconds
}
init(url: URL,
init(outputDiretory: String,
fileType: AVFileType,
onChunkReady: @escaping ((ChunkedRecorder.Chunk) -> Void),
completion: @escaping (RecordingSession, AVAssetWriter.Status, Error?) -> Void) throws {
completionHandler = completion
do {
assetWriter = try AVAssetWriter(outputURL: url, fileType: fileType)
let outputURL = URL(fileURLWithPath: outputDiretory)
recorder = try ChunkedRecorder(outputURL: outputURL, onChunkReady: onChunkReady)
assetWriter = AVAssetWriter(contentType: UTType(fileType.rawValue)!)
assetWriter.shouldOptimizeForNetworkUse = false
assetWriter.outputFileTypeProfile = .mpeg4AppleHLS
assetWriter.preferredOutputSegmentInterval = CMTime(seconds: 6, preferredTimescale: 1)
/*
Apple HLS fMP4 does not have an Edit List Box ('elst') in an initialization segment to remove
audio priming duration which advanced audio formats like AAC have, since the sample tables
are empty. As a result, if the output PTS of the first non-fully trimmed audio sample buffer is
kCMTimeZero, the audio samples presentation time in segment files may be pushed forward by the
audio priming duration. This may cause audio and video to be out of sync. You should add a time
offset to all samples to avoid this situation.
*/
let startTimeOffset = CMTime(value: 10, timescale: 1)
assetWriter.initialSegmentStartTime = startTimeOffset
assetWriter.delegate = recorder
} catch let error as NSError {
throw CameraError.capture(.createRecorderError(message: error.description))
}

View File

@@ -0,0 +1,37 @@
//
// AppDelegate.swift
// TestRecorder
//
// Created by Rafael Bastos on 11/07/2024.
// Copyright © 2024 mrousavy. All rights reserved.
//
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}

View File

@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,13 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="TestRecorder" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="VWP-nN-U6K">
<rect key="frame" x="157.33333333333334" y="722.66666666666663" width="78.333333333333343" height="34.333333333333371"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" title="Record"/>
<buttonConfiguration key="configuration" style="filled" title="Record"/>
<connections>
<action selector="toggleRecord:" destination="BYZ-38-t0r" eventType="touchUpInside" id="63a-uH-hTe"/>
</connections>
</button>
</subviews>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="6Tk-OE-BBY" firstAttribute="bottom" secondItem="VWP-nN-U6K" secondAttribute="bottom" constant="61" id="0iW-h7-WDE"/>
<constraint firstItem="VWP-nN-U6K" firstAttribute="centerX" secondItem="6Tk-OE-BBY" secondAttribute="centerX" id="yZb-ba-qfO"/>
</constraints>
</view>
<connections>
<outlet property="recordButton" destination="VWP-nN-U6K" id="gSk-uh-nDX"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="115" y="-27"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,15 @@
//
// ReactStubs.h
// TestRecorder
//
// Created by Rafael Bastos on 12/07/2024.
// Copyright © 2024 mrousavy. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface UIView (React)
- (void)didSetProps:(NSArray<NSString *> *)changedProps;
@end

View File

@@ -0,0 +1,17 @@
//
// ReactStubs.m
// TestRecorder
//
// Created by Rafael Bastos on 12/07/2024.
// Copyright © 2024 mrousavy. All rights reserved.
//
#import "ReactStubs.h"
@implementation UIView (React)
- (void)didSetProps:(__unused NSArray<NSString *> *)changedProps
{
}
@end

View File

@@ -0,0 +1,102 @@
//
// ReactStubs.swift
// TestRecorder
//
// Created by Rafael Bastos on 11/07/2024.
// Copyright © 2024 mrousavy. All rights reserved.
//
import UIKit
enum RCTLogLevel: String {
case trace
case info
case warning
case error
}
enum RCTLogSource {
case native
}
func RCTDefaultLogFunction(_ level: RCTLogLevel, _ source: RCTLogSource, _ file: String, _ line: NSNumber, _ message: String) {
print(level.rawValue, "-", message)
}
typealias RCTDirectEventBlock = (Any?) -> Void
typealias RCTPromiseResolveBlock = (Any?) -> Void
typealias RCTPromiseRejectBlock = (String, String, NSError?) -> Void
typealias RCTResponseSenderBlock = (Any) -> Void
func NSNull() -> [String: String] {
return [:]
}
func makeReactError(_ cameraError: CameraError, cause: NSError?) -> [String: Any] {
var causeDictionary: [String: Any]?
if let cause = cause {
causeDictionary = [
"cause": "\(cause.domain): \(cause.code) \(cause.description)",
"userInfo": cause.userInfo
]
}
return [
"error": "\(cameraError.code): \(cameraError.message)",
"extra": [
"code": cameraError.code,
"message": cameraError.message,
"cause": causeDictionary ?? NSNull(),
]
]
}
func makeReactError(_ cameraError: CameraError) -> [String: Any] {
return makeReactError(cameraError, cause: nil)
}
class RCTFPSGraph: UIView {
convenience init(frame: CGRect, color: UIColor) {
self.init(frame: frame)
}
func onTick(_ tick: CFTimeInterval) {
}
}
func RCTTempFilePath(_ ext: String, _ error: ErrorPointer) -> String? {
let directory = NSTemporaryDirectory().appending("ReactNative")
let fm = FileManager.default
if fm.fileExists(atPath: directory) {
try! fm.removeItem(atPath: directory)
}
if !fm.fileExists(atPath: directory) {
try! fm.createDirectory(atPath: directory, withIntermediateDirectories: true)
}
return directory
.appending("/").appending(UUID().uuidString)
.appending(".").appending(ext)
}
class RCTViewManager: NSObject {
var methodQueue: DispatchQueue! { nil }
class func requiresMainQueueSetup() -> Bool { false }
func view() -> UIView! { nil }
struct Bridge {
let uiManager = UIManager()
}
struct UIManager {
func view(forReactTag: NSNumber) -> UIView! {
nil
}
}
let bridge: Bridge = Bridge()
}

View File

@@ -0,0 +1,53 @@
//
// SceneDelegate.swift
// TestRecorder
//
// Created by Rafael Bastos on 11/07/2024.
// Copyright © 2024 mrousavy. All rights reserved.
//
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
guard let _ = (scene as? UIWindowScene) else { return }
}
func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
}
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}
func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}
func sceneDidEnterBackground(_ scene: UIScene) {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
}
}

View File

@@ -0,0 +1,6 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import "ReactStubs.h"

View File

@@ -0,0 +1,117 @@
//
// ViewController.swift
// TestRecorder
//
// Created by Rafael Bastos on 11/07/2024.
// Copyright © 2024 mrousavy. All rights reserved.
//
import UIKit
import AVFoundation
class ViewController: UIViewController {
@IBOutlet weak var recordButton: UIButton!
let cameraView = CameraView()
let filePath: String = {
NSTemporaryDirectory() + "TestRecorder"
}()
override func viewDidLoad() {
super.viewDidLoad()
try? FileManager.default.removeItem(atPath: filePath)
cameraView.translatesAutoresizingMaskIntoConstraints = false;
view.insertSubview(cameraView, at: 0)
NSLayoutConstraint.activate([
cameraView.topAnchor.constraint(equalTo: view.topAnchor),
cameraView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
cameraView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
cameraView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
recordButton.isHidden = true
cameraView.onInitialized = { _ in
DispatchQueue.main.async {
self.recordButton.isHidden = false
}
}
cameraView.onInitReady = { json in
print("onInitReady:", json ?? "nil")
}
cameraView.onVideoChunkReady = { json in
print("onVideoChunkReady:", json ?? "nil")
}
Task { @MainActor in
await requestAuthorizations()
cameraView.photo = true
cameraView.video = true
cameraView.audio = false
cameraView.isActive = true
cameraView.cameraId = getCameraDeviceId() as NSString?
cameraView.didSetProps([])
}
}
func isAuthorized(for mediaType: AVMediaType) async -> Bool {
let status = AVCaptureDevice.authorizationStatus(for: mediaType)
var isAuthorized = status == .authorized
if status == .notDetermined {
isAuthorized = await AVCaptureDevice.requestAccess(for: mediaType)
}
return isAuthorized
}
func requestAuthorizations() async {
guard await isAuthorized(for: .video) else { return }
guard await isAuthorized(for: .audio) else { return }
// Set up the capture session.
}
private func getCameraDeviceId() -> String? {
let deviceTypes: [AVCaptureDevice.DeviceType] = [
.builtInWideAngleCamera
]
let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: deviceTypes, mediaType: .video, position: .back)
let device = discoverySession.devices.first
return device?.uniqueID
}
@IBAction
func toggleRecord(_ button: UIButton) {
if button.title(for: .normal) == "Stop" {
cameraView.stopRecording(promise: Promise(
resolver: { result in
print("result")
}, rejecter: { code, message, cause in
print("error")
}))
button.setTitle("Record", for: .normal)
button.configuration = .filled()
} else {
cameraView.startRecording(
options: [
"fileType": "mp4",
"videoCodec": "h265",
],
filePath: filePath) { callback in
print("callback", callback)
}
button.setTitle("Stop", for: .normal)
button.configuration = .bordered()
}
}
}

View File

@@ -10,7 +10,7 @@ import AVFoundation
import Foundation
struct RecordVideoOptions {
var fileType: AVFileType = .mov
var fileType: AVFileType = .mp4
var flash: Torch = .off
var codec: AVVideoCodecType?
/**

View File

@@ -7,6 +7,79 @@
objects = {
/* Begin PBXBuildFile section */
B31481772C46547B00084A26 /* CameraViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887518125E0102000DB86D6 /* CameraViewManager.swift */; };
B31481782C46558C00084A26 /* CameraView+TakePhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517125E0102000DB86D6 /* CameraView+TakePhoto.swift */; };
B31481792C46559700084A26 /* CameraView+Focus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A1AEC52AD7F08E00169C0D /* CameraView+Focus.swift */; };
B3AF8E862C410FB700CC198C /* ReactStubs.m in Sources */ = {isa = PBXBuildFile; fileRef = B3AF8E852C410FB700CC198C /* ReactStubs.m */; };
B3AF8E882C41159300CC198C /* ChunkedRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AF8E872C41159300CC198C /* ChunkedRecorder.swift */; };
B3AF8E892C41159300CC198C /* ChunkedRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AF8E872C41159300CC198C /* ChunkedRecorder.swift */; };
B3EF9F0D2C3FBD8300832EE7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3EF9F0C2C3FBD8300832EE7 /* AppDelegate.swift */; };
B3EF9F0F2C3FBD8300832EE7 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3EF9F0E2C3FBD8300832EE7 /* SceneDelegate.swift */; };
B3EF9F112C3FBD8300832EE7 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3EF9F102C3FBD8300832EE7 /* ViewController.swift */; };
B3EF9F142C3FBD8300832EE7 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = B3EF9F132C3FBD8300832EE7 /* Base */; };
B3EF9F162C3FBD8400832EE7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B3EF9F152C3FBD8400832EE7 /* Assets.xcassets */; };
B3EF9F192C3FBD8400832EE7 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = B3EF9F182C3FBD8400832EE7 /* Base */; };
B3EF9F1E2C3FBDCF00832EE7 /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887518425E0102000DB86D6 /* CameraView.swift */; };
B3EF9F1F2C3FBDDC00832EE7 /* ReactLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516F25E0102000DB86D6 /* ReactLogger.swift */; };
B3EF9F212C3FBDFC00832EE7 /* ReactStubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3EF9F202C3FBDFC00832EE7 /* ReactStubs.swift */; };
B3EF9F222C3FBE8200832EE7 /* CameraConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88685E62AD698DF00E93869 /* CameraConfiguration.swift */; };
B3EF9F232C3FBE8B00832EE7 /* VideoStabilizationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85882332AD969E000317161 /* VideoStabilizationMode.swift */; };
B3EF9F242C3FBEBC00832EE7 /* CameraError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887518325E0102000DB86D6 /* CameraError.swift */; };
B3EF9F252C3FBED900832EE7 /* Orientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88103DE2AD6FB230087F063 /* Orientation.swift */; };
B3EF9F262C3FBEEA00832EE7 /* CameraDeviceFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85882312AD966FC00317161 /* CameraDeviceFormat.swift */; };
B3EF9F272C3FBEF800832EE7 /* PixelFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = B87B11BE2A8E63B700732EBF /* PixelFormat.swift */; };
B3EF9F282C3FBF1900832EE7 /* JSUnionValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85882372AD96B4400317161 /* JSUnionValue.swift */; };
B3EF9F292C3FBF2500832EE7 /* Torch.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88103E02AD7046E0087F063 /* Torch.swift */; };
B3EF9F2A2C3FBF3400832EE7 /* CodeScannerOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8FF60AD2ACC9731009D612F /* CodeScannerOptions.swift */; };
B3EF9F2B2C3FBF4100832EE7 /* AVMetadataObject.ObjectType+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8FF60B02ACC981B009D612F /* AVMetadataObject.ObjectType+descriptor.swift */; };
B3EF9F2C2C3FBF4A00832EE7 /* EnumParserError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517325E0102000DB86D6 /* EnumParserError.swift */; };
B3EF9F2D2C3FBF9600832EE7 /* CameraSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88103E22AD7065C0087F063 /* CameraSessionDelegate.swift */; };
B3EF9F2E2C3FBFA600832EE7 /* CameraSession+CodeScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88685EC2AD6A5E600E93869 /* CameraSession+CodeScanner.swift */; };
B3EF9F2F2C3FBFB200832EE7 /* CameraSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88685E42AD68D9300E93869 /* CameraSession.swift */; };
B3EF9F302C3FBFBB00832EE7 /* RecordingSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DB3BC9263DC4D8004C18D7 /* RecordingSession.swift */; };
B3EF9F312C3FBFD500832EE7 /* AVAssetWriter.Status+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DB3BC7263DC28C004C18D7 /* AVAssetWriter.Status+descriptor.swift */; };
B3EF9F322C3FBFF100832EE7 /* CameraQueues.swift in Sources */ = {isa = PBXBuildFile; fileRef = B84760DE2608F57D004C3180 /* CameraQueues.swift */; };
B3EF9F332C3FC00900832EE7 /* CameraSession+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A1AEC72AD8005400169C0D /* CameraSession+Configuration.swift */; };
B3EF9F362C3FC05600832EE7 /* ResizeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80175EB2ABDEBD000E7DE90 /* ResizeMode.swift */; };
B3EF9F372C3FC0CA00832EE7 /* CameraView+Zoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887518225E0102000DB86D6 /* CameraView+Zoom.swift */; };
B3EF9F382C3FC0D900832EE7 /* PreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B83D5EE629377117000AFD2F /* PreviewView.swift */; };
B3EF9F3A2C3FC2EB00832EE7 /* AutoFocusSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85882352AD96AFF00317161 /* AutoFocusSystem.swift */; };
B3EF9F3C2C3FC30D00832EE7 /* AVCaptureDevice.Position+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517C25E0102000DB86D6 /* AVCaptureDevice.Position+descriptor.swift */; };
B3EF9F4A2C3FC31E00832EE7 /* AVFrameRateRange+includes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516725E0102000DB86D6 /* AVFrameRateRange+includes.swift */; };
B3EF9F4B2C3FC31E00832EE7 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D22CDB2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift */; };
B3EF9F4C2C3FC31E00832EE7 /* AVAudioSession+updateCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80E069F266632F000728644 /* AVAudioSession+updateCategory.swift */; };
B3EF9F4D2C3FC31E00832EE7 /* AVCaptureVideoDataOutput+findPixelFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = B881D35F2ABC8E4E009B21C8 /* AVCaptureVideoDataOutput+findPixelFormat.swift */; };
B3EF9F4E2C3FC31E00832EE7 /* AVCaptureOutput+mirror.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516825E0102000DB86D6 /* AVCaptureOutput+mirror.swift */; };
B3EF9F4F2C3FC31E00832EE7 /* Collection+safe.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516225E0102000DB86D6 /* Collection+safe.swift */; };
B3EF9F502C3FC31E00832EE7 /* AVCaptureVideoDataOutput+recommendedVideoSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8207AAE2B0E67460002990F /* AVCaptureVideoDataOutput+recommendedVideoSettings.swift */; };
B3EF9F512C3FC31E00832EE7 /* AVCaptureDevice+minFocusDistance.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88977BD2B556DBA0095C92C /* AVCaptureDevice+minFocusDistance.swift */; };
B3EF9F522C3FC31E00832EE7 /* AVCaptureDevice+physicalDevices.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516625E0102000DB86D6 /* AVCaptureDevice+physicalDevices.swift */; };
B3EF9F532C3FC31E00832EE7 /* AVCaptureDevice+neutralZoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516325E0102000DB86D6 /* AVCaptureDevice+neutralZoom.swift */; };
B3EF9F542C3FC31E00832EE7 /* AVCaptureDevice.Format+dimensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81BE1BE26B936FF002696CC /* AVCaptureDevice.Format+dimensions.swift */; };
B3EF9F552C3FC31E00832EE7 /* AVCaptureVideoDataOutput+pixelFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A1AEC32AD7EDE800169C0D /* AVCaptureVideoDataOutput+pixelFormat.swift */; };
B3EF9F562C3FC31E00832EE7 /* AVCaptureSession+synchronizeBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8207AAC2B0E5DD70002990F /* AVCaptureSession+synchronizeBuffer.swift */; };
B3EF9F572C3FC31E00832EE7 /* AVCaptureDevice+isMultiCam.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516525E0102000DB86D6 /* AVCaptureDevice+isMultiCam.swift */; };
B3EF9F582C3FC31E00832EE7 /* AVCaptureDevice+toDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B881D35D2ABC775E009B21C8 /* AVCaptureDevice+toDictionary.swift */; };
B3EF9F592C3FC31E00832EE7 /* AVCaptureDevice.Format+toDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516A25E0102000DB86D6 /* AVCaptureDevice.Format+toDictionary.swift */; };
B3EF9F5A2C3FC31E00832EE7 /* CMVideoDimensions+toCGSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F127CF2ACF054A00B39EA3 /* CMVideoDimensions+toCGSize.swift */; };
B3EF9F5B2C3FC33000832EE7 /* AVCaptureDevice.DeviceType+physicalDeviceDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517A25E0102000DB86D6 /* AVCaptureDevice.DeviceType+physicalDeviceDescriptor.swift */; };
B3EF9F5C2C3FC33E00832EE7 /* RecordVideoOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A1AEC92AD8034E00169C0D /* RecordVideoOptions.swift */; };
B3EF9F5D2C3FC34600832EE7 /* Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A1AECB2AD803B200169C0D /* Video.swift */; };
B3EF9F5E2C3FC43000832EE7 /* AVCapturePhotoOutput.QualityPrioritization+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517925E0102000DB86D6 /* AVCapturePhotoOutput.QualityPrioritization+descriptor.swift */; };
B3EF9F5F2C3FC43000832EE7 /* AVAuthorizationStatus+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517B25E0102000DB86D6 /* AVAuthorizationStatus+descriptor.swift */; };
B3EF9F602C3FC43000832EE7 /* AVCaptureDevice.FlashMode+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517E25E0102000DB86D6 /* AVCaptureDevice.FlashMode+descriptor.swift */; };
B3EF9F612C3FC43000832EE7 /* AVFileType+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DB3BCB263DC97E004C18D7 /* AVFileType+descriptor.swift */; };
B3EF9F622C3FC43000832EE7 /* AVVideoCodecType+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517525E0102000DB86D6 /* AVVideoCodecType+descriptor.swift */; };
B3EF9F632C3FC43000832EE7 /* AVCaptureDevice.TorchMode+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517725E0102000DB86D6 /* AVCaptureDevice.TorchMode+descriptor.swift */; };
B3EF9F642C3FC43000832EE7 /* AVCaptureDevice.Format.AutoFocusSystem+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517F25E0102000DB86D6 /* AVCaptureDevice.Format.AutoFocusSystem+descriptor.swift */; };
B3EF9F652C3FC43C00832EE7 /* CameraSession+Audio.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88103DA2AD6F0A00087F063 /* CameraSession+Audio.swift */; };
B3EF9F662C3FC44B00832EE7 /* CameraSession+Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88685E82AD6A5D600E93869 /* CameraSession+Video.swift */; };
B3EF9F672C3FC44B00832EE7 /* CameraSession+Photo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88685EA2AD6A5DE00E93869 /* CameraSession+Photo.swift */; };
B3EF9F682C3FC44B00832EE7 /* CameraSession+Focus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88103DC2AD6F62C0087F063 /* CameraSession+Focus.swift */; };
B3EF9F692C3FC44B00832EE7 /* PhotoCaptureDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887515C25E0102000DB86D6 /* PhotoCaptureDelegate.swift */; };
B3EF9F6A2C3FC46900832EE7 /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517025E0102000DB86D6 /* Promise.swift */; };
B3EF9F6B2C3FD35600832EE7 /* CameraView+RecordVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887515D25E0102000DB86D6 /* CameraView+RecordVideo.swift */; };
B3EF9F6C2C3FD36800832EE7 /* Callback.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BD3BA1266E22D2006C80A2 /* Callback.swift */; };
B80175EC2ABDEBD000E7DE90 /* ResizeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80175EB2ABDEBD000E7DE90 /* ResizeMode.swift */; };
B80C0E00260BDDF7001699AB /* FrameProcessorPluginRegistry.m in Sources */ = {isa = PBXBuildFile; fileRef = B80C0DFF260BDDF7001699AB /* FrameProcessorPluginRegistry.m */; };
B80E06A0266632F000728644 /* AVAudioSession+updateCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80E069F266632F000728644 /* AVAudioSession+updateCategory.swift */; };
@@ -94,6 +167,19 @@
/* Begin PBXFileReference section */
134814201AA4EA6300B7C361 /* libVisionCamera.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libVisionCamera.a; sourceTree = BUILT_PRODUCTS_DIR; };
B3AF8E832C410FB600CC198C /* TestRecorder-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "TestRecorder-Bridging-Header.h"; sourceTree = "<group>"; };
B3AF8E842C410FB700CC198C /* ReactStubs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReactStubs.h; sourceTree = "<group>"; };
B3AF8E852C410FB700CC198C /* ReactStubs.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ReactStubs.m; sourceTree = "<group>"; };
B3AF8E872C41159300CC198C /* ChunkedRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChunkedRecorder.swift; sourceTree = "<group>"; };
B3EF9F0A2C3FBD8300832EE7 /* TestRecorder.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestRecorder.app; sourceTree = BUILT_PRODUCTS_DIR; };
B3EF9F0C2C3FBD8300832EE7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
B3EF9F0E2C3FBD8300832EE7 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
B3EF9F102C3FBD8300832EE7 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
B3EF9F132C3FBD8300832EE7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
B3EF9F152C3FBD8400832EE7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
B3EF9F182C3FBD8400832EE7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
B3EF9F1A2C3FBD8400832EE7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B3EF9F202C3FBDFC00832EE7 /* ReactStubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactStubs.swift; sourceTree = "<group>"; };
B80175EB2ABDEBD000E7DE90 /* ResizeMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResizeMode.swift; sourceTree = "<group>"; };
B80C02EB2A6A954D001975E2 /* FrameProcessorPluginHostObject.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FrameProcessorPluginHostObject.mm; sourceTree = "<group>"; };
B80C02EC2A6A9552001975E2 /* FrameProcessorPluginHostObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FrameProcessorPluginHostObject.h; sourceTree = "<group>"; };
@@ -191,6 +277,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
B3EF9F072C3FBD8300832EE7 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -221,10 +314,30 @@
B887516125E0102000DB86D6 /* Extensions */,
B887517225E0102000DB86D6 /* Parsers */,
B887516D25E0102000DB86D6 /* React Utils */,
B3EF9F0B2C3FBD8300832EE7 /* TestRecorder */,
134814211AA4EA7D00B7C361 /* Products */,
B3EF9F0A2C3FBD8300832EE7 /* TestRecorder.app */,
);
sourceTree = "<group>";
};
B3EF9F0B2C3FBD8300832EE7 /* TestRecorder */ = {
isa = PBXGroup;
children = (
B3EF9F0C2C3FBD8300832EE7 /* AppDelegate.swift */,
B3EF9F0E2C3FBD8300832EE7 /* SceneDelegate.swift */,
B3EF9F102C3FBD8300832EE7 /* ViewController.swift */,
B3EF9F202C3FBDFC00832EE7 /* ReactStubs.swift */,
B3AF8E842C410FB700CC198C /* ReactStubs.h */,
B3AF8E852C410FB700CC198C /* ReactStubs.m */,
B3AF8E832C410FB600CC198C /* TestRecorder-Bridging-Header.h */,
B3EF9F122C3FBD8300832EE7 /* Main.storyboard */,
B3EF9F152C3FBD8400832EE7 /* Assets.xcassets */,
B3EF9F172C3FBD8400832EE7 /* LaunchScreen.storyboard */,
B3EF9F1A2C3FBD8400832EE7 /* Info.plist */,
);
path = TestRecorder;
sourceTree = "<group>";
};
B80175EA2ABDEBBB00E7DE90 /* Types */ = {
isa = PBXGroup;
children = (
@@ -257,6 +370,7 @@
B88103E22AD7065C0087F063 /* CameraSessionDelegate.swift */,
B83D5EE629377117000AFD2F /* PreviewView.swift */,
B8DB3BC9263DC4D8004C18D7 /* RecordingSession.swift */,
B3AF8E872C41159300CC198C /* ChunkedRecorder.swift */,
B887515C25E0102000DB86D6 /* PhotoCaptureDelegate.swift */,
B84760DE2608F57D004C3180 /* CameraQueues.swift */,
B887518325E0102000DB86D6 /* CameraError.swift */,
@@ -366,18 +480,42 @@
productReference = 134814201AA4EA6300B7C361 /* libVisionCamera.a */;
productType = "com.apple.product-type.library.static";
};
B3EF9F092C3FBD8300832EE7 /* TestRecorder */ = {
isa = PBXNativeTarget;
buildConfigurationList = B3EF9F1D2C3FBD8400832EE7 /* Build configuration list for PBXNativeTarget "TestRecorder" */;
buildPhases = (
B3EF9F062C3FBD8300832EE7 /* Sources */,
B3EF9F072C3FBD8300832EE7 /* Frameworks */,
B3EF9F082C3FBD8300832EE7 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = TestRecorder;
productName = TestRecorder;
productReference = B3EF9F0A2C3FBD8300832EE7 /* TestRecorder.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
58B511D31A9E6C8500147676 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1540;
LastUpgradeCheck = 1240;
ORGANIZATIONNAME = mrousavy;
TargetAttributes = {
58B511DA1A9E6C8500147676 = {
CreatedOnToolsVersion = 6.1.1;
};
B3EF9F092C3FBD8300832EE7 = {
CreatedOnToolsVersion = 15.4;
DevelopmentTeam = HP3AMBWJGS;
LastSwiftMigration = 1540;
ProvisioningStyle = Automatic;
};
};
};
buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "VisionCamera" */;
@@ -387,6 +525,7 @@
knownRegions = (
English,
en,
Base,
);
mainGroup = 58B511D21A9E6C8500147676;
productRefGroup = 58B511D21A9E6C8500147676;
@@ -394,10 +533,24 @@
projectRoot = "";
targets = (
58B511DA1A9E6C8500147676 /* VisionCamera */,
B3EF9F092C3FBD8300832EE7 /* TestRecorder */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
B3EF9F082C3FBD8300832EE7 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B3EF9F162C3FBD8400832EE7 /* Assets.xcassets in Resources */,
B3EF9F192C3FBD8400832EE7 /* Base in Resources */,
B3EF9F142C3FBD8300832EE7 /* Base in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
B80D6CAB25F770FE006F2CB7 /* Run SwiftFormat */ = {
isa = PBXShellScriptBuildPhase;
@@ -490,6 +643,7 @@
B88977BE2B556DBA0095C92C /* AVCaptureDevice+minFocusDistance.swift in Sources */,
B80175EC2ABDEBD000E7DE90 /* ResizeMode.swift in Sources */,
B887519F25E0102000DB86D6 /* AVCaptureDevice.DeviceType+physicalDeviceDescriptor.swift in Sources */,
B3AF8E882C41159300CC198C /* ChunkedRecorder.swift in Sources */,
B88685ED2AD6A5E600E93869 /* CameraSession+CodeScanner.swift in Sources */,
B8207AAD2B0E5DD70002990F /* AVCaptureSession+synchronizeBuffer.swift in Sources */,
B8D22CDC2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */,
@@ -516,8 +670,103 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
B3EF9F062C3FBD8300832EE7 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B3EF9F372C3FC0CA00832EE7 /* CameraView+Zoom.swift in Sources */,
B3EF9F232C3FBE8B00832EE7 /* VideoStabilizationMode.swift in Sources */,
B3EF9F4A2C3FC31E00832EE7 /* AVFrameRateRange+includes.swift in Sources */,
B3EF9F6A2C3FC46900832EE7 /* Promise.swift in Sources */,
B3EF9F4B2C3FC31E00832EE7 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */,
B3EF9F5E2C3FC43000832EE7 /* AVCapturePhotoOutput.QualityPrioritization+descriptor.swift in Sources */,
B3AF8E892C41159300CC198C /* ChunkedRecorder.swift in Sources */,
B3EF9F5F2C3FC43000832EE7 /* AVAuthorizationStatus+descriptor.swift in Sources */,
B3EF9F602C3FC43000832EE7 /* AVCaptureDevice.FlashMode+descriptor.swift in Sources */,
B3EF9F612C3FC43000832EE7 /* AVFileType+descriptor.swift in Sources */,
B3EF9F622C3FC43000832EE7 /* AVVideoCodecType+descriptor.swift in Sources */,
B3EF9F632C3FC43000832EE7 /* AVCaptureDevice.TorchMode+descriptor.swift in Sources */,
B3EF9F642C3FC43000832EE7 /* AVCaptureDevice.Format.AutoFocusSystem+descriptor.swift in Sources */,
B3EF9F4C2C3FC31E00832EE7 /* AVAudioSession+updateCategory.swift in Sources */,
B3EF9F4D2C3FC31E00832EE7 /* AVCaptureVideoDataOutput+findPixelFormat.swift in Sources */,
B3EF9F4E2C3FC31E00832EE7 /* AVCaptureOutput+mirror.swift in Sources */,
B3EF9F4F2C3FC31E00832EE7 /* Collection+safe.swift in Sources */,
B3EF9F502C3FC31E00832EE7 /* AVCaptureVideoDataOutput+recommendedVideoSettings.swift in Sources */,
B3EF9F512C3FC31E00832EE7 /* AVCaptureDevice+minFocusDistance.swift in Sources */,
B3EF9F5B2C3FC33000832EE7 /* AVCaptureDevice.DeviceType+physicalDeviceDescriptor.swift in Sources */,
B31481792C46559700084A26 /* CameraView+Focus.swift in Sources */,
B31481772C46547B00084A26 /* CameraViewManager.swift in Sources */,
B3EF9F522C3FC31E00832EE7 /* AVCaptureDevice+physicalDevices.swift in Sources */,
B3EF9F532C3FC31E00832EE7 /* AVCaptureDevice+neutralZoom.swift in Sources */,
B3EF9F542C3FC31E00832EE7 /* AVCaptureDevice.Format+dimensions.swift in Sources */,
B3EF9F552C3FC31E00832EE7 /* AVCaptureVideoDataOutput+pixelFormat.swift in Sources */,
B3EF9F562C3FC31E00832EE7 /* AVCaptureSession+synchronizeBuffer.swift in Sources */,
B3EF9F572C3FC31E00832EE7 /* AVCaptureDevice+isMultiCam.swift in Sources */,
B3EF9F582C3FC31E00832EE7 /* AVCaptureDevice+toDictionary.swift in Sources */,
B3EF9F592C3FC31E00832EE7 /* AVCaptureDevice.Format+toDictionary.swift in Sources */,
B3EF9F5A2C3FC31E00832EE7 /* CMVideoDimensions+toCGSize.swift in Sources */,
B3EF9F212C3FBDFC00832EE7 /* ReactStubs.swift in Sources */,
B3EF9F5C2C3FC33E00832EE7 /* RecordVideoOptions.swift in Sources */,
B3EF9F6B2C3FD35600832EE7 /* CameraView+RecordVideo.swift in Sources */,
B3EF9F222C3FBE8200832EE7 /* CameraConfiguration.swift in Sources */,
B3EF9F282C3FBF1900832EE7 /* JSUnionValue.swift in Sources */,
B3EF9F332C3FC00900832EE7 /* CameraSession+Configuration.swift in Sources */,
B3EF9F362C3FC05600832EE7 /* ResizeMode.swift in Sources */,
B3EF9F312C3FBFD500832EE7 /* AVAssetWriter.Status+descriptor.swift in Sources */,
B3EF9F292C3FBF2500832EE7 /* Torch.swift in Sources */,
B31481782C46558C00084A26 /* CameraView+TakePhoto.swift in Sources */,
B3EF9F2C2C3FBF4A00832EE7 /* EnumParserError.swift in Sources */,
B3EF9F272C3FBEF800832EE7 /* PixelFormat.swift in Sources */,
B3EF9F652C3FC43C00832EE7 /* CameraSession+Audio.swift in Sources */,
B3EF9F382C3FC0D900832EE7 /* PreviewView.swift in Sources */,
B3EF9F3A2C3FC2EB00832EE7 /* AutoFocusSystem.swift in Sources */,
B3EF9F112C3FBD8300832EE7 /* ViewController.swift in Sources */,
B3EF9F5D2C3FC34600832EE7 /* Video.swift in Sources */,
B3EF9F2B2C3FBF4100832EE7 /* AVMetadataObject.ObjectType+descriptor.swift in Sources */,
B3AF8E862C410FB700CC198C /* ReactStubs.m in Sources */,
B3EF9F0D2C3FBD8300832EE7 /* AppDelegate.swift in Sources */,
B3EF9F2D2C3FBF9600832EE7 /* CameraSessionDelegate.swift in Sources */,
B3EF9F262C3FBEEA00832EE7 /* CameraDeviceFormat.swift in Sources */,
B3EF9F242C3FBEBC00832EE7 /* CameraError.swift in Sources */,
B3EF9F2E2C3FBFA600832EE7 /* CameraSession+CodeScanner.swift in Sources */,
B3EF9F252C3FBED900832EE7 /* Orientation.swift in Sources */,
B3EF9F662C3FC44B00832EE7 /* CameraSession+Video.swift in Sources */,
B3EF9F672C3FC44B00832EE7 /* CameraSession+Photo.swift in Sources */,
B3EF9F682C3FC44B00832EE7 /* CameraSession+Focus.swift in Sources */,
B3EF9F6C2C3FD36800832EE7 /* Callback.swift in Sources */,
B3EF9F692C3FC44B00832EE7 /* PhotoCaptureDelegate.swift in Sources */,
B3EF9F302C3FBFBB00832EE7 /* RecordingSession.swift in Sources */,
B3EF9F322C3FBFF100832EE7 /* CameraQueues.swift in Sources */,
B3EF9F2F2C3FBFB200832EE7 /* CameraSession.swift in Sources */,
B3EF9F2A2C3FBF3400832EE7 /* CodeScannerOptions.swift in Sources */,
B3EF9F0F2C3FBD8300832EE7 /* SceneDelegate.swift in Sources */,
B3EF9F1E2C3FBDCF00832EE7 /* CameraView.swift in Sources */,
B3EF9F3C2C3FC30D00832EE7 /* AVCaptureDevice.Position+descriptor.swift in Sources */,
B3EF9F1F2C3FBDDC00832EE7 /* ReactLogger.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
B3EF9F122C3FBD8300832EE7 /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
B3EF9F132C3FBD8300832EE7 /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
B3EF9F172C3FBD8400832EE7 /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
B3EF9F182C3FBD8400832EE7 /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
58B511ED1A9E6C8500147676 /* Debug */ = {
isa = XCBuildConfiguration;
@@ -660,6 +909,94 @@
};
name = Release;
};
B3EF9F1B2C3FBD8400832EE7 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = HP3AMBWJGS;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = TestRecorder/Info.plist;
INFOPLIST_KEY_NSCameraUsageDescription = "Record form camera";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Record from microphone";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = camera.TestRecorder;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "TestRecorder/TestRecorder-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
B3EF9F1C2C3FBD8400832EE7 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = HP3AMBWJGS;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = TestRecorder/Info.plist;
INFOPLIST_KEY_NSCameraUsageDescription = "Record form camera";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Record from microphone";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = camera.TestRecorder;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "TestRecorder/TestRecorder-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -681,6 +1018,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
B3EF9F1D2C3FBD8400832EE7 /* Build configuration list for PBXNativeTarget "TestRecorder" */ = {
isa = XCConfigurationList;
buildConfigurations = (
B3EF9F1B2C3FBD8400832EE7 /* Debug */,
B3EF9F1C2C3FBD8400832EE7 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 58B511D31A9E6C8500147676 /* Project object */;

View File

@@ -18,37 +18,41 @@ export type CameraPermissionStatus = 'granted' | 'not-determined' | 'denied' | '
export type CameraPermissionRequestResult = 'granted' | 'denied'
interface OnCodeScannedEvent {
codes: Code[]
frame: CodeScannerFrame
codes: Code[]
frame: CodeScannerFrame
}
interface OnErrorEvent {
code: string
message: string
cause?: ErrorWithCause
code: string
message: string
cause?: ErrorWithCause
}
interface OnInitReadyEvent {
filepath: string
}
interface OnVideoChunkReadyEvent {
filepath: string
index: int
filepath: string
index: number
}
type NativeCameraViewProps = Omit<CameraProps, 'device' | 'onInitialized' | 'onError' | 'frameProcessor' | 'codeScanner'> & {
cameraId: string
enableFrameProcessor: boolean
codeScannerOptions?: Omit<CodeScanner, 'onCodeScanned'>
onInitialized?: (event: NativeSyntheticEvent<void>) => void
onError?: (event: NativeSyntheticEvent<OnErrorEvent>) => void
onCodeScanned?: (event: NativeSyntheticEvent<OnCodeScannedEvent>) => void
onStarted?: (event: NativeSyntheticEvent<void>) => void
onStopped?: (event: NativeSyntheticEvent<void>) => void
onVideoChunkReady?: (event: NativeSyntheticEvent<OnVideoChunkReadyEvent>) => void
onViewReady: () => void
cameraId: string
enableFrameProcessor: boolean
codeScannerOptions?: Omit<CodeScanner, 'onCodeScanned'>
onInitialized?: (event: NativeSyntheticEvent<void>) => void
onError?: (event: NativeSyntheticEvent<OnErrorEvent>) => void
onCodeScanned?: (event: NativeSyntheticEvent<OnCodeScannedEvent>) => void
onStarted?: (event: NativeSyntheticEvent<void>) => void
onStopped?: (event: NativeSyntheticEvent<void>) => void
onInitReady?: (event: NativeSyntheticEvent<OnInitReadyEvent>) => void
onVideoChunkReady?: (event: NativeSyntheticEvent<OnVideoChunkReadyEvent>) => void
onViewReady: () => void
}
type NativeRecordVideoOptions = Omit<RecordVideoOptions, 'onRecordingError' | 'onRecordingFinished' | 'videoBitRate'> & {
videoBitRateOverride?: number
videoBitRateMultiplier?: number
videoBitRateOverride?: number
videoBitRateMultiplier?: number
}
type RefType = React.Component<NativeCameraViewProps> & Readonly<NativeMethods>
interface CameraState {
isRecordingWithFlash: boolean
isRecordingWithFlash: boolean
}
//#endregion
@@ -82,427 +86,427 @@ interface CameraState {
* @component
*/
export class Camera extends React.PureComponent<CameraProps, CameraState> {
/** @internal */
static displayName = 'Camera'
/** @internal */
displayName = Camera.displayName
private lastFrameProcessor: FrameProcessor | undefined
private isNativeViewMounted = false
/** @internal */
static displayName = 'Camera'
/** @internal */
displayName = Camera.displayName
private lastFrameProcessor: FrameProcessor | undefined
private isNativeViewMounted = false
private readonly ref: React.RefObject<RefType>
private readonly ref: React.RefObject<RefType>
/** @internal */
constructor(props: CameraProps) {
super(props)
this.onViewReady = this.onViewReady.bind(this)
this.onInitialized = this.onInitialized.bind(this)
this.onStarted = this.onStarted.bind(this)
this.onStopped = this.onStopped.bind(this)
this.onError = this.onError.bind(this)
this.onCodeScanned = this.onCodeScanned.bind(this)
this.ref = React.createRef<RefType>()
this.lastFrameProcessor = undefined
this.state = {
isRecordingWithFlash: false,
}
}
/** @internal */
constructor(props: CameraProps) {
super(props)
this.onViewReady = this.onViewReady.bind(this)
this.onInitialized = this.onInitialized.bind(this)
this.onStarted = this.onStarted.bind(this)
this.onStopped = this.onStopped.bind(this)
this.onError = this.onError.bind(this)
this.onCodeScanned = this.onCodeScanned.bind(this)
this.ref = React.createRef<RefType>()
this.lastFrameProcessor = undefined
this.state = {
isRecordingWithFlash: false,
}
}
private get handle(): number {
const nodeHandle = findNodeHandle(this.ref.current)
if (nodeHandle == null || nodeHandle === -1) {
throw new CameraRuntimeError(
'system/view-not-found',
"Could not get the Camera's native view tag! Does the Camera View exist in the native view-tree?",
)
}
private get handle(): number {
const nodeHandle = findNodeHandle(this.ref.current)
if (nodeHandle == null || nodeHandle === -1) {
throw new CameraRuntimeError(
'system/view-not-found',
"Could not get the Camera's native view tag! Does the Camera View exist in the native view-tree?",
)
}
return nodeHandle
}
return nodeHandle
}
//#region View-specific functions (UIViewManager)
/**
* Take a single photo and write it's content to a temporary file.
*
* @throws {@linkcode CameraCaptureError} When any kind of error occured while capturing the photo. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
* @example
* ```ts
* const photo = await camera.current.takePhoto({
* qualityPrioritization: 'quality',
* flash: 'on',
* enableAutoRedEyeReduction: true
* })
* ```
*/
public async takePhoto(options?: TakePhotoOptions): Promise<PhotoFile> {
try {
return await CameraModule.takePhoto(this.handle, options ?? {})
} catch (e) {
throw tryParseNativeCameraError(e)
}
}
//#region View-specific functions (UIViewManager)
/**
* Take a single photo and write it's content to a temporary file.
*
* @throws {@linkcode CameraCaptureError} When any kind of error occured while capturing the photo. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
* @example
* ```ts
* const photo = await camera.current.takePhoto({
* qualityPrioritization: 'quality',
* flash: 'on',
* enableAutoRedEyeReduction: true
* })
* ```
*/
public async takePhoto(options?: TakePhotoOptions): Promise<PhotoFile> {
try {
return await CameraModule.takePhoto(this.handle, options ?? {})
} catch (e) {
throw tryParseNativeCameraError(e)
}
}
private getBitRateMultiplier(bitRate: RecordVideoOptions['videoBitRate']): number {
if (typeof bitRate === 'number' || bitRate == null) return 1
switch (bitRate) {
case 'extra-low':
return 0.6
case 'low':
return 0.8
case 'normal':
return 1
case 'high':
return 1.2
case 'extra-high':
return 1.4
}
}
private getBitRateMultiplier(bitRate: RecordVideoOptions['videoBitRate']): number {
if (typeof bitRate === 'number' || bitRate == null) return 1
switch (bitRate) {
case 'extra-low':
return 0.6
case 'low':
return 0.8
case 'normal':
return 1
case 'high':
return 1.2
case 'extra-high':
return 1.4
}
}
/**
* Start a new video recording.
*
* @throws {@linkcode CameraCaptureError} When any kind of error occured while starting the video recording. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
*
* @example
* ```ts
* camera.current.startRecording({
* onRecordingFinished: (video) => console.log(video),
* onRecordingError: (error) => console.error(error),
* })
* setTimeout(() => {
* camera.current.stopRecording()
* }, 5000)
* ```
*/
public startRecording(options: RecordVideoOptions): void {
const { onRecordingError, onRecordingFinished, videoBitRate, ...passThruOptions } = options
if (typeof onRecordingError !== 'function' || typeof onRecordingFinished !== 'function')
throw new CameraRuntimeError('parameter/invalid-parameter', 'The onRecordingError or onRecordingFinished functions were not set!')
/**
* Start a new video recording.
*
* @throws {@linkcode CameraCaptureError} When any kind of error occured while starting the video recording. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
*
* @example
* ```ts
* camera.current.startRecording({
* onRecordingFinished: (video) => console.log(video),
* onRecordingError: (error) => console.error(error),
* })
* setTimeout(() => {
* camera.current.stopRecording()
* }, 5000)
* ```
*/
public startRecording(options: RecordVideoOptions, filePath: string): void {
const { onRecordingError, onRecordingFinished, videoBitRate, ...passThruOptions } = options
if (typeof onRecordingError !== 'function' || typeof onRecordingFinished !== 'function')
throw new CameraRuntimeError('parameter/invalid-parameter', 'The onRecordingError or onRecordingFinished functions were not set!')
if (options.flash === 'on') {
// Enable torch for video recording
this.setState({
isRecordingWithFlash: true,
})
}
if (options.flash === 'on') {
// Enable torch for video recording
this.setState({
isRecordingWithFlash: true,
})
}
const nativeOptions: NativeRecordVideoOptions = passThruOptions
if (typeof videoBitRate === 'number') {
// If the user passed an absolute number as a bit-rate, we just use this as a full override.
nativeOptions.videoBitRateOverride = videoBitRate
} else if (typeof videoBitRate === 'string' && videoBitRate !== 'normal') {
// If the user passed 'low'/'normal'/'high', we need to apply this as a multiplier to the native bitrate instead of absolutely setting it
nativeOptions.videoBitRateMultiplier = this.getBitRateMultiplier(videoBitRate)
}
const nativeOptions: NativeRecordVideoOptions = passThruOptions
if (typeof videoBitRate === 'number') {
// If the user passed an absolute number as a bit-rate, we just use this as a full override.
nativeOptions.videoBitRateOverride = videoBitRate
} else if (typeof videoBitRate === 'string' && videoBitRate !== 'normal') {
// If the user passed 'low'/'normal'/'high', we need to apply this as a multiplier to the native bitrate instead of absolutely setting it
nativeOptions.videoBitRateMultiplier = this.getBitRateMultiplier(videoBitRate)
}
const onRecordCallback = (video?: VideoFile, error?: CameraCaptureError): void => {
if (this.state.isRecordingWithFlash) {
// disable torch again if it was enabled
this.setState({
isRecordingWithFlash: false,
})
}
const onRecordCallback = (video?: VideoFile, error?: CameraCaptureError): void => {
if (this.state.isRecordingWithFlash) {
// disable torch again if it was enabled
this.setState({
isRecordingWithFlash: false,
})
}
if (error != null) return onRecordingError(error)
if (video != null) return onRecordingFinished(video)
}
try {
// TODO: Use TurboModules to make this awaitable.
CameraModule.startRecording(this.handle, nativeOptions, onRecordCallback)
} catch (e) {
throw tryParseNativeCameraError(e)
}
}
if (error != null) return onRecordingError(error)
if (video != null) return onRecordingFinished(video)
}
try {
// TODO: Use TurboModules to make this awaitable.
CameraModule.startRecording(this.handle, nativeOptions, filePath, onRecordCallback)
} catch (e) {
throw tryParseNativeCameraError(e)
}
}
/**
* Pauses the current video recording.
*
* @throws {@linkcode CameraCaptureError} When any kind of error occured while pausing the video recording. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
*
* @example
* ```ts
* // Start
* await camera.current.startRecording()
* await timeout(1000)
* // Pause
* await camera.current.pauseRecording()
* await timeout(500)
* // Resume
* await camera.current.resumeRecording()
* await timeout(2000)
* // Stop
* const video = await camera.current.stopRecording()
* ```
*/
public async pauseRecording(): Promise<void> {
try {
return await CameraModule.pauseRecording(this.handle)
} catch (e) {
throw tryParseNativeCameraError(e)
}
}
/**
* Pauses the current video recording.
*
* @throws {@linkcode CameraCaptureError} When any kind of error occured while pausing the video recording. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
*
* @example
* ```ts
* // Start
* await camera.current.startRecording()
* await timeout(1000)
* // Pause
* await camera.current.pauseRecording()
* await timeout(500)
* // Resume
* await camera.current.resumeRecording()
* await timeout(2000)
* // Stop
* const video = await camera.current.stopRecording()
* ```
*/
public async pauseRecording(): Promise<void> {
try {
return await CameraModule.pauseRecording(this.handle)
} catch (e) {
throw tryParseNativeCameraError(e)
}
}
/**
* Resumes a currently paused video recording.
*
* @throws {@linkcode CameraCaptureError} When any kind of error occured while resuming the video recording. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
*
* @example
* ```ts
* // Start
* await camera.current.startRecording()
* await timeout(1000)
* // Pause
* await camera.current.pauseRecording()
* await timeout(500)
* // Resume
* await camera.current.resumeRecording()
* await timeout(2000)
* // Stop
* const video = await camera.current.stopRecording()
* ```
*/
public async resumeRecording(): Promise<void> {
try {
return await CameraModule.resumeRecording(this.handle)
} catch (e) {
throw tryParseNativeCameraError(e)
}
}
/**
* Resumes a currently paused video recording.
*
* @throws {@linkcode CameraCaptureError} When any kind of error occured while resuming the video recording. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
*
* @example
* ```ts
* // Start
* await camera.current.startRecording()
* await timeout(1000)
* // Pause
* await camera.current.pauseRecording()
* await timeout(500)
* // Resume
* await camera.current.resumeRecording()
* await timeout(2000)
* // Stop
* const video = await camera.current.stopRecording()
* ```
*/
public async resumeRecording(): Promise<void> {
try {
return await CameraModule.resumeRecording(this.handle)
} catch (e) {
throw tryParseNativeCameraError(e)
}
}
/**
* Stop the current video recording.
*
* @throws {@linkcode CameraCaptureError} When any kind of error occured while stopping the video recording. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
*
* @example
* ```ts
* await camera.current.startRecording()
* setTimeout(async () => {
* const video = await camera.current.stopRecording()
* }, 5000)
* ```
*/
public async stopRecording(): Promise<void> {
try {
return await CameraModule.stopRecording(this.handle)
} catch (e) {
throw tryParseNativeCameraError(e)
}
}
/**
* Stop the current video recording.
*
* @throws {@linkcode CameraCaptureError} When any kind of error occured while stopping the video recording. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
*
* @example
* ```ts
* await camera.current.startRecording()
* setTimeout(async () => {
* const video = await camera.current.stopRecording()
* }, 5000)
* ```
*/
public async stopRecording(): Promise<void> {
try {
return await CameraModule.stopRecording(this.handle)
} catch (e) {
throw tryParseNativeCameraError(e)
}
}
/**
* Focus the camera to a specific point in the coordinate system.
* @param {Point} point The point to focus to. This should be relative
* to the Camera view's coordinate system and is expressed in points.
* * `(0, 0)` means **top left**.
* * `(CameraView.width, CameraView.height)` means **bottom right**.
*
* Make sure the value doesn't exceed the CameraView's dimensions.
*
* @throws {@linkcode CameraRuntimeError} When any kind of error occured while focussing. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
* @example
* ```ts
* await camera.current.focus({
* x: tapEvent.x,
* y: tapEvent.y
* })
* ```
*/
public async focus(point: Point): Promise<void> {
try {
return await CameraModule.focus(this.handle, point)
} catch (e) {
throw tryParseNativeCameraError(e)
}
}
//#endregion
/**
* Focus the camera to a specific point in the coordinate system.
* @param {Point} point The point to focus to. This should be relative
* to the Camera view's coordinate system and is expressed in points.
* * `(0, 0)` means **top left**.
* * `(CameraView.width, CameraView.height)` means **bottom right**.
*
* Make sure the value doesn't exceed the CameraView's dimensions.
*
* @throws {@linkcode CameraRuntimeError} When any kind of error occured while focussing. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
* @example
* ```ts
* await camera.current.focus({
* x: tapEvent.x,
* y: tapEvent.y
* })
* ```
*/
public async focus(point: Point): Promise<void> {
try {
return await CameraModule.focus(this.handle, point)
} catch (e) {
throw tryParseNativeCameraError(e)
}
}
//#endregion
//#region Static Functions (NativeModule)
/**
* Get a list of all available camera devices on the current phone.
*
* If you use Hooks, use the `useCameraDevices(..)` hook instead.
*
* * For Camera Devices attached to the phone, it is safe to assume that this will never change.
* * For external Camera Devices (USB cameras, Mac continuity cameras, etc.) the available Camera Devices could change over time when the external Camera device gets plugged in or plugged out, so use {@link addCameraDevicesChangedListener | addCameraDevicesChangedListener(...)} to listen for such changes.
*
* @example
* ```ts
* const devices = Camera.getAvailableCameraDevices()
* const backCameras = devices.filter((d) => d.position === "back")
* const frontCameras = devices.filter((d) => d.position === "front")
* ```
*/
public static getAvailableCameraDevices(): CameraDevice[] {
return CameraDevices.getAvailableCameraDevices()
}
/**
* Adds a listener that gets called everytime the Camera Devices change, for example
* when an external Camera Device (USB or continuity Camera) gets plugged in or plugged out.
*
* If you use Hooks, use the `useCameraDevices()` hook instead.
*/
public static addCameraDevicesChangedListener(listener: (newDevices: CameraDevice[]) => void): EmitterSubscription {
return CameraDevices.addCameraDevicesChangedListener(listener)
}
/**
* Gets the current Camera Permission Status. Check this before mounting the Camera to ensure
* the user has permitted the app to use the camera.
*
* To actually prompt the user for camera permission, use {@linkcode Camera.requestCameraPermission | requestCameraPermission()}.
*/
public static getCameraPermissionStatus(): CameraPermissionStatus {
return CameraModule.getCameraPermissionStatus()
}
/**
* Gets the current Microphone-Recording Permission Status. Check this before mounting the Camera to ensure
* the user has permitted the app to use the microphone.
*
* To actually prompt the user for microphone permission, use {@linkcode Camera.requestMicrophonePermission | requestMicrophonePermission()}.
*/
public static getMicrophonePermissionStatus(): CameraPermissionStatus {
return CameraModule.getMicrophonePermissionStatus()
}
/**
* Shows a "request permission" alert to the user, and resolves with the new camera permission status.
*
* If the user has previously blocked the app from using the camera, the alert will not be shown
* and `"denied"` will be returned.
*
* @throws {@linkcode CameraRuntimeError} When any kind of error occured while requesting permission. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
*/
public static async requestCameraPermission(): Promise<CameraPermissionRequestResult> {
try {
return await CameraModule.requestCameraPermission()
} catch (e) {
throw tryParseNativeCameraError(e)
}
}
/**
* Shows a "request permission" alert to the user, and resolves with the new microphone permission status.
*
* If the user has previously blocked the app from using the microphone, the alert will not be shown
* and `"denied"` will be returned.
*
* @throws {@linkcode CameraRuntimeError} When any kind of error occured while requesting permission. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
*/
public static async requestMicrophonePermission(): Promise<CameraPermissionRequestResult> {
try {
return await CameraModule.requestMicrophonePermission()
} catch (e) {
throw tryParseNativeCameraError(e)
}
}
//#endregion
//#region Static Functions (NativeModule)
/**
* Get a list of all available camera devices on the current phone.
*
* If you use Hooks, use the `useCameraDevices(..)` hook instead.
*
* * For Camera Devices attached to the phone, it is safe to assume that this will never change.
* * For external Camera Devices (USB cameras, Mac continuity cameras, etc.) the available Camera Devices could change over time when the external Camera device gets plugged in or plugged out, so use {@link addCameraDevicesChangedListener | addCameraDevicesChangedListener(...)} to listen for such changes.
*
* @example
* ```ts
* const devices = Camera.getAvailableCameraDevices()
* const backCameras = devices.filter((d) => d.position === "back")
* const frontCameras = devices.filter((d) => d.position === "front")
* ```
*/
public static getAvailableCameraDevices(): CameraDevice[] {
return CameraDevices.getAvailableCameraDevices()
}
/**
* Adds a listener that gets called everytime the Camera Devices change, for example
* when an external Camera Device (USB or continuity Camera) gets plugged in or plugged out.
*
* If you use Hooks, use the `useCameraDevices()` hook instead.
*/
public static addCameraDevicesChangedListener(listener: (newDevices: CameraDevice[]) => void): EmitterSubscription {
return CameraDevices.addCameraDevicesChangedListener(listener)
}
/**
* Gets the current Camera Permission Status. Check this before mounting the Camera to ensure
* the user has permitted the app to use the camera.
*
* To actually prompt the user for camera permission, use {@linkcode Camera.requestCameraPermission | requestCameraPermission()}.
*/
public static getCameraPermissionStatus(): CameraPermissionStatus {
return CameraModule.getCameraPermissionStatus()
}
/**
* Gets the current Microphone-Recording Permission Status. Check this before mounting the Camera to ensure
* the user has permitted the app to use the microphone.
*
* To actually prompt the user for microphone permission, use {@linkcode Camera.requestMicrophonePermission | requestMicrophonePermission()}.
*/
public static getMicrophonePermissionStatus(): CameraPermissionStatus {
return CameraModule.getMicrophonePermissionStatus()
}
/**
* Shows a "request permission" alert to the user, and resolves with the new camera permission status.
*
* If the user has previously blocked the app from using the camera, the alert will not be shown
* and `"denied"` will be returned.
*
* @throws {@linkcode CameraRuntimeError} When any kind of error occured while requesting permission. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
*/
public static async requestCameraPermission(): Promise<CameraPermissionRequestResult> {
try {
return await CameraModule.requestCameraPermission()
} catch (e) {
throw tryParseNativeCameraError(e)
}
}
/**
* Shows a "request permission" alert to the user, and resolves with the new microphone permission status.
*
* If the user has previously blocked the app from using the microphone, the alert will not be shown
* and `"denied"` will be returned.
*
* @throws {@linkcode CameraRuntimeError} When any kind of error occured while requesting permission. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
*/
public static async requestMicrophonePermission(): Promise<CameraPermissionRequestResult> {
try {
return await CameraModule.requestMicrophonePermission()
} catch (e) {
throw tryParseNativeCameraError(e)
}
}
//#endregion
//#region Events (Wrapped to maintain reference equality)
private onError(event: NativeSyntheticEvent<OnErrorEvent>): void {
const error = event.nativeEvent
const cause = isErrorWithCause(error.cause) ? error.cause : undefined
// @ts-expect-error We're casting from unknown bridge types to TS unions, I expect it to hopefully work
const cameraError = new CameraRuntimeError(error.code, error.message, cause)
//#region Events (Wrapped to maintain reference equality)
private onError(event: NativeSyntheticEvent<OnErrorEvent>): void {
const error = event.nativeEvent
const cause = isErrorWithCause(error.cause) ? error.cause : undefined
// @ts-expect-error We're casting from unknown bridge types to TS unions, I expect it to hopefully work
const cameraError = new CameraRuntimeError(error.code, error.message, cause)
if (this.props.onError != null) {
this.props.onError(cameraError)
} else {
// User didn't pass an `onError` handler, so just log it to console
console.error(`Camera.onError(${cameraError.code}): ${cameraError.message}`, cameraError)
}
}
if (this.props.onError != null) {
this.props.onError(cameraError)
} else {
// User didn't pass an `onError` handler, so just log it to console
console.error(`Camera.onError(${cameraError.code}): ${cameraError.message}`, cameraError)
}
}
private onInitialized(): void {
this.props.onInitialized?.()
}
private onInitialized(): void {
this.props.onInitialized?.()
}
private onStarted(): void {
this.props.onStarted?.()
}
private onStarted(): void {
this.props.onStarted?.()
}
private onStopped(): void {
this.props.onStopped?.()
}
//#endregion
private onStopped(): void {
this.props.onStopped?.()
}
//#endregion
private onCodeScanned(event: NativeSyntheticEvent<OnCodeScannedEvent>): void {
const codeScanner = this.props.codeScanner
if (codeScanner == null) return
private onCodeScanned(event: NativeSyntheticEvent<OnCodeScannedEvent>): void {
const codeScanner = this.props.codeScanner
if (codeScanner == null) return
codeScanner.onCodeScanned(event.nativeEvent.codes, event.nativeEvent.frame)
}
codeScanner.onCodeScanned(event.nativeEvent.codes, event.nativeEvent.frame)
}
//#region Lifecycle
private setFrameProcessor(frameProcessor: FrameProcessor): void {
VisionCameraProxy.setFrameProcessor(this.handle, frameProcessor)
}
//#region Lifecycle
private setFrameProcessor(frameProcessor: FrameProcessor): void {
VisionCameraProxy.setFrameProcessor(this.handle, frameProcessor)
}
private unsetFrameProcessor(): void {
VisionCameraProxy.removeFrameProcessor(this.handle)
}
private unsetFrameProcessor(): void {
VisionCameraProxy.removeFrameProcessor(this.handle)
}
private onViewReady(): void {
this.isNativeViewMounted = true
if (this.props.frameProcessor != null) {
// user passed a `frameProcessor` but we didn't set it yet because the native view was not mounted yet. set it now.
this.setFrameProcessor(this.props.frameProcessor)
this.lastFrameProcessor = this.props.frameProcessor
}
}
private onViewReady(): void {
this.isNativeViewMounted = true
if (this.props.frameProcessor != null) {
// user passed a `frameProcessor` but we didn't set it yet because the native view was not mounted yet. set it now.
this.setFrameProcessor(this.props.frameProcessor)
this.lastFrameProcessor = this.props.frameProcessor
}
}
/** @internal */
componentDidUpdate(): void {
if (!this.isNativeViewMounted) return
const frameProcessor = this.props.frameProcessor
if (frameProcessor !== this.lastFrameProcessor) {
// frameProcessor argument identity changed. Update native to reflect the change.
if (frameProcessor != null) this.setFrameProcessor(frameProcessor)
else this.unsetFrameProcessor()
/** @internal */
componentDidUpdate(): void {
if (!this.isNativeViewMounted) return
const frameProcessor = this.props.frameProcessor
if (frameProcessor !== this.lastFrameProcessor) {
// frameProcessor argument identity changed. Update native to reflect the change.
if (frameProcessor != null) this.setFrameProcessor(frameProcessor)
else this.unsetFrameProcessor()
this.lastFrameProcessor = frameProcessor
}
}
//#endregion
this.lastFrameProcessor = frameProcessor
}
}
//#endregion
/** @internal */
public render(): React.ReactNode {
// We remove the big `device` object from the props because we only need to pass `cameraId` to native.
const { device, frameProcessor, codeScanner, ...props } = this.props
/** @internal */
public render(): React.ReactNode {
// We remove the big `device` object from the props because we only need to pass `cameraId` to native.
const { device, frameProcessor, codeScanner, ...props } = this.props
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (device == null) {
throw new Error(
'Camera: `device` is null! Select a valid Camera device. See: https://mrousavy.com/react-native-vision-camera/docs/guides/devices',
)
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (device == null) {
throw new Error(
'Camera: `device` is null! Select a valid Camera device. See: https://mrousavy.com/react-native-vision-camera/docs/guides/devices',
)
}
const shouldEnableBufferCompression = props.video === true && frameProcessor == null
const pixelFormat = props.pixelFormat ?? (frameProcessor != null ? 'yuv' : 'native')
const torch = this.state.isRecordingWithFlash ? 'on' : props.torch
const shouldEnableBufferCompression = props.video === true && frameProcessor == null
const pixelFormat = props.pixelFormat ?? (frameProcessor != null ? 'yuv' : 'native')
const torch = this.state.isRecordingWithFlash ? 'on' : props.torch
return (
<NativeCameraView
{...props}
cameraId={device.id}
ref={this.ref}
torch={torch}
onViewReady={this.onViewReady}
onInitialized={this.onInitialized}
onCodeScanned={this.onCodeScanned}
onStarted={this.onStarted}
onStopped={this.onStopped}
onError={this.onError}
codeScannerOptions={codeScanner}
enableFrameProcessor={frameProcessor != null}
enableBufferCompression={props.enableBufferCompression ?? shouldEnableBufferCompression}
pixelFormat={pixelFormat}
/>
)
}
return (
<NativeCameraView
{...props}
cameraId={device.id}
ref={this.ref}
torch={torch}
onViewReady={this.onViewReady}
onInitialized={this.onInitialized}
onCodeScanned={this.onCodeScanned}
onStarted={this.onStarted}
onStopped={this.onStopped}
onError={this.onError}
codeScannerOptions={codeScanner}
enableFrameProcessor={frameProcessor != null}
enableBufferCompression={props.enableBufferCompression ?? shouldEnableBufferCompression}
pixelFormat={pixelFormat}
/>
)
}
}
//#endregion
// requireNativeComponent automatically resolves 'CameraView' to 'CameraViewManager'
const NativeCameraView = requireNativeComponent<NativeCameraViewProps>(
'CameraView',
// @ts-expect-error because the type declarations are kinda wrong, no?
Camera,
'CameraView',
// @ts-expect-error because the type declarations are kinda wrong, no?
Camera,
)