// Writing output video import AVFoundation extension SampleProvider { func isFinished() -> Bool { return counter == audios.count } func isVideoFinished() -> Bool { return counter == infoVideo.count && qFrame == 0 } } class WriteVideo: NSObject, ObservableObject { private let synthesizer = AVSpeechSynthesizer() var canExport = false var url: URL? private var infoVideo = [(photoIndex: Int, frameTime: Float64)]() var videoURL: URL? var counterImage = 0 // Each word = add one unit let semaphore = DispatchSemaphore(value: 0) override init() { super.init() Misc.obj.selectedPhotos.removeAll() Misc.obj.selectedPhotos.append(createBlueImage(CGSize(width: 1920, height: 1080))) Misc.obj.selectedPhotos.append(createBlueImage(CGSize(width: 1920, height: 1080))) synthesizer.delegate = self } func beginWriteVideo(_ texto: String) { Misc.obj.selectedPhotos.removeAll() Misc.obj.selectedPhotos.append(createBlueImage(CGSize(width: 1920, height: 1080))) Misc.obj.selectedPhotos.append(createBlueImage(CGSize(width: 1920, height: 1080))) DispatchQueue.global().async { do { try self.writtingVideo(texto) print("Video created successful!") } catch { print(error.localizedDescription) } } } func writtingVideo(_ texto: String) throws { infoVideo.removeAll() var audioBuffers = [CMSampleBuffer]() var pixelBuffer = Misc.obj.selectedPhotos[0].toCVPixelBuffer() var audioReaderInput: AVAssetWriterInput? var audioReaderBuffers = [CMSampleBuffer]() var videoReaderBuffers = [(frame: CVPixelBuffer, time: CMTime)]() // Restante do texto let utterance = AVSpeechUtterance(string: texto) utterance.voice = AVSpeechSynthesisVoice(identifier: "pt-BR") // Escreve texto restante synthesizer.write(utterance) { buffer in autoreleasepool { if let buffer = buffer as? AVAudioPCMBuffer, let sampleBuffer = buffer.toCMSampleBuffer(presentationTime: .zero) { audioBuffers.append(sampleBuffer) } } usleep(1000) } semaphore.wait() // Diretório do arquivo url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("teste/output.mp4") guard let url = url else { return } try FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true) if FileManager.default.fileExists(atPath: url.path()) { try FileManager.default.removeItem(at: url) } // Get CMSampleBuffer of a video asset if let videoURL = videoURL { let videoAsset = AVAsset(url: videoURL) Task { let videoAssetTrack = try await videoAsset.loadTracks(withMediaType: .video).first! let audioTrack = try await videoAsset.loadTracks(withMediaType: .audio).first! let reader = try AVAssetReader(asset: videoAsset) let videoSettings = [ kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_32BGRA, kCVPixelBufferWidthKey: videoAssetTrack.naturalSize.width, kCVPixelBufferHeightKey: videoAssetTrack.naturalSize.height ] as [String: Any] let readerVideoOutput = AVAssetReaderTrackOutput(track: videoAssetTrack, outputSettings: videoSettings) let audioSettings = [ AVFormatIDKey: kAudioFormatLinearPCM, AVSampleRateKey: 44100, AVNumberOfChannelsKey: 2 ] as [String : Any] let readerAudioOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: audioSettings) reader.add(readerVideoOutput) reader.add(readerAudioOutput) reader.startReading() // Video CMSampleBuffer while let sampleBuffer = readerVideoOutput.copyNextSampleBuffer() { autoreleasepool { if let imgBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) { let pixBuf = imgBuffer as CVPixelBuffer let pTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) videoReaderBuffers.append((frame: pixBuf, time: pTime)) } } } // Audio CMSampleBuffer while let sampleBuffer = readerAudioOutput.copyNextSampleBuffer() { audioReaderBuffers.append(sampleBuffer) } semaphore.signal() } } semaphore.wait() let audioProvider = SampleProvider(audios: audioBuffers) let videoProvider = SampleProvider(infoVideo: infoVideo) let audioInput = createAudioInput(sampleBuffer: audioReaderBuffers[0]) let videoInput = createVideoInput(videoBuffers: pixelBuffer!) let adaptor = createPixelBufferAdaptor(videoInput: videoInput) let assetWriter = try AVAssetWriter(outputURL: url, fileType: .mp4) assetWriter.add(videoInput) assetWriter.add(audioInput) assetWriter.startWriting() assetWriter.startSession(atSourceTime: .zero) // Add video buffer and audio buffer in AVAssetWriter var frameCounter = Int64.zero let videoReaderProvider = SampleReaderProvider(frames: videoReaderBuffers) let audioReaderProvider = SampleReaderProvider(audios: audioReaderBuffers) while true { if videoReaderProvider.isFinished() && audioReaderProvider.isFinished() { break // Video continuation beloow the while } autoreleasepool { if videoInput.isReadyForMoreMediaData { if let buffer = videoReaderProvider.getNextVideoBuffer() { adaptor.append(buffer.frame, withPresentationTime: buffer.time) frameCounter += 1 // Used in other while, but add here } } } // Audio buffer autoreleasepool { if audioInput.isReadyForMoreMediaData { if let buffer = audioReaderProvider.getNextAudioBuffer() { audioInput.append(buffer) } } } } // Now, add the audioBuffers and the idnexPhotoBuffers content while true { if videoProvider.isVideoFinished() && audioProvider.isFinished() { videoInput.markAsFinished() audioInput.markAsFinished() break } autoreleasepool { if videoInput.isReadyForMoreMediaData { if let frame = videoProvider.moreFrame() { frameCounter += 1 while !videoInput.isReadyForMoreMediaData { usleep(1000) } adaptor.append(frame, withPresentationTime: CMTimeMake(value: frameCounter, timescale: 30)) } else { videoInput.markAsFinished() } } } if audioInput.isReadyForMoreMediaData { autoreleasepool { if let buffer = audioProvider.getNextAudio() { audioInput.append(buffer) } } } if let error = assetWriter.error { print(error.localizedDescription) fatalError() } } assetWriter.finishWriting { switch assetWriter.status { case .completed: print("Operation completed successfully: \(url.absoluteString)") self.canExport = true case .failed: if let error = assetWriter.error { print("Error description: \(error.localizedDescription)") } else { print("Error not found.") } default: print("Error not found.") } } } } extension WriteVideo: AVSpeechSynthesizerDelegate { func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, willSpeakRangeOfSpeechString characterRange: NSRange, utterance: AVSpeechUtterance) { infoVideo.append((photoIndex: 0, frameTime: 1.0)) } func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) { infoVideo.append((photoIndex: counterImage, frameTime: 1.0)) self.semaphore.signal() } }