使用Swift将音频和视频文件合并成一个视频

27

我用Swift编写了一个程序。我想将视频与音频文件合并,但是出现了这个错误。

"failed Error Domain=AVFoundationErrorDomain Code=-11838 "Operation Stopped" UserInfo=0x17da4230 {NSLocalizedDescription=Operation Stopped, NSLocalizedFailureReason=The operation is not supported for this media.}"

代码

func mergeAudio(audioURL: NSURL, moviePathUrl: NSURL, savePathUrl: NSURL) {
    var composition = AVMutableComposition()
    let trackVideo:AVMutableCompositionTrack = composition.addMutableTrackWithMediaType(AVMediaTypeVideo, preferredTrackID: CMPersistentTrackID())
    let trackAudio:AVMutableCompositionTrack = composition.addMutableTrackWithMediaType(AVMediaTypeAudio, preferredTrackID: CMPersistentTrackID())
    let option = NSDictionary(object: true, forKey: "AVURLAssetPreferPreciseDurationAndTimingKey")
    let sourceAsset = AVURLAsset(URL: moviePathUrl, options: option as [NSObject : AnyObject])
    let audioAsset = AVURLAsset(URL: audioURL, options: option as [NSObject : AnyObject])

    let tracks = sourceAsset.tracksWithMediaType(AVMediaTypeVideo)
    let audios = audioAsset.tracksWithMediaType(AVMediaTypeAudio)

    if tracks.count > 0 {
        let assetTrack:AVAssetTrack = tracks[0] as! AVAssetTrack
        let assetTrackAudio:AVAssetTrack = audios[0] as! AVAssetTrack

        let audioDuration:CMTime = assetTrackAudio.timeRange.duration
        let audioSeconds:Float64 = CMTimeGetSeconds(assetTrackAudio.timeRange.duration)

        trackVideo.insertTimeRange(CMTimeRangeMake(kCMTimeZero,audioDuration), ofTrack: assetTrack, atTime: kCMTimeZero, error: nil)
        trackAudio.insertTimeRange(CMTimeRangeMake(kCMTimeZero,audioDuration), ofTrack: assetTrackAudio, atTime: kCMTimeZero, error: nil)
    }

    var assetExport: AVAssetExportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetPassthrough)
    assetExport.outputFileType = AVFileTypeMPEG4
    assetExport.outputURL = savePathUrl
    self.tmpMovieURL = savePathUrl
    assetExport.shouldOptimizeForNetworkUse = true
    assetExport.exportAsynchronouslyWithCompletionHandler { () -> Void in
        switch assetExport.status {
        case AVAssetExportSessionStatus.Completed:
            let assetsLib = ALAssetsLibrary()
            assetsLib.writeVideoAtPathToSavedPhotosAlbum(savePathUrl, completionBlock: nil)
            println("success")
        case  AVAssetExportSessionStatus.Failed:
            println("failed \(assetExport.error)")
        case AVAssetExportSessionStatus.Cancelled:
            println("cancelled \(assetExport.error)")
        default:
            println("complete")
        }
    }

}

在我看来,像mpeg4这样的媒体类型是错误的。 问题出在哪里?我错过了什么吗?


你解决了你的问题吗? - DShah
还没有。仍在寻找解决方案。 - Kei Maejima
你最终解决了这个问题吗? - user3344977
6个回答

31

改进代码(Govind的答案)并添加了一些额外的功能

  1. 合并视频的音频 + 外部音频(初始答案会丢失视频的声音)
  2. 水平翻转视频(如果需要的话,我个人在用户使用前置摄像头拍摄时使用它,顺便说一句Instagram也会翻转)
  3. 正确应用preferredTransform,解决了视频保存后旋转的问题(视频是外部的:由其他设备捕获/由其他应用程序生成)
  4. 删除了一些与VideoComposition无关的未使用的代码
  5. 为该方法添加了一个完成处理程序,以便可以从不同的类中调用
  6. 更新到Swift 4

步骤1。

