Массив Swift Merge AVasset-Videos

Я хочу объединить AVAsset-arrayVideos в одно видео и сохранить его в фотопленке. На Raywenderlich.com есть отличный учебник, где два видео слились в один. Я создал следующий код, однако видео, которое я получаю после экспорта в фотопленку, включает только первое и последнее видео из массива (исключая остальные видео в середине arrayVideos). Я что-то упустил здесь?

var arrayVideos = [AVAsset]() //Videos Array    
var atTimeM: CMTime = CMTimeMake(0, 0)
var lastAsset: AVAsset!
var layerInstructionsArray = [AVVideoCompositionLayerInstruction]()
var completeTrackDuration: CMTime = CMTimeMake(0, 1)
var videoSize: CGSize = CGSize(width: 0.0, height: 0.0)

func mergeVideoArray(){

    let mixComposition = AVMutableComposition()
    for videoAsset in arrayVideos{
        let videoTrack = mixComposition.addMutableTrack(withMediaType: AVMediaTypeVideo, preferredTrackID: Int32(kCMPersistentTrackID_Invalid))
        do {
            if videoAsset == arrayVideos.first{
                atTimeM = kCMTimeZero
            } else{
                atTimeM = lastAsset!.duration
            }
            try videoTrack.insertTimeRange(CMTimeRangeMake(kCMTimeZero, videoAsset.duration), of: videoAsset.tracks(withMediaType: AVMediaTypeVideo)[0], at: atTimeM)  
            videoSize = videoTrack.naturalSize
        } catch let error as NSError {
            print("error: \(error)")
        }
        completeTrackDuration = CMTimeAdd(completeTrackDuration, videoAsset.duration)
        let videoInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
        if videoAsset != arrayVideos.last{
            videoInstruction.setOpacity(0.0, at: videoAsset.duration)
        }
        layerInstructionsArray.append(videoInstruction)
        lastAsset = videoAsset            
    }

    let mainInstruction = AVMutableVideoCompositionInstruction()
    mainInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, completeTrackDuration)
    mainInstruction.layerInstructions = layerInstructionsArray        

    let mainComposition = AVMutableVideoComposition()
    mainComposition.instructions = [mainInstruction]
    mainComposition.frameDuration = CMTimeMake(1, 30)
    mainComposition.renderSize = CGSize(width: videoSize.width, height: videoSize.height)

    let documentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
    let dateFormatter = DateFormatter()
    dateFormatter.dateStyle = .long
    dateFormatter.timeStyle = .short
    let date = dateFormatter.string(from: NSDate() as Date)
    let savePath = (documentDirectory as NSString).appendingPathComponent("mergeVideo-\(date).mov")
    let url = NSURL(fileURLWithPath: savePath)

    let exporter = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality)
    exporter!.outputURL = url as URL
    exporter!.outputFileType = AVFileTypeQuickTimeMovie
    exporter!.shouldOptimizeForNetworkUse = true
    exporter!.videoComposition = mainComposition
    exporter!.exportAsynchronously {

        PHPhotoLibrary.shared().performChanges({
            PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: exporter!.outputURL!)
        }) { saved, error in
            if saved {
                let alertController = UIAlertController(title: "Your video was successfully saved", message: nil, preferredStyle: .alert)
                let defaultAction = UIAlertAction(title: "OK", style: .default, handler: nil)
                alertController.addAction(defaultAction)
                self.present(alertController, animated: true, completion: nil)
            } else{
                print("video erro: \(error)")

            }
        }
    }
} 

person marcelosarquis    schedule 16.08.2016    source источник


Ответы (3)


Вам нужно отслеживать общее время для всех ресурсов и обновлять его для каждого видео.

Код в вашем вопросе перезаписывал atTimeM текущим видео. Вот почему только первый и последний были включены.

Это будет выглядеть примерно так:

...
var totalTime : CMTime = CMTimeMake(0, 0)

