WIP - implement ChunkedRecorder
- configure AVAssetWriter for fragmented mp4 output - implement ChunkedRecorder to received chunk data via AVAssetWriterDelegate
This commit is contained in:
parent
23459b2635
commit
d9a1287b68
70
package/ios/Core/ChunkedRecorder.swift
Normal file
70
package/ios/Core/ChunkedRecorder.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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 */,
|
||||||
|
Loading…
Reference in New Issue
Block a user