import UIKit
import AVFoundation
import AVKit
import AssetsLibrary

第二步。
/// Merges video and sound while keeping sound of the video too
///
/// - Parameters:
///   - videoUrl: URL to video file
///   - audioUrl: URL to audio file
///   - shouldFlipHorizontally: pass True if video was recorded using frontal camera otherwise pass False
///   - completion: completion of saving: error or url with final video
func mergeVideoAndAudio(videoUrl: URL,
                        audioUrl: URL,
                        shouldFlipHorizontally: Bool = false,
                        completion: @escaping (_ error: Error?, _ url: URL?) -> Void) {

    let mixComposition = AVMutableComposition()
    var mutableCompositionVideoTrack = [AVMutableCompositionTrack]()
    var mutableCompositionAudioTrack = [AVMutableCompositionTrack]()
    var mutableCompositionAudioOfVideoTrack = [AVMutableCompositionTrack]()

    //start merge

    let aVideoAsset = AVAsset(url: videoUrl)
    let aAudioAsset = AVAsset(url: audioUrl)

    let compositionAddVideo = mixComposition.addMutableTrack(withMediaType: AVMediaType.video,
                                                             preferredTrackID: kCMPersistentTrackID_Invalid)!

    let compositionAddAudio = mixComposition.addMutableTrack(withMediaType: AVMediaType.audio,
                                                             preferredTrackID: kCMPersistentTrackID_Invalid)!

    let compositionAddAudioOfVideo = mixComposition.addMutableTrack(withMediaType: AVMediaType.audio,
                                                                    preferredTrackID: kCMPersistentTrackID_Invalid)!

    let aVideoAssetTrack: AVAssetTrack = aVideoAsset.tracks(withMediaType: AVMediaType.video)[0]
    let aAudioOfVideoAssetTrack: AVAssetTrack? = aVideoAsset.tracks(withMediaType: AVMediaType.audio).first
    let aAudioAssetTrack: AVAssetTrack = aAudioAsset.tracks(withMediaType: AVMediaType.audio)[0]

    // Default must have tranformation
    compositionAddVideo.preferredTransform = aVideoAssetTrack.preferredTransform

    if shouldFlipHorizontally {
        // Flip video horizontally
        var frontalTransform: CGAffineTransform = CGAffineTransform(scaleX: -1.0, y: 1.0)
        frontalTransform = frontalTransform.translatedBy(x: -aVideoAssetTrack.naturalSize.width, y: 0.0)
        frontalTransform = frontalTransform.translatedBy(x: 0.0, y: -aVideoAssetTrack.naturalSize.width)
        compositionAddVideo.preferredTransform = frontalTransform
    }

    mutableCompositionVideoTrack.append(compositionAddVideo)
    mutableCompositionAudioTrack.append(compositionAddAudio)
    mutableCompositionAudioOfVideoTrack.append(compositionAddAudioOfVideo)

    do {
        try mutableCompositionVideoTrack[0].insertTimeRange(CMTimeRangeMake(start: CMTime.zero,
                                                                            duration: aVideoAssetTrack.timeRange.duration),
                                                            of: aVideoAssetTrack,
                                                            at: CMTime.zero)

        //In my case my audio file is longer then video file so i took videoAsset duration
        //instead of audioAsset duration
        try mutableCompositionAudioTrack[0].insertTimeRange(CMTimeRangeMake(start: CMTime.zero,
                                                                            duration: aVideoAssetTrack.timeRange.duration),
                                                            of: aAudioAssetTrack,
                                                            at: CMTime.zero)

        // adding audio (of the video if exists) asset to the final composition
        if let aAudioOfVideoAssetTrack = aAudioOfVideoAssetTrack {
            try mutableCompositionAudioOfVideoTrack[0].insertTimeRange(CMTimeRangeMake(start: CMTime.zero,
                                                                                       duration: aVideoAssetTrack.timeRange.duration),
                                                                       of: aAudioOfVideoAssetTrack,
                                                                       at: CMTime.zero)
        }
    } catch {
        print(error.localizedDescription)
    }

    // Exporting
    let savePathUrl: URL = URL(fileURLWithPath: NSHomeDirectory() + "/Documents/newVideo.mp4")
    do { // delete old video
        try FileManager.default.removeItem(at: savePathUrl)
    } catch { print(error.localizedDescription) }

    let assetExport: AVAssetExportSession = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality)!
    assetExport.outputFileType = AVFileType.mp4
    assetExport.outputURL = savePathUrl
    assetExport.shouldOptimizeForNetworkUse = true

    assetExport.exportAsynchronously { () -> Void in
        switch assetExport.status {
        case AVAssetExportSession.Status.completed:
            print("success")
            completion(nil, savePathUrl)
        case AVAssetExportSession.Status.failed:
            print("failed \(assetExport.error?.localizedDescription ?? "error nil")")
            completion(assetExport.error, nil)
        case AVAssetExportSession.Status.cancelled:
            print("cancelled \(assetExport.error?.localizedDescription ?? "error nil")")
            completion(assetExport.error, nil)
        default:
            print("complete")
            completion(assetExport.error, nil)
        }
    }

}

