Search code examples
iosswiftuikitswift5avkit

Split video into chunks of 30 seconds Ios swift 5


I've been trying to let user choose from video gallery and split video to chunks of 30 seconds!

When I select a video of 1 minute, it splits to two videos of 30 seconds each, and it works fine!

I tried another example of video 35 seconds, and it splits to two videos of 30 and 4 seconds each

But when I select a video with more than 1 minute, it splits the video to chunks of random

numbers such as 34 seconds or 40, etc.

I don't want that!

I want to split videos to 30 seconds each!

This's my viewController code so far

import UIKit
import AVKit
import MobileCoreServices
import Photos
class ViewController: UIViewController { 
    @IBOutlet weak var videoView: UIImageView!
    @IBOutlet var imageView: UIImageView!
    var player: AVPlayer!
    var avpController = AVPlayerViewController()
    var isVideoGettinGEdited = false
    @IBAction func didTapButton(){     
        let picker = UIImagePickerController()
        picker.delegate = self
        picker.sourceType = .savedPhotosAlbum
        picker.mediaTypes = ["public.movie"]
        picker.allowsEditing = false
        present(picker, animated: true, completion: nil)
    }  
}

extension ViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {  
        if let image = info[UIImagePickerController.InfoKey(rawValue: "UIImagePickerControllerEditedImage")]as? UIImage{
            imageView.image = image
        } else {
            guard
                let mediaType = info[UIImagePickerController.InfoKey.mediaType] as? String,
                mediaType == (kUTTypeMovie as String),
                let url = info[UIImagePickerController.InfoKey.mediaURL] as? URL
            else { return }
            let videoAsset = AVURLAsset(url: url)
            let videoDuration = videoAsset.duration
            let durationTime = ceil( CMTimeGetSeconds(videoDuration))
            var startTime = 0.0
            var endTime = durationTime
            var numberOfBreaks = Int((Double(durationTime)/30.0))
            let isReminderTime = Double(durationTime.truncatingRemainder(dividingBy: 30.0))
            if isReminderTime > 0 {
                numberOfBreaks = numberOfBreaks + 1
            }
            if Double(durationTime) <= 30 {
                self.cropVideo(atURL: url, startTime: startTime, endTime: endTime, fileName: "Output.mp4")
            } else {
                endTime = 30
                while numberOfBreaks != 0 {
                    if !isVideoGettinGEdited {
                        print("Start time = \(startTime) and Endtime = \(endTime)")
                        self.cropVideo(atURL: url, startTime: startTime, endTime: endTime, fileName: "Output-\(numberOfBreaks).mp4")
                        numberOfBreaks = numberOfBreaks - 1
                        startTime = endTime
                        let timeLeft = Double(durationTime) - startTime
                        if timeLeft >= 30.0 {
                         endTime = endTime + 30.0
                        } else {
                            endTime =  timeLeft
                        }
                    }
                }
            }    
        }
        picker.dismiss(animated: true, completion: nil)
    }
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        picker.dismiss(animated: true, completion: nil)
    }
}

extension ViewController {
    func cropVideo(atURL url:URL, startTime:Double, endTime:Double, fileName:String) {
        let asset = AVURLAsset(url: url)
        let exportSession = AVAssetExportSession.init(asset: asset, presetName: AVAssetExportPresetHighestQuality)!
        var outputURL = URL(string:NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).last!)
        let fileManager = FileManager.default
        do {
            try fileManager.createDirectory(at: outputURL!, withIntermediateDirectories: true, attributes: nil)
        } catch {
            print(error)
        }
        outputURL?.appendPathComponent("\(fileName).mp4")
        // Remove existing file
        do {
            try fileManager.removeItem(atPath: outputURL!.absoluteString)
        } catch {
            print(error)
        }
        exportSession.outputURL = URL(fileURLWithPath: outputURL!.absoluteString)
        exportSession.shouldOptimizeForNetworkUse = true
        exportSession.outputFileType = AVFileType.mp4
        let start = CMTimeMakeWithSeconds(startTime, preferredTimescale: 600) // you will modify time range here
        let duration = CMTimeMakeWithSeconds(endTime, preferredTimescale: 600)
        let range = CMTimeRangeMake(start: start, duration: duration)
        exportSession.timeRange = range
        exportSession.exportAsynchronously {
            self.isVideoGettinGEdited = false
            switch(exportSession.status) {   
            case .completed:
                PHPhotoLibrary.shared().performChanges({
                    PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: outputURL!.absoluteString))
                }) { completed, error in
                    DispatchQueue.main.async {
                        self.view.isUserInteractionEnabled = true
                        if completed {
                            print("Video has been saved to your photos.")  
                        } else {
                            if error != nil {
                                print("Failed to save video in photos \(error).")
                            }
                        }
                    }
                }
                break
            case .failed:
                print("failed with \(exportSession.error)")
                break
            case .cancelled: break
            default:
                print("default")
                break
            }
        }
    }
    //MARK:- saveVideoFromURL
    func saveVideoFromURL(_ videoURL:String) {
        self.view.isUserInteractionEnabled = false
        DispatchQueue.global(qos: .background).async {
            if let url = URL(string: videoURL),
                let urlData = NSData(contentsOf: url) {
                let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0];
                print("url path component = \(url.lastPathComponent)")
                let filePath="\(documentsPath)/\(url.lastPathComponent)"
                    urlData.write(toFile: filePath, atomically: true)
                    PHPhotoLibrary.shared().performChanges({
                        PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: filePath))
                    }) { completed, error in
                        DispatchQueue.main.async {
                            self.view.isUserInteractionEnabled = true
                            if completed {
                                print("Video has been saved to your photos.")
                            } else {
                                if error != nil {
                                    print("Failed to save video.")
                                }
                            }
                        }
                    }
            }
        }
    }
}

