Exporting mp4 through AVAssetExportSession fails
Asked Answered
C

7

12

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)
    }
}
Constringent answered 9/1, 2017 at 9:4 Comment(0)
C
26

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)
    }
}
Constringent answered 9/1, 2017 at 10:12 Comment(2)
Hi @Dincer, I'm sorry, I didn't try it on iOS11 so far, I stopped to work on that projectFreund
Yes, this is very strange but using a composition solves the problem.Perorate
C
5

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)
    }
}
Callen answered 22/10, 2020 at 9:45 Comment(0)
E
4

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.

Exploration answered 23/12, 2017 at 17:55 Comment(4)
AVURLAsset.resourceLoader should be unrelated to AVAssetExportSession (which works with any AVAsset, not just AVURLAsset)Unconditioned
Although this makes zero sense, this is literally all you have to do. Set the delegate on the asset resource loader to self and conform to the protocol. You do not even have to implement any methods or anything.Lambert
Do not know why it work for me, but in my case exportAsynchronously method got failed until I set up delegate and on main queue also important thing. Maybe some version bugExploration
@panychyk.dima, It worked for me as well. I used it in swift like this: asset.resourceLoader.setDelegate(self, queue: .main)Sphery
C
4

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

Colly answered 24/9, 2020 at 21:39 Comment(1)
#56729961Wichita
H
1

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)
}
Hienhieracosphinx answered 4/12, 2020 at 9:14 Comment(0)
P
0

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

Pignut answered 4/2, 2020 at 9:48 Comment(0)
T
0

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

Twirp answered 9/12, 2021 at 17:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.