再次感谢@Govind的回答!它对我帮助很大!
希望这个更新也能帮到其他人:)

如果我想要用外部音频文件完全替换视频中的原始音频,我需要进行哪些更改? - omarojo
@omarojo,我不确定在评论中回答这样一个大问题是否方便。请随意创建一个新问题,我会在那里回复。创建后,请在评论中发送链接给我。我会尽力帮助。 - Tung Fam
1
我已经弄清楚了,我只是在你的代码中注释掉了包含视频原始音频的行。 - omarojo
2
这个完美地运行了!感谢更新的函数和完成处理程序。 - Shalin Shah
非常有帮助!非常感谢。 - S.S.D
显示剩余5条评论

15

在上述问题中,我发现了同样的错误,原因是savePathUrl错了,目标URL应该像下面的代码一样包括新的视频名称。

我正在寻找合并音频和视频文件的代码,但无论在哪里都找不到,所以在阅读苹果文档几个小时后,我编写了这段代码。

注意:这是经过测试的100%工作的代码

步骤1: 在您的视图控制器中导入这些模块。

import UIKit
import AVFoundation
import AVKit
import AssetsLibrary

步骤 2: 将此函数添加到您的代码中

func mergeFilesWithUrl(videoUrl:NSURL, audioUrl:NSURL)
{
    let mixComposition : AVMutableComposition = AVMutableComposition()
    var mutableCompositionVideoTrack : [AVMutableCompositionTrack] = []
    var mutableCompositionAudioTrack : [AVMutableCompositionTrack] = []
    let totalVideoCompositionInstruction : AVMutableVideoCompositionInstruction = AVMutableVideoCompositionInstruction()


    //start merge

    let aVideoAsset : AVAsset = AVAsset(URL: videoUrl)
    let aAudioAsset : AVAsset = AVAsset(URL: audioUrl)

    mutableCompositionVideoTrack.append(mixComposition.addMutableTrackWithMediaType(AVMediaTypeVideo, preferredTrackID: kCMPersistentTrackID_Invalid))
    mutableCompositionAudioTrack.append( mixComposition.addMutableTrackWithMediaType(AVMediaTypeAudio, preferredTrackID: kCMPersistentTrackID_Invalid))

    let aVideoAssetTrack : AVAssetTrack = aVideoAsset.tracksWithMediaType(AVMediaTypeVideo)[0]
    let aAudioAssetTrack : AVAssetTrack = aAudioAsset.tracksWithMediaType(AVMediaTypeAudio)[0]



    do{
        try mutableCompositionVideoTrack[0].insertTimeRange(CMTimeRangeMake(kCMTimeZero, aVideoAssetTrack.timeRange.duration), ofTrack: aVideoAssetTrack, atTime: kCMTimeZero)

        //In my case my audio file is longer then video file so i took videoAsset duration
        //instead of audioAsset duration

        try mutableCompositionAudioTrack[0].insertTimeRange(CMTimeRangeMake(kCMTimeZero, aVideoAssetTrack.timeRange.duration), ofTrack: aAudioAssetTrack, atTime: kCMTimeZero)

        //Use this instead above line if your audiofile and video file's playing durations are same

        //            try mutableCompositionAudioTrack[0].insertTimeRange(CMTimeRangeMake(kCMTimeZero, aVideoAssetTrack.timeRange.duration), ofTrack: aAudioAssetTrack, atTime: kCMTimeZero)

    }catch{

    }

    totalVideoCompositionInstruction.timeRange = CMTimeRangeMake(kCMTimeZero,aVideoAssetTrack.timeRange.duration )

    let mutableVideoComposition : AVMutableVideoComposition = AVMutableVideoComposition()
    mutableVideoComposition.frameDuration = CMTimeMake(1, 30)

    mutableVideoComposition.renderSize = CGSizeMake(1280,720)

    //        playerItem = AVPlayerItem(asset: mixComposition)
    //        player = AVPlayer(playerItem: playerItem!)
    //
    //
    //        AVPlayerVC.player = player



    //find your video on this URl
    let savePathUrl : NSURL = NSURL(fileURLWithPath: NSHomeDirectory() + "/Documents/newVideo.mp4")

    let assetExport: AVAssetExportSession = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality)!
    assetExport.outputFileType = AVFileTypeMPEG4
    assetExport.outputURL = savePathUrl
    assetExport.shouldOptimizeForNetworkUse = true

    assetExport.exportAsynchronouslyWithCompletionHandler { () -> Void in
        switch assetExport.status {

        case AVAssetExportSessionStatus.Completed:

            //Uncomment this if u want to store your video in asset

            //let assetsLib = ALAssetsLibrary()
            //assetsLib.writeVideoAtPathToSavedPhotosAlbum(savePathUrl, completionBlock: nil)

            print("success")
        case  AVAssetExportSessionStatus.Failed:
            print("failed \(assetExport.error)")
        case AVAssetExportSessionStatus.Cancelled:
            print("cancelled \(assetExport.error)")
        default:
            print("complete")
        }
    }


}

