Exporting mp4 through AVAssetExportSession fails

18.5k views Asked by At

I start saying that I spent a lot of time searching through documentation, posts here and somewhere else, but I can't figure out the solution for this problem.

I'm using AVAssetExportSession for exporting an .mp4 file stored in a AVAsset instance. What I do is:

  • I check the isExportable property of AVAsset
  • I then get an array of exportPresets compatible with the AVAsset instance
  • I take the AVAssetExportPreset1920x1080, or, if not existing I try to export the media with AVAssetExportPresetPassthrough (FYI, 100% of times, the preset I need is always contained in the list, but I tried also the passthrough option and it doesn't work anyway)

The outputFileType is AVFileTypeMPEG4 and I tried also by assigning the .mp4 extension to the file, but nothing makes it work. I always receive this error

Error Domain=AVFoundationErrorDomain Code=-11838 "Operation Stopped" UserInfo={NSUnderlyingError=0x600000658c30 {Error Domain=NSOSStatusErrorDomain Code=-12109 "(null)"}, NSLocalizedFailureReason=The operation is not supported for this media., NSLocalizedDescription=Operation Stopped}

Below is the code I'm using

func _getDataFor(_ item: AVPlayerItem, completion: @escaping (Data?) -> ()) {
    guard item.asset.isExportable else {
        completion(nil)
        return
    }

    let compatiblePresets = AVAssetExportSession.exportPresets(compatibleWith: item.asset)
    var preset: String = AVAssetExportPresetPassthrough
    if compatiblePresets.contains(AVAssetExportPreset1920x1080) { preset = AVAssetExportPreset1920x1080 }

    guard
        let exportSession = AVAssetExportSession(asset: item.asset, presetName: preset),
        exportSession.supportedFileTypes.contains(AVFileTypeMPEG4) else {
        completion(nil)
        return
    }

    var tempFileUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("temp_video_data.mp4", isDirectory: false)
    tempFileUrl = URL(fileURLWithPath: tempFileUrl.path)

    exportSession.outputURL = tempFileUrl
    exportSession.outputFileType = AVFileTypeMPEG4
    let startTime = CMTimeMake(0, 1)
    let timeRange = CMTimeRangeMake(startTime, item.duration)
    exportSession.timeRange = timeRange

    exportSession.exportAsynchronously {
        print("\(exportSession.error)")
        let data = try? Data(contentsOf: tempFileUrl)
        _ = try? FileManager.default.removeItem(at: tempFileUrl)
        completion(data)
    }
}
7

There are 7 answers

2
Luca D'Alberti On BEST ANSWER

Seems like converting the AVAsset instance in a AVMutableComposition did the trick. If, please, anyone knows the reason why this works let me know.

This is the new _getDataFor(_:completion:) method implementation

func _getDataFor(_ item: AVPlayerItem, completion: @escaping (Data?) -> ()) {
    guard item.asset.isExportable else {
        completion(nil)
        return
    }

    let composition = AVMutableComposition()
    let compositionVideoTrack = composition.addMutableTrack(withMediaType: AVMediaTypeVideo, preferredTrackID: CMPersistentTrackID(kCMPersistentTrackID_Invalid))
    let compositionAudioTrack = composition.addMutableTrack(withMediaType: AVMediaTypeAudio, preferredTrackID: CMPersistentTrackID(kCMPersistentTrackID_Invalid))

    let sourceVideoTrack = item.asset.tracks(withMediaType: AVMediaTypeVideo).first!
    let sourceAudioTrack = item.asset.tracks(withMediaType: AVMediaTypeAudio).first!
    do {
        try compositionVideoTrack.insertTimeRange(CMTimeRangeMake(kCMTimeZero, item.duration), of: sourceVideoTrack, at: kCMTimeZero)
        try compositionAudioTrack.insertTimeRange(CMTimeRangeMake(kCMTimeZero, item.duration), of: sourceAudioTrack, at: kCMTimeZero)
    } catch(_) {
        completion(nil)
        return
    }

    let compatiblePresets = AVAssetExportSession.exportPresets(compatibleWith: composition)
    var preset: String = AVAssetExportPresetPassthrough
    if compatiblePresets.contains(AVAssetExportPreset1920x1080) { preset = AVAssetExportPreset1920x1080 }

    guard
        let exportSession = AVAssetExportSession(asset: composition, presetName: preset),
        exportSession.supportedFileTypes.contains(AVFileTypeMPEG4) else {
        completion(nil)
        return
    }

    var tempFileUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("temp_video_data.mp4", isDirectory: false)
    tempFileUrl = URL(fileURLWithPath: tempFileUrl.path)

    exportSession.outputURL = tempFileUrl
    exportSession.outputFileType = AVFileTypeMPEG4
    let startTime = CMTimeMake(0, 1)
    let timeRange = CMTimeRangeMake(startTime, item.duration)
    exportSession.timeRange = timeRange

    exportSession.exportAsynchronously {
        print("\(tempFileUrl)")
        print("\(exportSession.error)")
        let data = try? Data(contentsOf: tempFileUrl)
        _ = try? FileManager.default.removeItem(at: tempFileUrl)
        completion(data)
    }
}
0
Sajjad Sarkoobi On

Swift 5:

import Foundation
import AVKit

func getDataFor(_ asset: AVAsset, completion: @escaping (Data?) -> ()) {
    
    guard asset.isExportable,
          let sourceVideoTrack = asset.tracks(withMediaType: .video).first,
          let sourceAudioTrack = asset.tracks(withMediaType: .audio).first else {
              completion(nil)
              return
          }
    
    let composition = AVMutableComposition()
    let compositionVideoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: CMPersistentTrackID(kCMPersistentTrackID_Invalid))
    let compositionAudioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: CMPersistentTrackID(kCMPersistentTrackID_Invalid))
            
    do {
        try compositionVideoTrack?.insertTimeRange(CMTimeRangeMake(start: .zero, duration: asset.duration), of: sourceVideoTrack, at: .zero)
        try compositionAudioTrack?.insertTimeRange(CMTimeRangeMake(start: .zero, duration: asset.duration), of: sourceAudioTrack, at: .zero)
    } catch {
        completion(nil)
        return
    }
    
    let compatiblePresets = AVAssetExportSession.exportPresets(compatibleWith: composition)
    var preset = AVAssetExportPresetPassthrough
    let preferredPreset = AVAssetExportPreset1920x1080
    if compatiblePresets.contains(preferredPreset) {
        preset = preferredPreset
    }
    
    let fileType: AVFileType = .mp4

    guard let exportSession = AVAssetExportSession(asset: composition, presetName: preset),
          exportSession.supportedFileTypes.contains(fileType) else {
              completion(nil)
              return
          }
    
    let tempFileUrl = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("temp_video_data")
    
    exportSession.outputURL = tempFileUrl
    exportSession.outputFileType = fileType
    let startTime = CMTimeMake(value: 0, timescale: 1)
    let timeRange = CMTimeRangeMake(start: startTime, duration: asset.duration)
    exportSession.timeRange = timeRange
    
    exportSession.exportAsynchronously {
        print(tempFileUrl)
        print(String(describing: exportSession.error))
        let data = try? Data(contentsOf: tempFileUrl)
        try? FileManager.default.removeItem(at: tempFileUrl)
        completion(data)
    }
}
0
黄水胜 On

