2024-07-12 09:51:09 -06:00
|
|
|
//
|
|
|
|
// ChunkedRecorder.swift
|
|
|
|
// VisionCamera
|
|
|
|
//
|
|
|
|
// Created by Rafael Bastos on 12/07/2024.
|
|
|
|
// Copyright © 2024 mrousavy. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
import AVFoundation
|
|
|
|
|
|
|
|
|
|
|
|
class ChunkedRecorder: NSObject {
|
2024-07-28 16:37:20 -06:00
|
|
|
|
2024-07-15 01:46:41 -06:00
|
|
|
enum ChunkType {
|
|
|
|
case initialization
|
2024-07-16 03:46:24 -06:00
|
|
|
case data(index: UInt64, duration: CMTime?)
|
2024-07-15 01:46:41 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
struct Chunk {
|
|
|
|
let url: URL
|
|
|
|
let type: ChunkType
|
|
|
|
}
|
2024-07-28 16:37:20 -06:00
|
|
|
|
2024-07-12 09:51:09 -06:00
|
|
|
let outputURL: URL
|
2024-07-15 01:46:41 -06:00
|
|
|
let onChunkReady: ((Chunk) -> Void)
|
2024-07-28 16:37:20 -06:00
|
|
|
|
2024-07-16 00:59:52 -06:00
|
|
|
private var chunkIndex: UInt64 = 0
|
2024-07-28 16:37:20 -06:00
|
|
|
|
2024-07-16 02:50:21 -06:00
|
|
|
init(outputURL: URL, onChunkReady: @escaping ((Chunk) -> Void)) throws {
|
|
|
|
self.outputURL = outputURL
|
2024-07-15 01:46:41 -06:00
|
|
|
self.onChunkReady = onChunkReady
|
2024-07-12 09:51:09 -06:00
|
|
|
guard FileManager.default.fileExists(atPath: outputURL.path) else {
|
|
|
|
throw CameraError.unknown(message: "output directory does not exist at: \(outputURL.path)", cause: nil)
|
|
|
|
}
|
|
|
|
}
|
2024-07-28 16:37:20 -06:00
|
|
|
|
2024-07-12 09:51:09 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
extension ChunkedRecorder: AVAssetWriterDelegate {
|
2024-07-28 16:37:20 -06:00
|
|
|
|
|
|
|
func assetWriter(_ writer: AVAssetWriter,
|
2024-07-12 09:51:09 -06:00
|
|
|
didOutputSegmentData segmentData: Data,
|
|
|
|
segmentType: AVAssetSegmentType,
|
|
|
|
segmentReport: AVAssetSegmentReport?) {
|
2024-07-28 16:37:20 -06:00
|
|
|
|
2024-07-12 09:51:09 -06:00
|
|
|
switch segmentType {
|
|
|
|
case .initialization:
|
|
|
|
saveInitSegment(segmentData)
|
|
|
|
case .separable:
|
2024-07-16 03:46:24 -06:00
|
|
|
saveSegment(segmentData, report: segmentReport)
|
2024-07-12 09:51:09 -06:00
|
|
|
@unknown default:
|
|
|
|
fatalError("Unknown AVAssetSegmentType!")
|
|
|
|
}
|
|
|
|
}
|
2024-07-28 16:37:20 -06:00
|
|
|
|
2024-07-15 01:46:41 -06:00
|
|
|
private func saveInitSegment(_ data: Data) {
|
|
|
|
let url = outputURL.appendingPathComponent("init.mp4")
|
|
|
|
save(data: data, url: url)
|
|
|
|
onChunkReady(url: url, type: .initialization)
|
2024-07-12 09:51:09 -06:00
|
|
|
}
|
2024-07-28 16:37:20 -06:00
|
|
|
|
2024-07-16 03:46:24 -06:00
|
|
|
private func saveSegment(_ data: Data, report: AVAssetSegmentReport?) {
|
2024-07-16 00:59:52 -06:00
|
|
|
let name = "\(chunkIndex).mp4"
|
2024-07-15 01:46:41 -06:00
|
|
|
let url = outputURL.appendingPathComponent(name)
|
|
|
|
save(data: data, url: url)
|
2024-07-16 03:46:24 -06:00
|
|
|
let duration = report?
|
|
|
|
.trackReports
|
|
|
|
.filter { $0.mediaType == .video }
|
|
|
|
.first?
|
|
|
|
.duration
|
|
|
|
onChunkReady(url: url, type: .data(index: chunkIndex, duration: duration))
|
2024-07-16 00:59:52 -06:00
|
|
|
chunkIndex += 1
|
2024-07-15 01:46:41 -06:00
|
|
|
}
|
2024-07-28 16:37:20 -06:00
|
|
|
|
2024-07-15 01:46:41 -06:00
|
|
|
private func save(data: Data, url: URL) {
|
2024-07-12 09:51:09 -06:00
|
|
|
do {
|
2024-07-15 01:46:41 -06:00
|
|
|
try data.write(to: url)
|
2024-07-12 09:51:09 -06:00
|
|
|
} catch {
|
2024-07-15 01:46:41 -06:00
|
|
|
ReactLogger.log(level: .error, message: "Unable to write \(url): \(error.localizedDescription)")
|
2024-07-12 09:51:09 -06:00
|
|
|
}
|
|
|
|
}
|
2024-07-28 16:37:20 -06:00
|
|
|
|
2024-07-15 01:46:41 -06:00
|
|
|
private func onChunkReady(url: URL, type: ChunkType) {
|
|
|
|
onChunkReady(Chunk(url: url, type: type))
|
|
|
|
}
|
2024-07-28 16:37:20 -06:00
|
|
|
|
2024-07-12 09:51:09 -06:00
|
|
|
}
|