步骤3:在您想要的位置调用函数,如下所示

let videoUrl : NSURL =  NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("SampleVideo", ofType: "mp4")!)
let audioUrl : NSURL = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("SampleAudio", ofType: "mp3")!)

mergeFilesWithUrl(videoUrl, audioUrl: audioUrl)

希望这可以对你有所帮助,节省你的时间。


1
如果录制的声音格式为AVFileTypeAppleM4A,则它无法正常工作,有什么建议吗? - Dumitru Rogojinaru
如果我添加自定义音频,视频的原始音频将被删除。我想要保留原始视频的声音,并添加我的自定义声音。你能帮我吗? - parth
抱歉朋友,我只有以上的代码,没有时间再深入探索了。 - Govind Prajapati
1
@parth 嘿,老兄,我找到了一个解决方案。看看这个链接 https://dev59.com/26Pia4cB1Zd3GeqP3cEw#45327843 - Faruk
与'mutableVideoComposition'相关的三行是无用的,应该删除。 - rommex
@DumitruRogojinaru,关于M4A格式你找到解决方案了吗? - rommex

8

Swift 4.2 / 5

func mergeVideoWithAudio(videoUrl: URL, audioUrl: URL, success: @escaping ((URL) -> Void), failure: @escaping ((Error?) -> Void)) {


    let mixComposition: AVMutableComposition = AVMutableComposition()
    var mutableCompositionVideoTrack: [AVMutableCompositionTrack] = []
    var mutableCompositionAudioTrack: [AVMutableCompositionTrack] = []
    let totalVideoCompositionInstruction : AVMutableVideoCompositionInstruction = AVMutableVideoCompositionInstruction()

    let aVideoAsset: AVAsset = AVAsset(url: videoUrl)
    let aAudioAsset: AVAsset = AVAsset(url: audioUrl)

    if let videoTrack = mixComposition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid), let audioTrack = mixComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) {
        mutableCompositionVideoTrack.append(videoTrack)
        mutableCompositionAudioTrack.append(audioTrack)

    if let aVideoAssetTrack: AVAssetTrack = aVideoAsset.tracks(withMediaType: .video).first, let aAudioAssetTrack: AVAssetTrack = aAudioAsset.tracks(withMediaType: .audio).first {
        do {
            try mutableCompositionVideoTrack.first?.insertTimeRange(CMTimeRangeMake(start: CMTime.zero, duration: aVideoAssetTrack.timeRange.duration), of: aVideoAssetTrack, at: CMTime.zero)
            try mutableCompositionAudioTrack.first?.insertTimeRange(CMTimeRangeMake(start: CMTime.zero, duration: aVideoAssetTrack.timeRange.duration), of: aAudioAssetTrack, at: CMTime.zero)
               videoTrack.preferredTransform = aVideoAssetTrack.preferredTransform 

        } catch{
            print(error)
        }


       totalVideoCompositionInstruction.timeRange = CMTimeRangeMake(start: CMTime.zero,duration: aVideoAssetTrack.timeRange.duration)
    }
    }

    let mutableVideoComposition: AVMutableVideoComposition = AVMutableVideoComposition()
    mutableVideoComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
    mutableVideoComposition.renderSize = CGSize(width: 480, height: 640)

    if let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first {
        let outputURL = URL(fileURLWithPath: documentsPath).appendingPathComponent("\("fileName").m4v")

        do {
            if FileManager.default.fileExists(atPath: outputURL.path) {

                try FileManager.default.removeItem(at: outputURL)
            }
        } catch { }

        if let exportSession = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality) {
            exportSession.outputURL = outputURL
            exportSession.outputFileType = AVFileType.mp4
            exportSession.shouldOptimizeForNetworkUse = true

            /// try to export the file and handle the status cases
            exportSession.exportAsynchronously(completionHandler: {
                switch exportSession.status {
                case .failed:
                    if let _error = exportSession.error {
                        failure(_error)
                    }

                case .cancelled:
                    if let _error = exportSession.error {
                        failure(_error)
                    }

                default:
                    print("finished")
                    success(outputURL)
                }
            })
        } else {
            failure(nil)
        }
    }
}