Thanks!


Solution

  • You main issue is when calculating the duration. Btw I would change the preferred scale to 1 as well:

    change

    let duration = CMTimeMakeWithSeconds(endTime, preferredTimescale: 1)
    

    to

    let duration = CMTimeMakeWithSeconds(endTime-startTime, preferredTimescale: 1)
    

    I have also done some other changes to your code as following:


    extension ViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            if let image = info[.editedImage] as? UIImage {
                imageView.image = image
            } else {
                guard
                    let mediaType = info[.mediaType] as? String,
                    mediaType == "public.movie",
                    let url = info[.mediaURL] as? URL
                else { return }
                let videoAsset = AVURLAsset(url: url)
                let videoDuration = videoAsset.duration
                let durationTime = ceil(videoDuration.seconds)
                print("durationTime:" , durationTime)
                struct Duration {
                    let start: Double
                    let end: Double
                }
                let durations: [Duration]
                if durationTime < 30 {
                    durations = [Duration(start: 0, end: durationTime)]
                } else {
                    durations = (0...Int(durationTime)/30).compactMap {
                        if Double($0*30) == min(Double($0*30)+30, durationTime) {
                            return nil
                        }
                        return Duration(
                            start: Double($0*30),
                            end: min(Double($0*30)+30, durationTime)
                        )
                    }
                }
                for index in durations.indices {
                    let startTime = durations[index].start
                    let endTime = durations[index].end
                    print("Start time = \(startTime) and Endtime = \(endTime)")
                    saveVideo(
                        at: url,
                        startTime: startTime,
                        endTime: endTime,
                        fileName: "Output-\(index)"
                    )
                }
            }
            picker.dismiss(animated: true, completion: nil)
        }
        func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
            picker.dismiss(animated: true, completion: nil)
        }
    }
    

    extension ViewController {
        func saveVideo(
            at url: URL,
            startTime: Double,
            endTime:Double,
            fileName: String
        ) {
            let asset = AVURLAsset(url: url)
            let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality)!
            let outputURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
                .appendingPathComponent(fileName)
                .appendingPathExtension("mp4")
            // Remove existing file
            if FileManager.default.fileExists(atPath: outputURL.path) {
                do {
                    try FileManager.default.removeItem(at: outputURL)
                } catch {
                    print(error)
                }
            }
            exportSession.outputURL = outputURL
            exportSession.shouldOptimizeForNetworkUse = true
            exportSession.outputFileType = .mp4
            let start = CMTimeMakeWithSeconds(startTime, preferredTimescale: 1)
            let duration = CMTimeMakeWithSeconds(endTime-startTime, preferredTimescale: 1)
            let range = CMTimeRangeMake(start: start, duration: duration)
            print("Will Render \(fileName) from \(start.seconds) to \(duration)")
            exportSession.timeRange = range
            exportSession.exportAsynchronously {
                print("Did Render \(fileName) from \(start.seconds) to \(duration)")
                self.isVideoGettinGEdited = false
                switch exportSession.status {
                case .completed:
                    self.checkDuration(for: fileName, at: outputURL)
                    PHPhotoLibrary.shared().performChanges({
                        PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: outputURL)
                    }) { completed, error in
                        if let error = error {
                            print("Failed to save video in photos", error)
                            return
                        }
                        DispatchQueue.main.async {
                            self.view.isUserInteractionEnabled = true
                            if completed {
                                print("Video has been saved to your photos.")
                            } else {
                                print("Video saving has NOT been completed")
                            }
                        }
                    }
                    break
                case .failed:
                    print("failed with:", exportSession.error ?? "no error")
                    break
                case .cancelled: break
                default: break
                }
            }
        }
    }
    

    Sample Project