I solved this problem by removing the CompositionTrack with media type .audio and empty segments from the AVMutableComposition

if let audioTrack = exportComposition.tracks(withMediaType: .audio).first,
    audioTrack.segments.isEmpty {
        exportComposition.removeTrack(audioTrack)
}
1
PatPatchPatrick On

I had this same issue because I was adding an audio track to a video without audio. Removing the audio track fixed it.

4
panychyk.dima On

Check if you set delegate property for AVURLAsset correctly.

[self.playerAsset.resourceLoader setDelegate:self queue:dispatch_get_main_queue()];

And conform to AVAssetResourceLoaderDelegate protocol. That is all you need to do.

0
DevSemenuk On

I had the same error, when I try to ExportSession with AVAssetExportPresetPassthrough always fail and in my case I can't use another preset because I must have the same resolution like at origin video

I fixed

let asset = AVAsset(url: originUrl)
let videoTrack = asset.tracks( withMediaType: .video ).first! as AVAssetTrack

let videoComposition = AVMutableVideoComposition()
videoComposition.renderSize = CGSize(
       width: videoTrack.naturalSize.width, 
       height: videoTrack.naturalSize.height
)
videoComposition.frameDuration = CMTime(
       value: 1, 
       timescale: videoTrack.naturalTimeScale
)
        
let transformer = AVMutableVideoCompositionLayerInstruction(
       assetTrack: videoTrack 
)
transformer.setOpacity(1.0, at: CMTime.zero)
        
let instruction = AVMutableVideoCompositionInstruction()
instruction.timeRange = timeRange
instruction.layerInstructions = [transformer]
videoComposition.instructions = [instruction]

guard let exportSession = AVAssetExportSession(
       asset: asset, 
       presetName: AVAssetExportPresetMediumQuality
) else {
       return handleFailure(error: .mediaSavingError, completion: completion)
}
        
exportSession.videoComposition = videoComposition
exportSession.outputURL = outputUrl
exportSession.outputFileType = .mp4
exportSession.timeRange = timeRange

exportSession.exportAsynchronously { [weak self] in 
    "your code"
}

Forks great for me, and it's saved the same resolution as video before

0
Lance Samaria On

I ran into this problem because the Microphone permission was off/denied. Once I set turned it on this error went away.