1
我能合并两个wav文件吗? - Paresh. P

3

新版本Swift3具备URL和新语法。

func mergeFilesWithUrl(videoUrl:URL, audioUrl:URL)
{
    let mixComposition : AVMutableComposition = AVMutableComposition()
    var mutableCompositionVideoTrack : [AVMutableCompositionTrack] = []
    var mutableCompositionAudioTrack : [AVMutableCompositionTrack] = []
    let totalVideoCompositionInstruction : AVMutableVideoCompositionInstruction = AVMutableVideoCompositionInstruction()


    //start merge

    let aVideoAsset : AVAsset = AVAsset(url: videoUrl)
    let aAudioAsset : AVAsset = AVAsset(url: audioUrl)

    mutableCompositionVideoTrack.append(mixComposition.addMutableTrack(withMediaType: AVMediaTypeVideo, preferredTrackID: kCMPersistentTrackID_Invalid))
    mutableCompositionAudioTrack.append( mixComposition.addMutableTrack(withMediaType: AVMediaTypeAudio, preferredTrackID: kCMPersistentTrackID_Invalid))

    let aVideoAssetTrack : AVAssetTrack = aVideoAsset.tracks(withMediaType: AVMediaTypeVideo)[0]
    let aAudioAssetTrack : AVAssetTrack = aAudioAsset.tracks(withMediaType: AVMediaTypeAudio)[0]



    do{
        try mutableCompositionVideoTrack[0].insertTimeRange(CMTimeRangeMake(kCMTimeZero, aVideoAssetTrack.timeRange.duration), of: aVideoAssetTrack, at: kCMTimeZero)

        //In my case my audio file is longer then video file so i took videoAsset duration
        //instead of audioAsset duration

        try mutableCompositionAudioTrack[0].insertTimeRange(CMTimeRangeMake(kCMTimeZero, aVideoAssetTrack.timeRange.duration), of: aAudioAssetTrack, at: kCMTimeZero)

        //Use this instead above line if your audiofile and video file's playing durations are same

        //            try mutableCompositionAudioTrack[0].insertTimeRange(CMTimeRangeMake(kCMTimeZero, aVideoAssetTrack.timeRange.duration), ofTrack: aAudioAssetTrack, atTime: kCMTimeZero)

    }catch{

    }

    totalVideoCompositionInstruction.timeRange = CMTimeRangeMake(kCMTimeZero,aVideoAssetTrack.timeRange.duration )

    let mutableVideoComposition : AVMutableVideoComposition = AVMutableVideoComposition()
    mutableVideoComposition.frameDuration = CMTimeMake(1, 30)

    mutableVideoComposition.renderSize = CGSize(width: 1280, height: 720)
    //        playerItem = AVPlayerItem(asset: mixComposition)
    //        player = AVPlayer(playerItem: playerItem!)
    //
    //
    //        AVPlayerVC.player = player

    //find your video on this URl
    let savePathUrl : URL = URL(fileURLWithPath: NSHomeDirectory() + "/Documents/newVideo.mp4")

    let assetExport: AVAssetExportSession = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality)!
    assetExport.outputFileType = AVFileTypeMPEG4
    assetExport.outputURL = savePathUrl
    assetExport.shouldOptimizeForNetworkUse = true

    assetExport.exportAsynchronously { () -> Void in
        switch assetExport.status {

        case AVAssetExportSessionStatus.completed:

            //Uncomment this if u want to store your video in asset

            //let assetsLib = ALAssetsLibrary()
            //assetsLib.writeVideoAtPathToSavedPhotosAlbum(savePathUrl, completionBlock: nil)

            print("success")
        case  AVAssetExportSessionStatus.failed:
            print("failed \(assetExport.error)")
        case AVAssetExportSessionStatus.cancelled:
            print("cancelled \(assetExport.error)")
        default:
            print("complete")
        }
    }
}

