AVAssetExportSession.exportAsynchronously returns 'Operation Stopped'

213 Views Asked by At

I'm trying to splice together some videos stored as files and then export these videos to the photo library. The code looks more complicated than it is. Walks through 'instructionURLs' which point to generated video files and then should splice them together, but it returns 'Operation Stopped'. I have the following code:

import SwiftUI
import AVKit
import AVFoundation
import Photos

class VideoRecorder: NSObject, ObservableObject, AVCaptureFileOutputRecordingDelegate, AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate {
    var captureSession: AVCaptureSession?
    var previewLayer: AVCaptureVideoPreviewLayer?
    var videoFileOutput: AVCaptureMovieFileOutput?
    var instructionURLs: [URL] = []
    var audioSamples: [CMSampleBuffer] = []
    
    override init() {
        super.init()
        startCaptureSession()
    }
    
    func startCaptureSession() {
        // Check if video recording is available
        guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
            print("Video recording is not available.")
            return
        }
        
        // Create a new AVCaptureSession
        captureSession = AVCaptureSession()
        
        // Set the video quality and resolution
        captureSession?.sessionPreset = .high
        
        // Get the back camera
        guard let backCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
            print("Back camera not found.")
            return
        }
        
        do {
            // Create an input from the back camera
            let input = try AVCaptureDeviceInput(device: backCamera)
            
            // Add the input to the capture session
            if captureSession!.canAddInput(input) {
                captureSession!.addInput(input)
            }
            
            // Create a video data output
            let videoOutput = AVCaptureVideoDataOutput()
            videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.global(qos: .default))
            
            // Add the video output to the capture session
            if captureSession!.canAddOutput(videoOutput) {
                captureSession!.addOutput(videoOutput)
            }
            
            // Create an audio data output
            let audioOutput = AVCaptureAudioDataOutput()
            audioOutput.setSampleBufferDelegate(self, queue: DispatchQueue.global(qos: .default))
            
            // Add the audio output to the capture session
            if captureSession!.canAddOutput(audioOutput) {
                captureSession!.addOutput(audioOutput)
            }
            
            // Create a movie file output
            videoFileOutput = AVCaptureMovieFileOutput()
            
            // Add the video file output to the capture session
            if captureSession!.canAddOutput(videoFileOutput!) {
                captureSession!.addOutput(videoFileOutput!)
            }
            
            // Create a preview layer for the video recording
            previewLayer = AVCaptureVideoPreviewLayer(session: captureSession!)
            previewLayer?.videoGravity = .resizeAspectFill
            
            // Start the capture session
            captureSession!.startRunning()
        } catch {
            print("Error setting up video recording: \(error.localizedDescription)")
        }
    }
    
    func stopCaptureSession() {
        // Stop the capture session
        captureSession?.stopRunning()
    }
    
    func startRecording() {
        guard let videoFileOutput = videoFileOutput else { return }
        
        // Generate a unique URL for the new video clip
        let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let outputURL = documentsDirectory.appendingPathComponent("instruction\(instructionURLs.count).mov")
        
        // Start recording to the output URL
        videoFileOutput.startRecording(to: outputURL, recordingDelegate: self)
    }
    
    func stopRecording() {
        guard let videoFileOutput = videoFileOutput else { return }
        
        // Stop recording
        videoFileOutput.stopRecording()
    }
    
    func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
        if let error = error {
            print("Video recording error: \(error.localizedDescription)")
        } else {
            // Append the recorded video URL to the instructionURLs array
            instructionURLs.append(outputFileURL)
        }
    }
    
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        // Handle the audio or video samples here
        if let audioDataOutput = output as? AVCaptureAudioDataOutput {
            // Capture audio samples
            handleAudioSampleBuffer(sampleBuffer)
        } else if let videoDataOutput = output as? AVCaptureVideoDataOutput {
            // Capture video samples
            handleVideoSampleBuffer(sampleBuffer)
        }
    }
    
    func handleAudioSampleBuffer(_ sampleBuffer: CMSampleBuffer) {
        // Store the audio samples for later use
        audioSamples.append(sampleBuffer)
    }
    
    func handleVideoSampleBuffer(_ sampleBuffer: CMSampleBuffer) {
        // Handle the video samples here
    }
    
    func spliceVideos(completion: @escaping (URL?) -> Void) {
        // Create a composition to combine the videos
        let composition = AVMutableComposition()
        guard let audioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid),
              let videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) else {
            completion(nil)
            return
        }
        
        var currentTime = CMTime.zero
        // Insert the instruction videos into the composition
        for instructionURL in instructionURLs {
            let asset = AVAsset(url: instructionURL)
            print("instructionURL is \(instructionURL) and does it exist? \(FileManager.default.fileExists(atPath: instructionURL.path)). The assets are as listed: \(asset.tracks(withMediaType: .audio)) and \(asset.tracks(withMediaType: .video))")
            
            do {
                // Insert video track
                let videoAssetTrack = asset.tracks(withMediaType: .video).first!
                try videoTrack.insertTimeRange(CMTimeRange(start: .zero, duration: asset.duration),
                                               of: videoAssetTrack,
                                               at: currentTime)
                
                // Insert audio track
                if let audioAssetTrack = asset.tracks(withMediaType: .audio).first {
                    try audioTrack.insertTimeRange(CMTimeRange(start: .zero, duration: asset.duration),
                                                   of: audioAssetTrack,
                                                   at: currentTime)
                }
                
                currentTime = CMTimeAdd(currentTime, asset.duration)
            } catch {
                print("Error inserting video: \(error.localizedDescription)")
                completion(nil)
                return
            }
        }
        // Create a URL for the final output video
        let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let outputURL = documentsDirectory.appendingPathComponent("finalVideo.mov")
        // Export the composition to the output URL
        let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetLowQuality)
        
        exporter?.outputURL = outputURL
        
        exporter?.outputFileType = .mov
        
        // AVAssetExportSession can't run 'exportAsynchronously' function
        exporter?.exportAsynchronously {
            if exporter?.status == .completed {
                completion(outputURL)
            } else if let error = exporter?.error {
                print("Video export error: \(error.localizedDescription)")
                completion(nil)
            } else {
                completion(nil)
            }
        }

        print("10")
    }
    
    func saveVideoToPhotosLibrary(url: URL) {
        PHPhotoLibrary.requestAuthorization { status in
            guard status == .authorized else {
                print("Permission to access Photos library denied.")
                return
            }
            
            PHPhotoLibrary.shared().performChanges {
                PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url)
            } completionHandler: { success, error in
                if let error = error {
                    print("Error saving video to Photos library: \(error.localizedDescription)")
                } else {
                    print("Video saved to Photos library.")
                }
            }
        }
    }
}

struct ContentView: View {
    @StateObject private var videoRecorder = VideoRecorder()
    @State private var isRecording = false
    
    var body: some View {
        ZStack {
            // Show the video preview
            if let previewLayer = videoRecorder.previewLayer {
                VideoPreviewView(previewLayer: previewLayer)
            }
            
            VStack {
                Spacer()
                // Record button
                Button(action: {
                    if isRecording {
                        videoRecorder.stopRecording()
                    } else {
                        videoRecorder.startRecording()
                    }
                    
                    isRecording.toggle()
                }) {
                    Text(isRecording ? "Stop Recording" : "Start Recording")
                        .font(.title)
                        .padding()
                        .background(Color.red)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
                .padding()
                
                // Splice videos button
                Button(action: {
                    videoRecorder.spliceVideos { outputURL in
                        if let url = outputURL {
                            videoRecorder.saveVideoToPhotosLibrary(url: url)
                        }
                    }
                }) {
                    Text("Splice Videos")
                        .font(.title)
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
                .padding()
            }
        }
    }
}

At runtime, the videos are taken by the user and when a button is pressed, the 'spliceVideos' function is called to theoretically combine + export them to photo library.

0

There are 0 best solutions below