VPCamera3/tdvideo/tdvideo/SpatialVideoConverter.swift
2024-03-05 11:44:34 +08:00

422 lines
17 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// SpatialVideoConverter.swift
// vp
//
// Created by soldoros on 2024/1/17.
//
import AVFoundation
import CoreImage
import Foundation
import Observation
import VideoToolbox
///
class SpatialVideoConverter:NSObject {
// MARK: - Properties
// MARK: Public
///
var totalFrames: Double = .zero
///
var framesProcessed: Double = 0.0
///
var timeRemaining: Double = .zero
///
var startTime: Date = .now
///使
var isProcessing = false
///
var lastConvertedFileURL: LastConvertedFile?
///
var leftEyeImage: CVPixelBuffer?
///
var rightEyeImage: CVPixelBuffer?
// MARK: Private
///
private let processor = FrameProcessor()
///
private var writer: AVAssetWriter?
///avassetwwriter
private let videoInputQueue = DispatchQueue(label: "com.test.spatialWriterVideo")
///avassetwwriter
private let audioInputQueue = DispatchQueue(label: "com.test.spatialWriterAudio")
///avassetwwriter
private var writerVideoInput: AVAssetWriterInput?
///avassetwwriter
private var writerAudioInput: AVAssetWriterInput?
///
private var heroReader: AVAssetReader?
/// AVAssetReader
private var readerVideoOutput: AVAssetReaderTrackOutput?
/// AVAssetReader
private var readerAudioOutput: AVAssetReaderTrackOutput?
///
private var videoWritingFinished = false
///
private var audioWritingFinished = false
///()
private var dateFormatter: DateComponentsFormatter {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.day, .hour, .minute, .second]
formatter.unitsStyle = .abbreviated
return formatter
}
///()
private var byteCountFormatter: ByteCountFormatter {
let formatter = ByteCountFormatter()
formatter.allowedUnits = [.useGB, .useMB, .useKB]
formatter.countStyle = .file
return formatter
}
// MARK: - Methods
// MARK: Public
///
/// -:
/// - sourceVideoURL:URLAVFoundationH.264H.265ProRes
/// - outputVideoURL:URL
func convertStereoscopicVideoToSpatialVideo(
sourceVideoURL: URL,
outputVideoURL: URL,
progress: ((Float)->())? = nil
) async throws {
let heroAsset = AVAsset(url: sourceVideoURL)
//
try removeExistingFile(at: outputVideoURL)
writer = try AVAssetWriter(outputURL: outputVideoURL, fileType: .mov)
guard let videoTrack = try await heroAsset.loadTracks(withMediaType: .video).first else {
return
}
let audioTrack = try await heroAsset.loadTracks(withMediaType: .audio).first
guard let videoFormatDescription = try await videoTrack.load(.formatDescriptions).first else {
return
}
if !processor.isPrepared {
processor.prepare(with: videoFormatDescription, outputRetainedBufferCountHint: 1)
}
//
let naturalSize = try await videoTrack.load(.naturalSize)
let leftEyeRegion = CGRect(
x: 0,
y: 0,
width: naturalSize.width / 2,
height: naturalSize.height
)
let rightEyeRegion = CGRect(
x: naturalSize.width / 2,
y: 0,
width: naturalSize.width / 2,
height: naturalSize.height
)
//
let frameRate = try await videoTrack.load(.nominalFrameRate)
let dataRate = try await videoTrack.load(.estimatedDataRate)
let duration = try await heroAsset.load(.duration)
let frames = CMTimeGetSeconds(duration) * Double(frameRate)
totalFrames = frames
// TODO:
// TODO:
//
var videoSettings = AVOutputSettingsAssistant(preset: .mvhevc1440x1440)?.videoSettings
videoSettings?[AVVideoWidthKey] = leftEyeRegion.width
videoSettings?[AVVideoHeightKey] = leftEyeRegion.height
var compressionProperties = videoSettings?[AVVideoCompressionPropertiesKey] as! [String: Any]
compressionProperties[AVVideoAverageBitRateKey] = dataRate
compressionProperties[kVTCompressionPropertyKey_HorizontalDisparityAdjustment as String] = 0
compressionProperties[kCMFormatDescriptionExtension_HorizontalFieldOfView as String] = 90
videoSettings?[AVVideoCompressionPropertiesKey] = compressionProperties
//
writerVideoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings)
guard let writerVideoInput else { return }
writerVideoInput.expectsMediaDataInRealTime = false
//
if audioTrack != nil {
writerAudioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: nil)
}
//
let pixelBufferAdaptor = AVAssetWriterInputTaggedPixelBufferGroupAdaptor(
assetWriterInput: writerVideoInput,
sourcePixelBufferAttributes: .none)
//()
//
let readerOutputSettings: [String:Any] = [
kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: kCVPixelFormatType_32BGRA),
kCVPixelBufferWidthKey as String: naturalSize.width,
kCVPixelBufferHeightKey as String: naturalSize.height
]
readerVideoOutput = AVAssetReaderTrackOutput(
track: videoTrack,
outputSettings: readerOutputSettings)
if let audioTrack {
readerAudioOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: nil)
}
heroReader = try AVAssetReader(asset: heroAsset)
//
guard let heroReader,
let readerVideoOutput
else { return }
heroReader.add(readerVideoOutput)
if let readerAudioOutput {
heroReader.add(readerAudioOutput)
}
heroReader.startReading()
//
guard let writer else { return }
writer.add(writerVideoInput)
if let writerAudioInput {
writer.add(writerAudioInput)
}
writer.startWriting()
writer.startSession(atSourceTime: .zero)
//
isProcessing = true
//
//
startTime = Date.now
writerVideoInput.requestMediaDataWhenReady(on: videoInputQueue) { [weak self] in
guard let self else { return }
while writerVideoInput.isReadyForMoreMediaData {
autoreleasepool {
guard self.processor.isPrepared else {
print("The processor is not prepared. Cannot write video")
return
}
if let frame = readerVideoOutput.copyNextSampleBuffer(),
let frameBuffer = CMSampleBufferGetImageBuffer(frame) {
let sourceBuffer = CIImage(cvImageBuffer: frameBuffer)
// Setup the left and right eye `CVPixelBuffer` references.
guard let leftEye = self.processor.cropPixelBuffer(
pixelBufferImage: sourceBuffer,
targetRect: leftEyeRegion
),
let rightEye = self.processor.cropPixelBuffer(
pixelBufferImage: sourceBuffer,
targetRect: rightEyeRegion
)
else { return }
// Set a video preview
self.leftEyeImage = leftEye
self.rightEyeImage = rightEye
// Create an array of `CMTaggedBuffers, one for each eye's view.
let taggedBuffers: [CMTaggedBuffer] = [
.init(tags: [.videoLayerID(0), .stereoView(.leftEye)], pixelBuffer: leftEye),
.init(tags: [.videoLayerID(1), .stereoView(.rightEye)], pixelBuffer: rightEye)
]
let didAppend = pixelBufferAdaptor.appendTaggedBuffers(
taggedBuffers,
withPresentationTime: frame.presentationTimeStamp
)
if !didAppend {
print("Failed to append frame.")
}
// Increment the number of frames processed.
if self.framesProcessed < (self.totalFrames - 1) {
self.framesProcessed += 1
}
// Calculate the estimated time remaining based on how long this frame took to process.
self.calculateTimeRemaining()
progress?( Float(self.framesProcessed)/Float(self.totalFrames))
} else {
if !self.videoWritingFinished {
sourceVideoURL.stopAccessingSecurityScopedResource()
self.videoWritingFinished.toggle()
writerVideoInput.markAsFinished()
self.stop(with: outputVideoURL)
}
}
}
}
}
//
if let writerAudioInput,
let readerAudioOutput {
writerAudioInput.requestMediaDataWhenReady(on: audioInputQueue) { [weak self] in
guard let self else { return }
while writerAudioInput.isReadyForMoreMediaData {
autoreleasepool {
if let sample = readerAudioOutput.copyNextSampleBuffer() {
writerAudioInput.append(sample)
} else {
if !self.audioWritingFinished {
self.audioWritingFinished.toggle()
writerAudioInput.markAsFinished()
self.stop(with: outputVideoURL)
}
}
}
}
}
}
}
///
///
/// -expectedOutputURL:' URL '
///
func cancel(expectedOutputURL: URL) {
writerVideoInput?.markAsFinished()
writerAudioInput?.markAsFinished()
writer?.cancelWriting()
try? removeExistingFile(at: expectedOutputURL)
resetWriter()
}
// MARK: - Private
///URL
/// -outputVideoURL:URL
private func removeExistingFile(at outputVideoURL: URL) throws {
try FileManager.default.removeItem(atPath: outputVideoURL.path)
}
///
private func calculateTimeRemaining() {
let totalTimeElapsed = Date.now.timeIntervalSince1970 - startTime.timeIntervalSince1970
let totalFramesCompleted = framesProcessed
let averageTimeBetweenFrames = totalTimeElapsed / totalFramesCompleted
let estimatedTimeRemaining = averageTimeBetweenFrames * (totalFrames - totalFramesCompleted)
guard self.timeRemaining != 0 else {
self.timeRemaining = estimatedTimeRemaining
return
}
if estimatedTimeRemaining < self.timeRemaining + 100 {
self.timeRemaining = estimatedTimeRemaining
}
}
///
///outputURL:URL
private func stop(with outputURL: URL) {
guard isProcessing,
let writerVideoInput,
videoWritingFinished
else { return }
if let writerAudioInput {
guard audioWritingFinished else { return }
}
self.writer?.finishWriting { [weak self] in
guard let self else {return}
Task {
try? await self.saveLastConvertedFile(outputURL: outputURL)
outputURL.stopAccessingSecurityScopedResource()
self.resetWriter()
}
print("finished writing")
}
}
///
///
private func resetWriter() {
isProcessing = false
self.totalFrames = 0
self.framesProcessed = 0
self.timeRemaining = 0
self.startTime = .now
self.writerVideoInput = nil
self.writerAudioInput = nil
self.writer = nil
self.readerVideoOutput = nil
self.readerAudioOutput = nil
self.heroReader = nil
self.videoWritingFinished = false
self.audioWritingFinished = false
}
///
///访
/// -outputURL:' URL '
private func saveLastConvertedFile(outputURL: URL) async throws {
do {
let attr = try FileManager.default.attributesOfItem(atPath: outputURL.path)
let fileSize = attr[FileAttributeKey.size] as? Int64
let asset = AVAsset(url: outputURL)
let duration = try await asset.load(.duration)
self.lastConvertedFileURL = LastConvertedFile(
filePath: outputURL,
timeToProcess: dateFormatter.string(from: startTime, to: Date.now) ?? "Unknown",
fileSize: byteCountFormatter.string(fromByteCount: fileSize ?? 0),
duration: dateFormatter.string(from: duration.seconds) ?? "Unknown"
)
} catch {
print("Error: \(error)")
}
}
}
///
struct LastConvertedFile: Codable, Equatable {
///
let filePath: URL
///
let timeToProcess: String
///
let fileSize: String
///
let duration: String
}