有没有办法合并两个WAV文件?我在assetExport?.exportAsynchronously(completionHandler: { [weak self] in})时遇到了崩溃问题。 - Paresh. P

2

Swift 5版本(如果视频大于音频,则重复音频):只需传递音频和视频URL。我已经尝试了本地视频和远程音频URL。

func mergeVideoWithAudio(videoUrl: URL,
                                audioUrl: URL,
                                success: @escaping ((URL) -> Void),
                                failure: @escaping ((Error?) -> Void)) {

       let mixComposition: AVMutableComposition = AVMutableComposition()
       var mutableCompositionVideoTrack: [AVMutableCompositionTrack] = []
       var mutableCompositionAudioTrack: [AVMutableCompositionTrack] = []
       let totalVideoCompositionInstruction: AVMutableVideoCompositionInstruction = AVMutableVideoCompositionInstruction()

       let aVideoAsset: AVAsset = AVAsset(url: videoUrl)
       let aAudioAsset: AVAsset = AVAsset(url: audioUrl)

       if let videoTrack = mixComposition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid), let audioTrack = mixComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) {
           mutableCompositionVideoTrack.append( videoTrack )
           mutableCompositionAudioTrack.append( audioTrack )

           if let aVideoAssetTrack: AVAssetTrack = aVideoAsset.tracks(withMediaType: .video).first, let aAudioAssetTrack: AVAssetTrack = aAudioAsset.tracks(withMediaType: .audio).first {
               do {
                   try mutableCompositionVideoTrack.first?.insertTimeRange(CMTimeRangeMake(start: CMTime.zero, duration: aVideoAssetTrack.timeRange.duration), of: aVideoAssetTrack, at: CMTime.zero)

                   let videoDuration = aVideoAsset.duration
                   if CMTimeCompare(videoDuration, aAudioAsset.duration) == -1 {
                       try mutableCompositionAudioTrack.first?.insertTimeRange(CMTimeRangeMake(start: CMTime.zero, duration: aVideoAssetTrack.timeRange.duration), of: aAudioAssetTrack, at: CMTime.zero)
                   } else if CMTimeCompare(videoDuration, aAudioAsset.duration) == 1 {
                       var currentTime = CMTime.zero
                       while true {
                           var audioDuration = aAudioAsset.duration
                           let totalDuration = CMTimeAdd(currentTime, audioDuration)
                           if CMTimeCompare(totalDuration, videoDuration) == 1 {
                               audioDuration = CMTimeSubtract(totalDuration, videoDuration)
                           }
                           try mutableCompositionAudioTrack.first?.insertTimeRange(CMTimeRangeMake(start: CMTime.zero, duration: aVideoAssetTrack.timeRange.duration), of: aAudioAssetTrack, at: currentTime)

                           currentTime = CMTimeAdd(currentTime, audioDuration)
                           if CMTimeCompare(currentTime, videoDuration) == 1 || CMTimeCompare(currentTime, videoDuration) == 0 {
                               break
                           }
                       }
                   }
                   videoTrack.preferredTransform = aVideoAssetTrack.preferredTransform

               } catch {
                   print(error)
               }

               totalVideoCompositionInstruction.timeRange = CMTimeRangeMake(start: CMTime.zero, duration: aVideoAssetTrack.timeRange.duration)
           }
       }

       let mutableVideoComposition: AVMutableVideoComposition = AVMutableVideoComposition()
       mutableVideoComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
       mutableVideoComposition.renderSize = CGSize(width: 480, height: 640)

       if let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first {
           let outputURL = URL(fileURLWithPath: documentsPath).appendingPathComponent("\("fileName").m4v")

           do {
               if FileManager.default.fileExists(atPath: outputURL.path) {

                   try FileManager.default.removeItem(at: outputURL)
               }
           } catch { }

           if let exportSession = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality) {
               exportSession.outputURL = outputURL
               exportSession.outputFileType = AVFileType.mp4
               exportSession.shouldOptimizeForNetworkUse = true

               // try to export the file and handle the status cases
               exportSession.exportAsynchronously(completionHandler: {
                   switch exportSession.status {
                   case .failed:
                       if let error = exportSession.error {
                           failure(error)
                       }

                   case .cancelled:
                       if let error = exportSession.error {
                           failure(error)
                       }

                   default:
                       print("finished")
                       success(outputURL)
                   }
               })
           } else {
               failure(nil)
           }
       }
   }

