Compare commits

...

3 Commits

Author SHA1 Message Date
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
6 changed files with 72 additions and 19 deletions

View File

@ -62,6 +62,8 @@ public final class CameraView: UIView, CameraSessionDelegate {
@objc var onStarted: RCTDirectEventBlock? @objc var onStarted: RCTDirectEventBlock?
@objc var onStopped: RCTDirectEventBlock? @objc var onStopped: RCTDirectEventBlock?
@objc var onViewReady: RCTDirectEventBlock? @objc var onViewReady: RCTDirectEventBlock?
@objc var onInitReady: RCTDirectEventBlock?
@objc var onVideoChunkReady: RCTDirectEventBlock?
@objc var onCodeScanned: RCTDirectEventBlock? @objc var onCodeScanned: RCTDirectEventBlock?
// zoom // zoom
@objc var enableZoomGesture = false { @objc var enableZoomGesture = false {
@ -335,6 +337,26 @@ public final class CameraView: UIView, CameraSessionDelegate {
} }
#endif #endif
} }
func onVideoChunkReady(chunk: ChunkedRecorder.Chunk) {
ReactLogger.log(level: .info, message: "Chunk ready: \(chunk)")
guard let onVideoChunkReady, let onInitReady else {
return
}
switch chunk.type {
case .initialization:
onInitReady([
"filepath": chunk.url.path,
])
case .data(index: let index):
onVideoChunkReady([
"filepath": chunk.url.path,
"index": index,
])
}
}
func onCodeScanned(codes: [CameraSession.Code], scannerFrame: CameraSession.CodeScannerFrame) { func onCodeScanned(codes: [CameraSession.Code], scannerFrame: CameraSession.CodeScannerFrame) {
guard let onCodeScanned = onCodeScanned else { 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(onStarted, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onStopped, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onStopped, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onViewReady, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onViewReady, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onInitReady, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoChunkReady, RCTDirectEventBlock);
// Code Scanner // Code Scanner
RCT_EXPORT_VIEW_PROPERTY(codeScannerOptions, NSDictionary); RCT_EXPORT_VIEW_PROPERTY(codeScannerOptions, NSDictionary);
RCT_EXPORT_VIEW_PROPERTY(onCodeScanned, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onCodeScanned, RCTDirectEventBlock);

View File

@ -33,6 +33,14 @@ extension CameraSession {
} }
let enableAudio = self.configuration?.audio != .disabled 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 // Callback for when the recording ends
let onFinish = { (recordingSession: RecordingSession, status: AVAssetWriter.Status, error: Error?) in let onFinish = { (recordingSession: RecordingSession, status: AVAssetWriter.Status, error: Error?) in
@ -89,6 +97,7 @@ extension CameraSession {
// Create RecordingSession for the temp file // Create RecordingSession for the temp file
let recordingSession = try RecordingSession(url: tempURL, let recordingSession = try RecordingSession(url: tempURL,
fileType: options.fileType, fileType: options.fileType,
onChunkReady: onChunkReady,
completion: onFinish) completion: onFinish)
// Init Audio + Activate Audio Session (optional) // 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) Called for every frame (if video or frameProcessor is enabled)
*/ */
func onFrame(sampleBuffer: CMSampleBuffer) 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 Called whenever a QR/Barcode has been scanned. Only if the CodeScanner Output is enabled
*/ */

View File

@ -12,14 +12,24 @@ import AVFoundation
class ChunkedRecorder: NSObject { class ChunkedRecorder: NSObject {
enum ChunkType {
case initialization
case data(index: UInt64)
}
struct Chunk {
let url: URL
let type: ChunkType
}
let outputURL: URL let outputURL: URL
let onChunkReady: ((Chunk) -> Void)
private var initSegment: Data? private var index: UInt64 = 0
private var index: Int = 0
init(url: URL) throws { init(url: URL, onChunkReady: @escaping ((Chunk) -> Void)) throws {
outputURL = url outputURL = url
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)
} }
@ -44,27 +54,32 @@ extension ChunkedRecorder: AVAssetWriterDelegate {
} }
} }
private func saveInitSegment(_ data: Data) { private func saveInitSegment(_ data: Data) {
initSegment = data let url = outputURL.appendingPathComponent("init.mp4")
save(data: data, url: url)
onChunkReady(url: url, type: .initialization)
} }
private func saveSegment(_ data: Data) { private func saveSegment(_ data: Data) {
guard let initSegment else { defer {
print("missing init segment") index += 1
return
} }
let name = String(format: "%06d.mp4", index)
let file = String(format: "%06d.mp4", index) let url = outputURL.appendingPathComponent(name)
index += 1 save(data: data, url: url)
let url = outputURL.appendingPathComponent(file) onChunkReady(url: url, type: .data(index: index))
}
private func save(data: Data, url: URL) {
do { do {
let outputData = initSegment + data try data.write(to: url)
try outputData.write(to: url)
print("writing", data.count, "to", url)
} catch { } catch {
print("Error--->", error) 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

@ -74,11 +74,12 @@ class RecordingSession {
init(url: URL, init(url: URL,
fileType: AVFileType, fileType: AVFileType,
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()) recorder = try ChunkedRecorder(url: url.deletingLastPathComponent(), 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