WIP - implement ChunkedRecorder

- configure AVAssetWriter for fragmented mp4 output
- implement ChunkedRecorder to received chunk data via AVAssetWriterDelegate
This commit is contained in:
Rui Rodrigues 2024-07-12 16:51:09 +01:00
parent 23459b2635
commit d9a1287b68
4 changed files with 98 additions and 3 deletions

View File

@ -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)
}
}
}

View File

@ -29,6 +29,7 @@ class RecordingSession {
private let assetWriter: AVAssetWriter private let assetWriter: AVAssetWriter
private var audioWriter: AVAssetWriterInput? private var audioWriter: AVAssetWriterInput?
private var videoWriter: AVAssetWriterInput? private var videoWriter: AVAssetWriterInput?
private let recorder: ChunkedRecorder
private let completionHandler: (RecordingSession, AVAssetWriter.Status, Error?) -> Void private let completionHandler: (RecordingSession, AVAssetWriter.Status, Error?) -> Void
private var startTimestamp: CMTime? private var startTimestamp: CMTime?
@ -49,7 +50,8 @@ class RecordingSession {
Gets the file URL of the recorded video. Gets the file URL of the recorded video.
*/ */
var url: URL { var url: URL {
return assetWriter.outputURL // FIXME:
return recorder.outputURL
} }
/** /**
@ -76,8 +78,24 @@ class RecordingSession {
completionHandler = completion completionHandler = completion
do { do {
assetWriter = try AVAssetWriter(outputURL: url, fileType: fileType) recorder = try ChunkedRecorder(url: url.deletingLastPathComponent())
assetWriter = AVAssetWriter(contentType: UTType(fileType.rawValue)!)
assetWriter.shouldOptimizeForNetworkUse = false 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 { } catch let error as NSError {
throw CameraError.capture(.createRecorderError(message: error.description)) throw CameraError.capture(.createRecorderError(message: error.description))
} }

View File

@ -39,7 +39,7 @@ class ViewController: UIViewController {
cameraView.photo = true cameraView.photo = true
cameraView.video = true cameraView.video = true
cameraView.audio = true cameraView.audio = false
cameraView.isActive = true cameraView.isActive = true
cameraView.cameraId = getCameraDeviceId() as NSString? cameraView.cameraId = getCameraDeviceId() as NSString?
cameraView.didSetProps([]) cameraView.didSetProps([])
@ -90,6 +90,7 @@ class ViewController: UIViewController {
} else { } else {
cameraView.startRecording( cameraView.startRecording(
options: [ options: [
"fileType": "mp4",
"videoCodec": "h265", "videoCodec": "h265",
]) { callback in ]) { callback in
print("callback", callback) print("callback", callback)

View File

@ -8,6 +8,8 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
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 */; };
B3AF8E892C41159300CC198C /* ChunkedRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AF8E872C41159300CC198C /* ChunkedRecorder.swift */; };
B3EF9F0D2C3FBD8300832EE7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3EF9F0C2C3FBD8300832EE7 /* AppDelegate.swift */; }; B3EF9F0D2C3FBD8300832EE7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3EF9F0C2C3FBD8300832EE7 /* AppDelegate.swift */; };
B3EF9F0F2C3FBD8300832EE7 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3EF9F0E2C3FBD8300832EE7 /* SceneDelegate.swift */; }; B3EF9F0F2C3FBD8300832EE7 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3EF9F0E2C3FBD8300832EE7 /* SceneDelegate.swift */; };
B3EF9F112C3FBD8300832EE7 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3EF9F102C3FBD8300832EE7 /* ViewController.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 = "<group>"; }; 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>"; }; 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>"; }; 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; }; 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>"; }; 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>"; }; B3EF9F0E2C3FBD8300832EE7 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
@ -364,6 +367,7 @@
B88103E22AD7065C0087F063 /* CameraSessionDelegate.swift */, B88103E22AD7065C0087F063 /* CameraSessionDelegate.swift */,
B83D5EE629377117000AFD2F /* PreviewView.swift */, B83D5EE629377117000AFD2F /* PreviewView.swift */,
B8DB3BC9263DC4D8004C18D7 /* RecordingSession.swift */, B8DB3BC9263DC4D8004C18D7 /* RecordingSession.swift */,
B3AF8E872C41159300CC198C /* ChunkedRecorder.swift */,
B887515C25E0102000DB86D6 /* PhotoCaptureDelegate.swift */, B887515C25E0102000DB86D6 /* PhotoCaptureDelegate.swift */,
B84760DE2608F57D004C3180 /* CameraQueues.swift */, B84760DE2608F57D004C3180 /* CameraQueues.swift */,
B887518325E0102000DB86D6 /* CameraError.swift */, B887518325E0102000DB86D6 /* CameraError.swift */,
@ -636,6 +640,7 @@
B88977BE2B556DBA0095C92C /* AVCaptureDevice+minFocusDistance.swift in Sources */, B88977BE2B556DBA0095C92C /* AVCaptureDevice+minFocusDistance.swift in Sources */,
B80175EC2ABDEBD000E7DE90 /* ResizeMode.swift in Sources */, B80175EC2ABDEBD000E7DE90 /* ResizeMode.swift in Sources */,
B887519F25E0102000DB86D6 /* AVCaptureDevice.DeviceType+physicalDeviceDescriptor.swift in Sources */, B887519F25E0102000DB86D6 /* AVCaptureDevice.DeviceType+physicalDeviceDescriptor.swift in Sources */,
B3AF8E882C41159300CC198C /* ChunkedRecorder.swift in Sources */,
B88685ED2AD6A5E600E93869 /* CameraSession+CodeScanner.swift in Sources */, B88685ED2AD6A5E600E93869 /* CameraSession+CodeScanner.swift in Sources */,
B8207AAD2B0E5DD70002990F /* AVCaptureSession+synchronizeBuffer.swift in Sources */, B8207AAD2B0E5DD70002990F /* AVCaptureSession+synchronizeBuffer.swift in Sources */,
B8D22CDC2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */, B8D22CDC2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */,
@ -672,6 +677,7 @@
B3EF9F6A2C3FC46900832EE7 /* Promise.swift in Sources */, B3EF9F6A2C3FC46900832EE7 /* Promise.swift in Sources */,
B3EF9F4B2C3FC31E00832EE7 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */, B3EF9F4B2C3FC31E00832EE7 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */,
B3EF9F5E2C3FC43000832EE7 /* AVCapturePhotoOutput.QualityPrioritization+descriptor.swift in Sources */, B3EF9F5E2C3FC43000832EE7 /* AVCapturePhotoOutput.QualityPrioritization+descriptor.swift in Sources */,
B3AF8E892C41159300CC198C /* ChunkedRecorder.swift in Sources */,
B3EF9F5F2C3FC43000832EE7 /* AVAuthorizationStatus+descriptor.swift in Sources */, B3EF9F5F2C3FC43000832EE7 /* AVAuthorizationStatus+descriptor.swift in Sources */,
B3EF9F602C3FC43000832EE7 /* AVCaptureDevice.FlashMode+descriptor.swift in Sources */, B3EF9F602C3FC43000832EE7 /* AVCaptureDevice.FlashMode+descriptor.swift in Sources */,
B3EF9F612C3FC43000832EE7 /* AVFileType+descriptor.swift in Sources */, B3EF9F612C3FC43000832EE7 /* AVFileType+descriptor.swift in Sources */,