0

已更新至 Swift 并发编程(Swift 5.7)

一些需要抛出的错误:

enum VideoAudioMergeError: Error {
    case compositionAddVideoFailed, compositionAddAudioFailed, compositionAddAudioOfVideoFailed, unknownError
}

还有这个方法:

/// Merges video and sound while keeping sound of the video too
///
/// - Parameters:
///   - videoUrl: URL to video file
///   - audioUrl: URL to audio file
///   - shouldFlipHorizontally: pass True if video was recorded using frontal camera otherwise pass False
func mergeVideoAndAudio(videoUrl: URL,
                        audioUrl: URL,
                        shouldFlipHorizontally: Bool = false) async throws -> URL {

    let mixComposition = AVMutableComposition()
    var mutableCompositionVideoTrack = [AVMutableCompositionTrack]()
    var mutableCompositionAudioTrack = [AVMutableCompositionTrack]()
    var mutableCompositionAudioOfVideoTrack = [AVMutableCompositionTrack]()

    //start merge

    let aVideoAsset = AVAsset(url: videoUrl)
    let aAudioAsset = AVAsset(url: audioUrl)

    guard let compositionAddVideo = mixComposition.addMutableTrack(withMediaType: AVMediaType.video,
                                                                   preferredTrackID: kCMPersistentTrackID_Invalid) else {
        throw VideoAudioMergeError.compositionAddVideoFailed
    }

    guard let compositionAddAudio = mixComposition.addMutableTrack(withMediaType: AVMediaType.audio,
                                                                   preferredTrackID: kCMPersistentTrackID_Invalid) else {
        throw VideoAudioMergeError.compositionAddAudioFailed
    }

    guard let compositionAddAudioOfVideo = mixComposition.addMutableTrack(withMediaType: AVMediaType.audio,
                                                                          preferredTrackID: kCMPersistentTrackID_Invalid) else {
        throw VideoAudioMergeError.compositionAddAudioOfVideoFailed
    }

    do {
        let aVideoAssetTrack: AVAssetTrack = try await aVideoAsset.loadTracks(withMediaType: AVMediaType.video)[0]
        let aAudioOfVideoAssetTrack: AVAssetTrack? = try await  aVideoAsset.loadTracks(withMediaType: AVMediaType.audio).first
        let aAudioAssetTrack: AVAssetTrack = try await aAudioAsset.loadTracks(withMediaType: AVMediaType.audio)[0]

        // Default must have transformation
        compositionAddVideo.preferredTransform = try await aVideoAssetTrack.load(.preferredTransform)

        if shouldFlipHorizontally {
            // Flip video horizontally
            var frontalTransform: CGAffineTransform = CGAffineTransform(scaleX: -1.0, y: 1.0)
            let naturalSize = try await aVideoAssetTrack.load(.naturalSize)
            frontalTransform = frontalTransform.translatedBy(x: -naturalSize.width, y: 0.0)
            frontalTransform = frontalTransform.translatedBy(x: 0.0, y: -naturalSize.width)
            compositionAddVideo.preferredTransform = frontalTransform
        }

        mutableCompositionVideoTrack.append(compositionAddVideo)
        mutableCompositionAudioTrack.append(compositionAddAudio)
        mutableCompositionAudioOfVideoTrack.append(compositionAddAudioOfVideo)
        
        let videoTimeRange = try await aVideoAssetTrack.load(.timeRange)

        try mutableCompositionVideoTrack[0].insertTimeRange(CMTimeRangeMake(start: CMTime.zero,
                                                                            duration: videoTimeRange.duration),
                                                            of: aVideoAssetTrack,
                                                            at: CMTime.zero)

        //In my case my audio file is longer then video file so i took videoAsset duration
        //instead of audioAsset duration
        try mutableCompositionAudioTrack[0].insertTimeRange(CMTimeRangeMake(start: CMTime.zero,
                                                                            duration: videoTimeRange.duration),
                                                            of: aAudioAssetTrack,
                                                            at: CMTime.zero)

        // adding audio (of the video if exists) asset to the final composition
        if let aAudioOfVideoAssetTrack = aAudioOfVideoAssetTrack {
            try mutableCompositionAudioOfVideoTrack[0].insertTimeRange(CMTimeRangeMake(start: CMTime.zero,
                                                                                       duration: videoTimeRange.duration),
                                                                       of: aAudioOfVideoAssetTrack,
                                                                       at: CMTime.zero)
        }
    } catch {
        throw error
    }

    // Exporting
    let savePathUrl: URL = URL(fileURLWithPath: NSHomeDirectory() + "/Documents/newVideo.mp4")
    do { // delete old video
        try FileManager.default.removeItem(at: savePathUrl)
    } catch { print(error.localizedDescription) }

    let assetExport: AVAssetExportSession = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality)!
    assetExport.outputFileType = AVFileType.mp4
    assetExport.outputURL = savePathUrl
    assetExport.shouldOptimizeForNetworkUse = true

    await assetExport.export()
    
    if assetExport.status == .completed {
        return savePathUrl
    }
    
    if let error = assetExport.error {
        throw error
    } else {
        throw VideoAudioMergeError.unknownError
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接