func mergeVideoArray() {

    let mixComposition = AVMutableComposition()
    for videoAsset in arrayVideos {
        let videoTrack = 
            mixComposition.addMutableTrack(withMediaType: AVMediaTypeVideo, 
                                           preferredTrackID: Int32(kCMPersistentTrackID_Invalid))          
        do {
            if videoAsset == arrayVideos.first {
                atTimeM = kCMTimeZero
            } else {
                atTimeM = totalTime // <-- Use the total time for all the videos seen so far.
            }
            try videoTrack.insertTimeRange(CMTimeRangeMake(kCMTimeZero, videoAsset.duration), 
                                           of: videoAsset.tracks(withMediaType: AVMediaTypeVideo)[0], 
                                           at: atTimeM)  
            videoSize = videoTrack.naturalSize
        } catch let error as NSError {
            print("error: \(error)")
        }
        totalTime += videoAsset.duration // <-- Update the total time for all videos.
...

Вы можете удалить использование lastAsset.

person Daniel Zhang    schedule 16.08.2016
comment
Приветствую Даниэля. Просто чтобы дополнить ваш ответ, нам нужно добавить общее время также в следующий код: - person marcelosarquis; 16.08.2016
comment
вы не можете увеличивать totalTime с помощью +=, вам нужно использовать CMTimeAdd. - person user3288724; 22.12.2017
comment
не используйте totalTime += videoAsset.duration используйте stackoverflow.com/a/15124487/4833705 вместо этого - person Lance Samaria; 10.10.2019
comment
Не забудьте инициализировать totalTime, иначе будет = NaN. Обязательно установите totalTime = CMTime.zero перед запуском цикла. - person PatPatchPatrick; 21.09.2020

Свифт 4

Используйте как

MeargeVide.mergeVideoArray(arrayVideos: arrayAsset) { (urlMeargeVide, error) in
 debugPrint("url",urlMeargeVide ?? "")
                debugPrint("error",error ?? "")
}

Завершите класс с ориентацией и объедините несколько клипов в один.

class MeargeVide {

   static func orientationFromTransform(_ transform: CGAffineTransform)
        -> (orientation: UIImageOrientation, isPortrait: Bool) {
            var assetOrientation = UIImageOrientation.up
            var isPortrait = false
            if transform.a == 0 && transform.b == 1.0 && transform.c == -1.0 && transform.d == 0 {
                assetOrientation = .right
                isPortrait = true
            } else if transform.a == 0 && transform.b == -1.0 && transform.c == 1.0 && transform.d == 0 {
                assetOrientation = .left
                isPortrait = true
            } else if transform.a == 1.0 && transform.b == 0 && transform.c == 0 && transform.d == 1.0 {
                assetOrientation = .up
            } else if transform.a == -1.0 && transform.b == 0 && transform.c == 0 && transform.d == -1.0 {
                assetOrientation = .down
            }
            return (assetOrientation, isPortrait)
    }

    static  func videoCompositionInstruction(_ track: AVCompositionTrack, asset: AVAsset)
        -> AVMutableVideoCompositionLayerInstruction {
            let instruction = AVMutableVideoCompositionLayerInstruction(assetTrack: track)
            let assetTrack = asset.tracks(withMediaType: .video)[0]

            let transform = assetTrack.preferredTransform
            let assetInfo = orientationFromTransform(transform)

            var scaleToFitRatio = UIScreen.main.bounds.width / assetTrack.naturalSize.width
            if assetInfo.isPortrait {
                scaleToFitRatio = UIScreen.main.bounds.width / assetTrack.naturalSize.height
                let scaleFactor = CGAffineTransform(scaleX: scaleToFitRatio, y: scaleToFitRatio)
                instruction.setTransform(assetTrack.preferredTransform.concatenating(scaleFactor), at: kCMTimeZero)
            } else {
                let scaleFactor = CGAffineTransform(scaleX: scaleToFitRatio, y: scaleToFitRatio)
                var concat = assetTrack.preferredTransform.concatenating(scaleFactor)
                    .concatenating(CGAffineTransform(translationX: 0, y: UIScreen.main.bounds.width / 2))
                if assetInfo.orientation == .down {
                    let fixUpsideDown = CGAffineTransform(rotationAngle: CGFloat(Double.pi))
                    let windowBounds = UIScreen.main.bounds
                    let yFix = assetTrack.naturalSize.height + windowBounds.height
                    let centerFix = CGAffineTransform(translationX: assetTrack.naturalSize.width, y: yFix)
                    concat = fixUpsideDown.concatenating(centerFix).concatenating(scaleFactor)
                }
                instruction.setTransform(concat, at: kCMTimeZero)
            }

            return instruction
    }

    class func mergeVideoArray(arrayVideos:[AVAsset], callBack:@escaping (_ urlGet:URL?,_ errorGet:Error?) -> Void){

        var atTimeM: CMTime = CMTimeMake(0, 0)
        var lastAsset: AVAsset!
        var layerInstructionsArray = [AVVideoCompositionLayerInstruction]()
        var completeTrackDuration: CMTime = CMTimeMake(0, 1)
        var videoSize: CGSize = CGSize(width: 0.0, height: 0.0)
        var totalTime : CMTime = CMTimeMake(0, 0)

        let mixComposition = AVMutableComposition.init()
        for videoAsset in arrayVideos{

            let videoTrack = mixComposition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: Int32(kCMPersistentTrackID_Invalid))
            do {
                if videoAsset == arrayVideos.first {
                    atTimeM = kCMTimeZero
                } else {
                    atTimeM = totalTime // <-- Use the total time for all the videos seen so far.
                }
                try videoTrack?.insertTimeRange(CMTimeRangeMake(kCMTimeZero, videoAsset.duration),
                                                of: videoAsset.tracks(withMediaType: AVMediaType.video)[0],
                                                at: completeTrackDuration)
                videoSize = (videoTrack?.naturalSize)!



            } catch let error as NSError {
                print("error: \(error)")
            }

            totalTime = CMTimeAdd(totalTime, videoAsset.duration)



            completeTrackDuration = CMTimeAdd(completeTrackDuration, videoAsset.duration)

            let firstInstruction = self.videoCompositionInstruction(videoTrack!, asset: videoAsset)
            firstInstruction.setOpacity(0.0, at: videoAsset.duration)

            layerInstructionsArray.append(firstInstruction)
            lastAsset = videoAsset
        }


        let mainInstruction = AVMutableVideoCompositionInstruction()
        mainInstruction.layerInstructions = layerInstructionsArray
        mainInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, completeTrackDuration)

        let mainComposition = AVMutableVideoComposition()
        mainComposition.instructions = [mainInstruction]
        mainComposition.frameDuration = CMTimeMake(1, 30)
        mainComposition.renderSize = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)

        let documentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
        let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .long
        dateFormatter.timeStyle = .short
        let date = dateFormatter.string(from: NSDate() as Date)
        let savePath = (documentDirectory as NSString).appendingPathComponent("mergeVideo-\(date).mov")
        let url = NSURL(fileURLWithPath: savePath)

        let exporter = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality)
        exporter!.outputURL = url as URL
        exporter!.outputFileType = AVFileType.mp4
        exporter!.shouldOptimizeForNetworkUse = true
        exporter!.videoComposition = mainComposition
        exporter!.exportAsynchronously {
            DispatchQueue.main.async {
                callBack(exporter?.outputURL, exporter?.error)
            }

        }
    } 
}
person chetu    schedule 18.06.2018
comment
второе видео отображается только одну секунду, а затем отображается черный экран - person Anita; 05.12.2018
comment
для исправления второго видео вам нужно установить firstInstruction.setOpacity(0.0, at: totalTime) - person medvedNick; 17.01.2019
comment
у меня это не сработало... второе видео черное, третье тоже... - person ; 26.08.2020
comment
Если вы все еще видите черные экраны, вам нужно установитьOpacity(0 at: totalTime). Вам также необходимо обязательно инициализировать totalTime перед запуском любых циклов (totalTime = CMTime.zero) - person PatPatchPatrick; 21.09.2020

Вам вообще не нужен atTimeM, так как вы просто маршируете completeTrackDuration вдоль него, где следует добавить следующий фрагмент. Так что замените

if videoAsset == arrayVideos.first{
            atTimeM = kCMTimeZero
        } else{
            atTimeM = lastAsset!.duration
        }
        try videoTrack.insertTimeRange(CMTimeRangeMake(kCMTimeZero, videoAsset.duration), of: videoAsset.tracks(withMediaType: AVMediaTypeVideo)[0], at: atTimeM)

с участием

try videoTrack.insertTimeRange(CMTimeRangeMake(kCMTimeZero, videoAsset.duration), of: videoAsset.tracks(withMediaType: AVMediaTypeVideo)[0], at: completeTrackDuration)
person user3288724    schedule 22.12.2017
comment
это не работает - второе видео отображается только в течение одной секунды, а затем отображается черный экран - person Anita; 05.12.2018