swiftaudiovideoavmutablecompositionmpmediapickercontroller

audio file plays fine but can't add in AVMutableVideoComposition


i'm trying to add audio file selected with MPMediaPickerController to video with AVMutableVideoComposition, but it gives error and doesn't work.

picking audio like this :

    func mediaPicker(_ mediaPicker: MPMediaPickerController, didPickMediaItems mediaItemCollection: MPMediaItemCollection) {
    
    if let musicUrl: NSURL = mediaItemCollection.items.first?.assetURL as NSURL? {
        musicURL = musicUrl as URL
        print("Music URL in did select \(musicUrl) !!!")
    }
    
}

and mixing audio and video like this :

func testMerge(url: URL) {
    
    let firstVideo = AVAsset(url: url)
    var audioAsset = AVAsset(url: musicURL!)
    
    print("Music URL\(musicURL)")

    let firstVideoTrack = firstVideo.tracks(withMediaType: AVMediaType.video).first
    
    let mixComposition = AVMutableComposition()
    
    let audioTrack = mixComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: Int32(kCMPersistentTrackID_Invalid))
    let firstTrack = mixComposition.addMutableTrack(withMediaType: .video, preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) 
    
    do {
        try firstTrack?.insertTimeRange(CMTimeRange(start: .zero, duration: firstVideo.duration), of: firstVideoTrack!, at: .zero)
        
        if musicURL.isFileURL {
            try audioTrack?.insertTimeRange(CMTimeRange(start: .zero, duration: firstVideo.duration), of: audioAsset.tracks(withMediaType: AVMediaType.audio).first, at: .zero)
        }
        
    } catch {
        debugPrint("Can't get track from the Video URL!")
    }
    
    let mainInstruction = AVMutableVideoCompositionInstruction()
    mainInstruction.timeRange = CMTimeRangeMake(start: .zero, duration: firstVideo.duration)
    
    let firstLayerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: firstTrack!)
        
    let scale = CGAffineTransform(scaleX: 1, y: 1)
    firstLayerInstruction.setTransform(scale, at: .zero)
    
    mainInstruction.layerInstructions = [firstLayerInstruction]
    
let mainCompositionInstruction = AVMutableVideoComposition()
mainCompositionInstruction.instructions = [mainInstruction]
mainCompositionInstruction.frameDuration = CMTimeMake(value: 1, timescale: 30)
mainCompositionInstruction.renderSize = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)

guard let documentDirectory = FileManager.default.urls (
    for: .documentDirectory,
    in: .userDomainMask).first else { print("ERROR"); return }

let url = documentDirectory.appendingPathComponent("mergeVideo-\(arc4random() % 10000).mp4")

guard let assetExport = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality) else {print("ERROR"); return }

assetExport.videoComposition = mainCompositionInstruction
assetExport.outputFileType = .mp4
assetExport.shouldOptimizeForNetworkUse = true
assetExport.outputURL = url
assetExport.exportAsynchronously {
    switch assetExport.status {
            case  AVAssetExportSessionStatus.failed:
                print("failed \(String(describing: assetExport.error))")
            case AVAssetExportSessionStatus.cancelled:
                print("cancelled \(String(describing: assetExport.error))")
            case AVAssetExportSessionStatus.completed:
                print("Completed")
                print(url)
                
            default:
                print("unknown")
            }
    
    DispatchQueue.main.async {
        self.exportDidFinish(assetExport)
    }
}
    
}

if i play that audio with AVAudioPlayer then it is playing but if i add in video it gives error {Error Domain=NSOSStatusErrorDomain Code=-12109 "(null)"}, NSLocalizedFailureReason=The operation is not supported for this media., NSLocalizedDescription=Operation Stopped}

any help or tip would be really appreciated, thank you.


Solution

  • If i add audio file to project directory and then access it with bundle url then it works fine it just doesn't work with audio picked through MPMediaPicker.

    This is how i was able to make it work :

    stored it as AVPlayerItem in variable like this

    var playerItem: AVPlayerItem!        
    
    func mediaPicker(_ mediaPicker: MPMediaPickerController, didPickMediaItems mediaItemCollection: MPMediaItemCollection) {
        
        if let musicUrl: NSURL = mediaItemCollection.items.first?.assetURL as NSURL? {
            musicURL = musicUrl as URL
            print("Music URL in did select \(musicUrl) !!!")
        }
        
        playerItem = AVPlayerItem(url: musicURL)
    }
    

    then in mergeVideo function accessed it's track like this

            do {
            try firstTrack?.insertTimeRange(CMTimeRange(start: .zero, duration: durationTrack.duration), of: firstVideoTrack!, at: .zero)
            try secondTrack?.insertTimeRange(CMTimeRange(start: .zero, duration: durationTrack.duration), of: secondVideoTrack!, at: .zero)
            
            if playerItem != nil {
                let sourceAudio = playerItem.asset.tracks(withMediaType: .audio).first
                
                let audioTrack = mixComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: Int32(kCMPersistentTrackID_Invalid))
                try audioTrack?.insertTimeRange(CMTimeRange(start: .zero, duration: durationTrack.duration), of: sourceAudio!, at: .zero)
            }
            
        } catch {
            debugPrint("Can't get track from the Video URL!")
        }