Compare commits
7 Commits
0a43d7a160
...
694d9cfa8c
Author | SHA1 | Date | |
---|---|---|---|
|
694d9cfa8c | ||
|
91767e71c8 | ||
|
9f2c7906e5 | ||
|
621bfe333c | ||
|
20f8fa2937 | ||
|
b03f9ea423 | ||
|
98d90a6442 |
@ -11,7 +11,7 @@ import AVFoundation
|
|||||||
// MARK: - CameraView + AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate
|
// MARK: - CameraView + AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate
|
||||||
|
|
||||||
extension 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
|
// Type-safety
|
||||||
let callback = Callback(jsCallback)
|
let callback = Callback(jsCallback)
|
||||||
|
|
||||||
@ -21,6 +21,7 @@ extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAud
|
|||||||
// Start Recording with success and error callbacks
|
// Start Recording with success and error callbacks
|
||||||
cameraSession.startRecording(
|
cameraSession.startRecording(
|
||||||
options: options,
|
options: options,
|
||||||
|
filePath: filePath,
|
||||||
onVideoRecorded: { video in
|
onVideoRecorded: { video in
|
||||||
callback.resolve(video.toJSValue())
|
callback.resolve(video.toJSValue())
|
||||||
},
|
},
|
||||||
|
@ -342,6 +342,7 @@ public final class CameraView: UIView, CameraSessionDelegate {
|
|||||||
ReactLogger.log(level: .info, message: "Chunk ready: \(chunk)")
|
ReactLogger.log(level: .info, message: "Chunk ready: \(chunk)")
|
||||||
|
|
||||||
guard let onVideoChunkReady, let onInitReady else {
|
guard let onVideoChunkReady, let onInitReady else {
|
||||||
|
ReactLogger.log(level: .warning, message: "Either onInitReady or onVideoChunkReady are not valid!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +64,8 @@ RCT_EXPORT_VIEW_PROPERTY(onCodeScanned, RCTDirectEventBlock);
|
|||||||
// Camera View Functions
|
// Camera View Functions
|
||||||
RCT_EXTERN_METHOD(startRecording
|
RCT_EXTERN_METHOD(startRecording
|
||||||
: (nonnull NSNumber*)node options
|
: (nonnull NSNumber*)node options
|
||||||
: (NSDictionary*)options onRecordCallback
|
: (NSDictionary*)options filePath
|
||||||
|
: (NSString*)filePath onRecordCallback
|
||||||
: (RCTResponseSenderBlock)onRecordCallback);
|
: (RCTResponseSenderBlock)onRecordCallback);
|
||||||
RCT_EXTERN_METHOD(pauseRecording
|
RCT_EXTERN_METHOD(pauseRecording
|
||||||
: (nonnull NSNumber*)node resolve
|
: (nonnull NSNumber*)node resolve
|
||||||
|
@ -43,9 +43,9 @@ final class CameraViewManager: RCTViewManager {
|
|||||||
// This means that any errors that occur in this function have to be delegated through
|
// 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.
|
// the callback, but I'd prefer for them to throw for the original function instead.
|
||||||
@objc
|
@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)
|
let component = getCameraView(withTag: node)
|
||||||
component.startRecording(options: options, callback: onRecordCallback)
|
component.startRecording(options: options, filePath: filePath as String, callback: onRecordCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
|
@ -176,6 +176,7 @@ enum CaptureError {
|
|||||||
case noRecordingInProgress
|
case noRecordingInProgress
|
||||||
case fileError
|
case fileError
|
||||||
case createTempFileError(message: String? = nil)
|
case createTempFileError(message: String? = nil)
|
||||||
|
case createRecordingDirectoryError(message: String? = nil)
|
||||||
case createRecorderError(message: String? = nil)
|
case createRecorderError(message: String? = nil)
|
||||||
case videoNotEnabled
|
case videoNotEnabled
|
||||||
case photoNotEnabled
|
case photoNotEnabled
|
||||||
@ -193,6 +194,8 @@ enum CaptureError {
|
|||||||
return "file-io-error"
|
return "file-io-error"
|
||||||
case .createTempFileError:
|
case .createTempFileError:
|
||||||
return "create-temp-file-error"
|
return "create-temp-file-error"
|
||||||
|
case .createRecordingDirectoryError:
|
||||||
|
return "create-recording-directory-error"
|
||||||
case .createRecorderError:
|
case .createRecorderError:
|
||||||
return "create-recorder-error"
|
return "create-recorder-error"
|
||||||
case .videoNotEnabled:
|
case .videoNotEnabled:
|
||||||
@ -218,6 +221,8 @@ enum CaptureError {
|
|||||||
return "An unexpected File IO error occured!"
|
return "An unexpected File IO error occured!"
|
||||||
case let .createTempFileError(message: message):
|
case let .createTempFileError(message: message):
|
||||||
return "Failed to create a temporary file! \(message ?? "(no additional 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):
|
case let .createRecorderError(message: message):
|
||||||
return "Failed to create the AVAssetWriter (Recorder)! \(message ?? "(no additional message)")"
|
return "Failed to create the AVAssetWriter (Recorder)! \(message ?? "(no additional message)")"
|
||||||
case .videoNotEnabled:
|
case .videoNotEnabled:
|
||||||
|
@ -15,6 +15,7 @@ extension CameraSession {
|
|||||||
Starts a video + audio recording with a custom Asset Writer.
|
Starts a video + audio recording with a custom Asset Writer.
|
||||||
*/
|
*/
|
||||||
func startRecording(options: RecordVideoOptions,
|
func startRecording(options: RecordVideoOptions,
|
||||||
|
filePath: String,
|
||||||
onVideoRecorded: @escaping (_ video: Video) -> Void,
|
onVideoRecorded: @escaping (_ video: Video) -> Void,
|
||||||
onError: @escaping (_ error: CameraError) -> Void) {
|
onError: @escaping (_ error: CameraError) -> Void) {
|
||||||
// Run on Camera Queue
|
// Run on Camera Queue
|
||||||
@ -70,7 +71,7 @@ extension CameraSession {
|
|||||||
} else {
|
} else {
|
||||||
if status == .completed {
|
if status == .completed {
|
||||||
// Recording was successfully saved
|
// Recording was successfully saved
|
||||||
let video = Video(path: recordingSession.url.absoluteString,
|
let video = Video(path: recordingSession.outputDiretory.absoluteString,
|
||||||
duration: recordingSession.duration,
|
duration: recordingSession.duration,
|
||||||
size: recordingSession.size ?? CGSize.zero)
|
size: recordingSession.size ?? CGSize.zero)
|
||||||
onVideoRecorded(video)
|
onVideoRecorded(video)
|
||||||
@ -81,21 +82,20 @@ extension CameraSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create temporary file
|
if !FileManager.default.fileExists(atPath: filePath) {
|
||||||
let errorPointer = ErrorPointer(nilLiteral: ())
|
do {
|
||||||
let fileExtension = options.fileType.descriptor ?? "mov"
|
try FileManager.default.createDirectory(atPath: filePath, withIntermediateDirectories: true)
|
||||||
guard let tempFilePath = RCTTempFilePath(fileExtension, errorPointer) else {
|
} catch {
|
||||||
let message = errorPointer?.pointee?.description
|
onError(.capture(.createRecordingDirectoryError(message: error.localizedDescription)))
|
||||||
onError(.capture(.createTempFileError(message: message)))
|
return
|
||||||
return
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactLogger.log(level: .info, message: "Will record to temporary file: \(tempFilePath)")
|
ReactLogger.log(level: .info, message: "Will record to temporary file: \(filePath)")
|
||||||
let tempURL = URL(string: "file://\(tempFilePath)")!
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
// Create RecordingSession for the temp file
|
// Create RecordingSession for the temp file
|
||||||
let recordingSession = try RecordingSession(url: tempURL,
|
let recordingSession = try RecordingSession(outputDiretory: filePath,
|
||||||
fileType: options.fileType,
|
fileType: options.fileType,
|
||||||
onChunkReady: onChunkReady,
|
onChunkReady: onChunkReady,
|
||||||
completion: onFinish)
|
completion: onFinish)
|
||||||
|
@ -25,10 +25,10 @@ class ChunkedRecorder: NSObject {
|
|||||||
let outputURL: URL
|
let outputURL: URL
|
||||||
let onChunkReady: ((Chunk) -> Void)
|
let onChunkReady: ((Chunk) -> Void)
|
||||||
|
|
||||||
private var index: UInt64 = 0
|
private var chunkIndex: UInt64 = 0
|
||||||
|
|
||||||
init(url: URL, onChunkReady: @escaping ((Chunk) -> Void)) throws {
|
init(outputURL: URL, onChunkReady: @escaping ((Chunk) -> Void)) throws {
|
||||||
outputURL = url
|
self.outputURL = outputURL
|
||||||
self.onChunkReady = onChunkReady
|
self.onChunkReady = onChunkReady
|
||||||
guard FileManager.default.fileExists(atPath: outputURL.path) else {
|
guard FileManager.default.fileExists(atPath: outputURL.path) else {
|
||||||
throw CameraError.unknown(message: "output directory does not exist at: \(outputURL.path)", cause: nil)
|
throw CameraError.unknown(message: "output directory does not exist at: \(outputURL.path)", cause: nil)
|
||||||
@ -61,13 +61,11 @@ extension ChunkedRecorder: AVAssetWriterDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func saveSegment(_ data: Data) {
|
private func saveSegment(_ data: Data) {
|
||||||
defer {
|
let name = "\(chunkIndex).mp4"
|
||||||
index += 1
|
|
||||||
}
|
|
||||||
let name = String(format: "%06d.mp4", index)
|
|
||||||
let url = outputURL.appendingPathComponent(name)
|
let url = outputURL.appendingPathComponent(name)
|
||||||
save(data: data, url: url)
|
save(data: data, url: url)
|
||||||
onChunkReady(url: url, type: .data(index: index))
|
onChunkReady(url: url, type: .data(index: chunkIndex))
|
||||||
|
chunkIndex += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
private func save(data: Data, url: URL) {
|
private func save(data: Data, url: URL) {
|
||||||
|
@ -49,8 +49,7 @@ class RecordingSession {
|
|||||||
/**
|
/**
|
||||||
Gets the file URL of the recorded video.
|
Gets the file URL of the recorded video.
|
||||||
*/
|
*/
|
||||||
var url: URL {
|
var outputDiretory: URL {
|
||||||
// FIXME:
|
|
||||||
return recorder.outputURL
|
return recorder.outputURL
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,14 +71,15 @@ class RecordingSession {
|
|||||||
return (lastWrittenTimestamp - startTimestamp).seconds
|
return (lastWrittenTimestamp - startTimestamp).seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
init(url: URL,
|
init(outputDiretory: String,
|
||||||
fileType: AVFileType,
|
fileType: AVFileType,
|
||||||
onChunkReady: @escaping ((ChunkedRecorder.Chunk) -> Void),
|
onChunkReady: @escaping ((ChunkedRecorder.Chunk) -> Void),
|
||||||
completion: @escaping (RecordingSession, AVAssetWriter.Status, Error?) -> Void) throws {
|
completion: @escaping (RecordingSession, AVAssetWriter.Status, Error?) -> Void) throws {
|
||||||
completionHandler = completion
|
completionHandler = completion
|
||||||
|
|
||||||
do {
|
do {
|
||||||
recorder = try ChunkedRecorder(url: url.deletingLastPathComponent(), onChunkReady: onChunkReady)
|
let outputURL = URL(fileURLWithPath: outputDiretory)
|
||||||
|
recorder = try ChunkedRecorder(outputURL: outputURL, onChunkReady: onChunkReady)
|
||||||
assetWriter = AVAssetWriter(contentType: UTType(fileType.rawValue)!)
|
assetWriter = AVAssetWriter(contentType: UTType(fileType.rawValue)!)
|
||||||
assetWriter.shouldOptimizeForNetworkUse = false
|
assetWriter.shouldOptimizeForNetworkUse = false
|
||||||
assetWriter.outputFileTypeProfile = .mpeg4AppleHLS
|
assetWriter.outputFileTypeProfile = .mpeg4AppleHLS
|
||||||
|
@ -10,18 +10,18 @@ import UIKit
|
|||||||
|
|
||||||
|
|
||||||
enum RCTLogLevel: String {
|
enum RCTLogLevel: String {
|
||||||
case trace
|
case trace
|
||||||
case info
|
case info
|
||||||
case warning
|
case warning
|
||||||
case error
|
case error
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RCTLogSource {
|
enum RCTLogSource {
|
||||||
case native
|
case native
|
||||||
}
|
}
|
||||||
|
|
||||||
func RCTDefaultLogFunction(_ level: RCTLogLevel, _ source: RCTLogSource, _ file: String, _ line: NSNumber, _ message: String) {
|
func RCTDefaultLogFunction(_ level: RCTLogLevel, _ source: RCTLogSource, _ file: String, _ line: NSNumber, _ message: String) {
|
||||||
print(level.rawValue, "-", message)
|
print(level.rawValue, "-", message)
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias RCTDirectEventBlock = (Any?) -> Void
|
typealias RCTDirectEventBlock = (Any?) -> Void
|
||||||
@ -30,53 +30,73 @@ typealias RCTPromiseRejectBlock = (String, String, NSError?) -> Void
|
|||||||
typealias RCTResponseSenderBlock = (Any) -> Void
|
typealias RCTResponseSenderBlock = (Any) -> Void
|
||||||
|
|
||||||
func NSNull() -> [String: String] {
|
func NSNull() -> [String: String] {
|
||||||
return [:]
|
return [:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func makeReactError(_ cameraError: CameraError, cause: NSError?) -> [String: Any] {
|
func makeReactError(_ cameraError: CameraError, cause: NSError?) -> [String: Any] {
|
||||||
var causeDictionary: [String: Any]?
|
var causeDictionary: [String: Any]?
|
||||||
if let cause = cause {
|
if let cause = cause {
|
||||||
causeDictionary = [
|
causeDictionary = [
|
||||||
"cause": "\(cause.domain): \(cause.code) \(cause.description)",
|
"cause": "\(cause.domain): \(cause.code) \(cause.description)",
|
||||||
"userInfo": cause.userInfo
|
"userInfo": cause.userInfo
|
||||||
]
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
"error": "\(cameraError.code): \(cameraError.message)",
|
|
||||||
"extra": [
|
|
||||||
"code": cameraError.code,
|
|
||||||
"message": cameraError.message,
|
|
||||||
"cause": causeDictionary ?? NSNull(),
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
"error": "\(cameraError.code): \(cameraError.message)",
|
||||||
|
"extra": [
|
||||||
|
"code": cameraError.code,
|
||||||
|
"message": cameraError.message,
|
||||||
|
"cause": causeDictionary ?? NSNull(),
|
||||||
|
]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeReactError(_ cameraError: CameraError) -> [String: Any] {
|
func makeReactError(_ cameraError: CameraError) -> [String: Any] {
|
||||||
return makeReactError(cameraError, cause: nil)
|
return makeReactError(cameraError, cause: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RCTFPSGraph: UIView {
|
class RCTFPSGraph: UIView {
|
||||||
convenience init(frame: CGRect, color: UIColor) {
|
convenience init(frame: CGRect, color: UIColor) {
|
||||||
self.init(frame: frame)
|
self.init(frame: frame)
|
||||||
}
|
}
|
||||||
|
|
||||||
func onTick(_ tick: CFTimeInterval) {
|
func onTick(_ tick: CFTimeInterval) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func RCTTempFilePath(_ ext: String, _ error: ErrorPointer) -> String? {
|
func RCTTempFilePath(_ ext: String, _ error: ErrorPointer) -> String? {
|
||||||
let directory = NSTemporaryDirectory().appending("ReactNative")
|
let directory = NSTemporaryDirectory().appending("ReactNative")
|
||||||
let fm = FileManager.default
|
let fm = FileManager.default
|
||||||
if fm.fileExists(atPath: directory) {
|
if fm.fileExists(atPath: directory) {
|
||||||
try! fm.removeItem(atPath: directory)
|
try! fm.removeItem(atPath: directory)
|
||||||
}
|
}
|
||||||
if !fm.fileExists(atPath: directory) {
|
if !fm.fileExists(atPath: directory) {
|
||||||
try! fm.createDirectory(atPath: directory, withIntermediateDirectories: true)
|
try! fm.createDirectory(atPath: directory, withIntermediateDirectories: true)
|
||||||
}
|
}
|
||||||
return directory
|
return directory
|
||||||
.appending("/").appending(UUID().uuidString)
|
.appending("/").appending(UUID().uuidString)
|
||||||
.appending(".").appending(ext)
|
.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()
|
||||||
}
|
}
|
||||||
|
@ -11,95 +11,107 @@ import AVFoundation
|
|||||||
|
|
||||||
class ViewController: UIViewController {
|
class ViewController: UIViewController {
|
||||||
|
|
||||||
@IBOutlet weak var recordButton: UIButton!
|
@IBOutlet weak var recordButton: UIButton!
|
||||||
|
|
||||||
let cameraView = CameraView()
|
let cameraView = CameraView()
|
||||||
|
let filePath: String = {
|
||||||
|
NSTemporaryDirectory() + "TestRecorder"
|
||||||
|
}()
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
cameraView.translatesAutoresizingMaskIntoConstraints = false;
|
try? FileManager.default.removeItem(atPath: filePath)
|
||||||
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.translatesAutoresizingMaskIntoConstraints = false;
|
||||||
cameraView.onInitialized = { _ in
|
view.insertSubview(cameraView, at: 0)
|
||||||
DispatchQueue.main.async {
|
NSLayoutConstraint.activate([
|
||||||
self.recordButton.isHidden = false
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
Task { @MainActor in
|
button.setTitle("Stop", for: .normal)
|
||||||
await requestAuthorizations()
|
button.configuration = .bordered()
|
||||||
|
|
||||||
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",
|
|
||||||
]) { callback in
|
|
||||||
print("callback", callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
button.setTitle("Stop", for: .normal)
|
|
||||||
button.configuration = .bordered()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ import AVFoundation
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct RecordVideoOptions {
|
struct RecordVideoOptions {
|
||||||
var fileType: AVFileType = .mov
|
var fileType: AVFileType = .mp4
|
||||||
var flash: Torch = .off
|
var flash: Torch = .off
|
||||||
var codec: AVVideoCodecType?
|
var codec: AVVideoCodecType?
|
||||||
/**
|
/**
|
||||||
|
@ -7,6 +7,9 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* 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 */; };
|
B3AF8E862C410FB700CC198C /* ReactStubs.m in Sources */ = {isa = PBXBuildFile; fileRef = B3AF8E852C410FB700CC198C /* ReactStubs.m */; };
|
||||||
B3AF8E882C41159300CC198C /* ChunkedRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AF8E872C41159300CC198C /* ChunkedRecorder.swift */; };
|
B3AF8E882C41159300CC198C /* ChunkedRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AF8E872C41159300CC198C /* ChunkedRecorder.swift */; };
|
||||||
B3AF8E892C41159300CC198C /* ChunkedRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AF8E872C41159300CC198C /* ChunkedRecorder.swift */; };
|
B3AF8E892C41159300CC198C /* ChunkedRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AF8E872C41159300CC198C /* ChunkedRecorder.swift */; };
|
||||||
@ -691,6 +694,8 @@
|
|||||||
B3EF9F502C3FC31E00832EE7 /* AVCaptureVideoDataOutput+recommendedVideoSettings.swift in Sources */,
|
B3EF9F502C3FC31E00832EE7 /* AVCaptureVideoDataOutput+recommendedVideoSettings.swift in Sources */,
|
||||||
B3EF9F512C3FC31E00832EE7 /* AVCaptureDevice+minFocusDistance.swift in Sources */,
|
B3EF9F512C3FC31E00832EE7 /* AVCaptureDevice+minFocusDistance.swift in Sources */,
|
||||||
B3EF9F5B2C3FC33000832EE7 /* AVCaptureDevice.DeviceType+physicalDeviceDescriptor.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 */,
|
B3EF9F522C3FC31E00832EE7 /* AVCaptureDevice+physicalDevices.swift in Sources */,
|
||||||
B3EF9F532C3FC31E00832EE7 /* AVCaptureDevice+neutralZoom.swift in Sources */,
|
B3EF9F532C3FC31E00832EE7 /* AVCaptureDevice+neutralZoom.swift in Sources */,
|
||||||
B3EF9F542C3FC31E00832EE7 /* AVCaptureDevice.Format+dimensions.swift in Sources */,
|
B3EF9F542C3FC31E00832EE7 /* AVCaptureDevice.Format+dimensions.swift in Sources */,
|
||||||
@ -709,6 +714,7 @@
|
|||||||
B3EF9F362C3FC05600832EE7 /* ResizeMode.swift in Sources */,
|
B3EF9F362C3FC05600832EE7 /* ResizeMode.swift in Sources */,
|
||||||
B3EF9F312C3FBFD500832EE7 /* AVAssetWriter.Status+descriptor.swift in Sources */,
|
B3EF9F312C3FBFD500832EE7 /* AVAssetWriter.Status+descriptor.swift in Sources */,
|
||||||
B3EF9F292C3FBF2500832EE7 /* Torch.swift in Sources */,
|
B3EF9F292C3FBF2500832EE7 /* Torch.swift in Sources */,
|
||||||
|
B31481782C46558C00084A26 /* CameraView+TakePhoto.swift in Sources */,
|
||||||
B3EF9F2C2C3FBF4A00832EE7 /* EnumParserError.swift in Sources */,
|
B3EF9F2C2C3FBF4A00832EE7 /* EnumParserError.swift in Sources */,
|
||||||
B3EF9F272C3FBEF800832EE7 /* PixelFormat.swift in Sources */,
|
B3EF9F272C3FBEF800832EE7 /* PixelFormat.swift in Sources */,
|
||||||
B3EF9F652C3FC43C00832EE7 /* CameraSession+Audio.swift in Sources */,
|
B3EF9F652C3FC43C00832EE7 /* CameraSession+Audio.swift in Sources */,
|
||||||
|
@ -26,6 +26,9 @@ interface OnErrorEvent {
|
|||||||
message: string
|
message: string
|
||||||
cause?: ErrorWithCause
|
cause?: ErrorWithCause
|
||||||
}
|
}
|
||||||
|
interface OnInitReadyEvent {
|
||||||
|
filepath: string
|
||||||
|
}
|
||||||
interface OnVideoChunkReadyEvent {
|
interface OnVideoChunkReadyEvent {
|
||||||
filepath: string
|
filepath: string
|
||||||
index: number
|
index: number
|
||||||
@ -39,6 +42,7 @@ type NativeCameraViewProps = Omit<CameraProps, 'device' | 'onInitialized' | 'onE
|
|||||||
onCodeScanned?: (event: NativeSyntheticEvent<OnCodeScannedEvent>) => void
|
onCodeScanned?: (event: NativeSyntheticEvent<OnCodeScannedEvent>) => void
|
||||||
onStarted?: (event: NativeSyntheticEvent<void>) => void
|
onStarted?: (event: NativeSyntheticEvent<void>) => void
|
||||||
onStopped?: (event: NativeSyntheticEvent<void>) => void
|
onStopped?: (event: NativeSyntheticEvent<void>) => void
|
||||||
|
onInitReady?: (event: NativeSyntheticEvent<OnInitReadyEvent>) => void
|
||||||
onVideoChunkReady?: (event: NativeSyntheticEvent<OnVideoChunkReadyEvent>) => void
|
onVideoChunkReady?: (event: NativeSyntheticEvent<OnVideoChunkReadyEvent>) => void
|
||||||
onViewReady: () => void
|
onViewReady: () => void
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user