diff --git a/package/ios/Core/ChunkedRecorder.swift b/package/ios/Core/ChunkedRecorder.swift new file mode 100644 index 0000000..053dac1 --- /dev/null +++ b/package/ios/Core/ChunkedRecorder.swift @@ -0,0 +1,70 @@ +// +// ChunkedRecorder.swift +// VisionCamera +// +// Created by Rafael Bastos on 12/07/2024. +// Copyright © 2024 mrousavy. All rights reserved. +// + +import Foundation +import AVFoundation + + +class ChunkedRecorder: NSObject { + + let outputURL: URL + + private var initSegment: Data? + private var index: Int = 0 + + init(url: URL) throws { + outputURL = url + + 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) + @unknown default: + fatalError("Unknown AVAssetSegmentType!") + } + } + + private func saveInitSegment(_ data: Data) { + initSegment = data + } + + private func saveSegment(_ data: Data) { + guard let initSegment else { + print("missing init segment") + return + } + + let file = String(format: "%06d.mp4", index) + index += 1 + let url = outputURL.appendingPathComponent(file) + + do { + let outputData = initSegment + data + try outputData.write(to: url) + print("writing", data.count, "to", url) + } catch { + print("Error--->", error) + } + } + +} diff --git a/package/ios/Core/RecordingSession.swift b/package/ios/Core/RecordingSession.swift index 85e9c62..cd28a44 100644 --- a/package/ios/Core/RecordingSession.swift +++ b/package/ios/Core/RecordingSession.swift @@ -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? @@ -49,7 +50,8 @@ class RecordingSession { Gets the file URL of the recorded video. */ var url: URL { - return assetWriter.outputURL + // FIXME: + return recorder.outputURL } /** @@ -76,8 +78,24 @@ class RecordingSession { completionHandler = completion do { - assetWriter = try AVAssetWriter(outputURL: url, fileType: fileType) + recorder = try ChunkedRecorder(url: url.deletingLastPathComponent()) + 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)) } diff --git a/package/ios/TestRecorder/ViewController.swift b/package/ios/TestRecorder/ViewController.swift index 47f555e..13c6ca9 100644 --- a/package/ios/TestRecorder/ViewController.swift +++ b/package/ios/TestRecorder/ViewController.swift @@ -39,7 +39,7 @@ class ViewController: UIViewController { cameraView.photo = true cameraView.video = true - cameraView.audio = true + cameraView.audio = false cameraView.isActive = true cameraView.cameraId = getCameraDeviceId() as NSString? cameraView.didSetProps([]) @@ -90,6 +90,7 @@ class ViewController: UIViewController { } else { cameraView.startRecording( options: [ + "fileType": "mp4", "videoCodec": "h265", ]) { callback in print("callback", callback) diff --git a/package/ios/VisionCamera.xcodeproj/project.pbxproj b/package/ios/VisionCamera.xcodeproj/project.pbxproj index cd6ce75..986a922 100644 --- a/package/ios/VisionCamera.xcodeproj/project.pbxproj +++ b/package/ios/VisionCamera.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ 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 */; }; @@ -165,6 +167,7 @@ B3AF8E832C410FB600CC198C /* TestRecorder-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "TestRecorder-Bridging-Header.h"; sourceTree = ""; }; B3AF8E842C410FB700CC198C /* ReactStubs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReactStubs.h; sourceTree = ""; }; B3AF8E852C410FB700CC198C /* ReactStubs.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ReactStubs.m; sourceTree = ""; }; + B3AF8E872C41159300CC198C /* ChunkedRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChunkedRecorder.swift; sourceTree = ""; }; 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 = ""; }; B3EF9F0E2C3FBD8300832EE7 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -364,6 +367,7 @@ B88103E22AD7065C0087F063 /* CameraSessionDelegate.swift */, B83D5EE629377117000AFD2F /* PreviewView.swift */, B8DB3BC9263DC4D8004C18D7 /* RecordingSession.swift */, + B3AF8E872C41159300CC198C /* ChunkedRecorder.swift */, B887515C25E0102000DB86D6 /* PhotoCaptureDelegate.swift */, B84760DE2608F57D004C3180 /* CameraQueues.swift */, B887518325E0102000DB86D6 /* CameraError.swift */, @@ -636,6 +640,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 */, @@ -672,6 +677,7 @@ 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 */,