2

I have been struggling with adding text subtitles to videos for a while. I have added some links that I referred in detail, but they are not helping.

In below code, I am trying to add a subtitle to a video. The output file path is as below:

file:///var/mobile/Applications/03E49B29-1070-4541-B7CB-B1366732C179/Documents/output_movie.mov

In addition, the input file was recorded with a call to UIPickerView in the same application, at temporary path below:

file:///private/var/mobile/Applications/03E49B29-1070-4541-B7CB-B1366732C179/tmp/capture/capturedvideo.MOV

The error I am getting is as follows,

Error:-
Domain=AVFoundationErrorDomain Code=-11841 "Operation Stopped" UserInfo=0x15ebcfb0 {NSLocalizedDescription=Operation Stopped, NSLocalizedFailureReason=The video could not be composed.}

Description:-
<AVAssetExportSession: 0x15d97c80, asset = <AVMutableComposition: 0x15d788d0 tracks = ("<AVMutableCompositionTrack: 0x15d86910 trackID = 1, mediaType = vide, editCount = 1>")>, presetName = AVAssetExportPresetHighestQuality, outputFileType = com.apple.quicktime-movie

Completed merging the video with status code 4

The code I am using is as below. I am running it on iOS 7.1.2 on an iPhone 4s.

class func mergeVideoWithTheme(outputUrl: NSURL, inputVideoUrl videoUrl: NSURL!, onComplete completionHandler: ((Int) -> ())!) -> Void {

    // 1. mergeComposition adds all the AVAssets

    var mergeComposition : AVMutableComposition = AVMutableComposition()
    var trackVideo : AVMutableCompositionTrack = mergeComposition.addMutableTrackWithMediaType(AVMediaTypeVideo, preferredTrackID: CMPersistentTrackID())
    //var trackAudio : AVMutableCompositionTrack = mergeComposition.addMutableTrackWithMediaType(AVMediaTypeAudio, preferredTrackID: CMPersistentTrackID())

    // 2. Add a bank for theme insertion later

    //trackVideo.insertTimeRange(range, ofTrack: VideoHelper.Static.blankTrack, atTime: kCMTimeZero, error: nil)

    // 3. Source tracks

    let sourceAsset = AVURLAsset(URL: videoUrl, options: nil)
    let sourceDuration = CMTimeRangeMake(kCMTimeZero, sourceAsset.duration)
    let vtrack: AVAssetTrack? = sourceAsset.tracksWithMediaType(AVMediaTypeVideo)[0] as? AVAssetTrack
    let atrack: AVAssetTrack? = sourceAsset.tracksWithMediaType(AVMediaTypeAudio)[0] as? AVAssetTrack

    if (vtrack == nil) {
        return
    }

    let renderWidth = vtrack?.naturalSize.width
    let renderHeight = vtrack?.naturalSize.height
    let insertTime = kCMTimeZero
    let endTime = sourceAsset.duration
    let range = sourceDuration

    // append tracks

    trackVideo.insertTimeRange(sourceDuration, ofTrack: vtrack, atTime: insertTime, error: nil)
    //if(atrack > 0){
    //    trackAudio.insertTimeRange(sourceDuration, ofTrack: atracks[0] as AVAssetTrack, atTime: insertTime, error: nil)
    //}

    // 4. Add subtitles (we call it theme)

    var themeVideoComposition : AVMutableVideoComposition = AVMutableVideoComposition(propertiesOfAsset: sourceAsset)

    // 4.1 - Create AVMutableVideoCompositionInstruction

    let mainInstruction: AVMutableVideoCompositionInstruction = AVMutableVideoCompositionInstruction()
    mainInstruction.timeRange = range

    // 4.2 - Create an AVMutableVideoCompositionLayerInstruction for the video track and fix the orientation.

    let videolayerInstruction : AVMutableVideoCompositionLayerInstruction = AVMutableVideoCompositionLayerInstruction()
    videolayerInstruction.setTransform(trackVideo.preferredTransform, atTime: insertTime)
    videolayerInstruction.setOpacity(0.0, atTime: endTime)

    // 4.3 - Add instructions

    mainInstruction.layerInstructions = NSArray(array: [videolayerInstruction])

    themeVideoComposition.renderScale = 1.0
    themeVideoComposition.renderSize = CGSizeMake(renderWidth!, renderHeight!)
    themeVideoComposition.frameDuration = CMTimeMake(1, 30)
    themeVideoComposition.instructions = NSArray(array: [mainInstruction])

    // add the theme

    // setup variables

    // add text

    let title = String("Testing this subtitle")

    var titleLayer = CATextLayer()
    titleLayer.string = title
    titleLayer.frame =  CGRect(x: 0, y: 0, width: renderWidth!, height: renderHeight!)
    let fontName: CFStringRef = "Helvetica-Bold"
    let fontSize = CGFloat(36)
    titleLayer.font = CTFontCreateWithName(fontName, fontSize, nil)
    titleLayer.alignmentMode = kCAAlignmentCenter
    titleLayer.foregroundColor = UIColor.whiteColor().CGColor

    var backgroundLayer = CALayer()
    backgroundLayer.frame = CGRect(x: 0, y: 0, width: renderWidth!, height: renderHeight!)
    backgroundLayer.masksToBounds = true
    backgroundLayer.addSublayer(titleLayer)

    // 2. set parent layer and video layer

    var parentLayer = CALayer()
    var videoLayer = CALayer()
    parentLayer.frame =  CGRect(x: 0, y: 0, width: renderWidth!, height: renderHeight!)
    videoLayer.frame =  CGRect(x: 0, y: 0, width: renderWidth!, height: renderHeight!)

    parentLayer.addSublayer(backgroundLayer)
    parentLayer.addSublayer(videoLayer)

    // 3. make animation

    themeVideoComposition.animationTool = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer: videoLayer, inLayer: parentLayer)

    // Remove the file if it already exists (merger does not overwrite)

    let fileManager = NSFileManager.defaultManager()
    fileManager.removeItemAtURL(outputUrl, error: nil)

    // export to output url

    var exporter = AVAssetExportSession(asset: mergeComposition, presetName: AVAssetExportPresetHighestQuality)
    exporter.outputURL = outputUrl
    exporter.videoComposition = themeVideoComposition
    exporter.outputFileType = AVFileTypeQuickTimeMovie
    exporter.shouldOptimizeForNetworkUse = true
    exporter.exportAsynchronouslyWithCompletionHandler({
        if (exporter.error != nil) {
            println("Error")
            println(exporter.error)
            println("Description")
            println(exporter.description)
        }
        completionHandler(exporter.status.rawValue)
    })
}

I would greatly appreciate your help. I did not find any swift examples of adding animations to the videos. Wondering if it worked for anyone on swift.

References : (1) https://gist.github.com/SheffieldKevin/c01789ccff2b2a87f5ea (2) http://www.raywenderlich.com/30200/avfoundation-tutorial-adding-overlays-and-animations-to-videos

navalsaini
  • 522
  • 6
  • 20
  • I didn't know how to accept answers on SO (just did). But this is years old. – navalsaini Jun 19 '18 at 15:21
  • Sorry, the point I was making is, looking over your past questions, you might want to go through them now that you _do_ know how to accept answers, and accept some. It closes the cycle on the question, and you get rep. Most important, acceptance is the "coin of the realm" on Stack Overflow. – matt Jun 19 '18 at 16:12

3 Answers3

3

You failed to insert trackID when creating layer instruction. Try :

let videolayerInstruction : AVMutableVideoCompositionLayerInstruction = AVMutableVideoCompositionLayerInstruction(vtrack)
Deepak Sharma
  • 5,577
  • 7
  • 55
  • 131
0

When I tried this the subtitle did not appear. I fixed it by swapping the order of the sublayers.

i.e

parentLayer.addSublayer(videoLayer)

parentLayer.addSubLayer(backgroundLayer)

0

I updated it to work with Swift 3 + adding the fix suggested by Deepak.

func mergeVideoWithTheme(outputUrl: NSURL, inputVideoUrl videoUrl: NSURL!, onComplete completionHandler: ((Int) -> ())!) -> Void {
   do {
        // 1. mergeComposition adds all the AVAssets

        var mergeComposition : AVMutableComposition = AVMutableComposition()
        var trackVideo : AVMutableCompositionTrack = mergeComposition.addMutableTrack(withMediaType: AVMediaTypeVideo, preferredTrackID: CMPersistentTrackID())
        //var trackAudio : AVMutableCompositionTrack = mergeComposition.addMutableTrackWithMediaType(AVMediaTypeAudio, preferredTrackID: CMPersistentTrackID())

        // 2. Add a bank for theme insertion later

        //trackVideo.insertTimeRange(range, ofTrack: VideoHelper.Static.blankTrack, atTime: kCMTimeZero, error: nil)

        // 3. Source tracks

        let sourceAsset = AVURLAsset(url: videoUrl as URL, options: nil)
        let sourceDuration = CMTimeRangeMake(kCMTimeZero, sourceAsset.duration)
        let vtrack: AVAssetTrack? = sourceAsset.tracks(withMediaType: AVMediaTypeVideo)[0] as? AVAssetTrack
        let atrack: AVAssetTrack? = sourceAsset.tracks(withMediaType: AVMediaTypeAudio)[0] as? AVAssetTrack

        if (vtrack == nil) {
            return
        }

        let renderWidth = vtrack?.naturalSize.width
        let renderHeight = vtrack?.naturalSize.height
        let insertTime = kCMTimeZero
        let endTime = sourceAsset.duration
        let range = sourceDuration

        // append tracks

        try trackVideo.insertTimeRange(sourceDuration, of: vtrack!, at: insertTime)
        //if(atrack > 0){
        //    trackAudio.insertTimeRange(sourceDuration, ofTrack: atracks[0] as AVAssetTrack, atTime: insertTime, error: nil)
        //}

        // 4. Add subtitles (we call it theme)

        var themeVideoComposition : AVMutableVideoComposition = AVMutableVideoComposition(propertiesOf: sourceAsset)

        // 4.1 - Create AVMutableVideoCompositionInstruction

        let mainInstruction: AVMutableVideoCompositionInstruction = AVMutableVideoCompositionInstruction()
        mainInstruction.timeRange = range

        // 4.2 - Create an AVMutableVideoCompositionLayerInstruction for the video track and fix the orientation.
        let videolayerInstruction : AVMutableVideoCompositionLayerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: vtrack!)
        videolayerInstruction.setTransform(trackVideo.preferredTransform, at: insertTime)
        videolayerInstruction.setOpacity(0.0, at: endTime)

        // 4.3 - Add instructions

        mainInstruction.layerInstructions = NSArray(array: [videolayerInstruction]) as! [AVVideoCompositionLayerInstruction]

        themeVideoComposition.renderScale = 1.0
        themeVideoComposition.renderSize =   CGSize(width: renderWidth!, height:  renderHeight!)
        themeVideoComposition.frameDuration = CMTimeMake(1, 30)
        themeVideoComposition.instructions = NSArray(array: [mainInstruction]) as! [AVVideoCompositionInstructionProtocol]

        // add the theme

        // setup variables

        // add text

        let title = String("Testing this subtitle")

        var titleLayer = CATextLayer()
        titleLayer.string = title
        titleLayer.frame =  CGRect(x: 0, y: 0, width: renderWidth!, height: renderHeight!)
        let fontName: CFString = "Helvetica-Bold" as CFString
        let fontSize = CGFloat(36)
        titleLayer.font = CTFontCreateWithName(fontName, fontSize, nil)
        titleLayer.alignmentMode = kCAAlignmentCenter
        titleLayer.foregroundColor = UIColor.white.cgColor

        var backgroundLayer = CALayer()
        backgroundLayer.frame = CGRect(x: 0, y: 0, width: renderWidth!, height: renderHeight!)
        backgroundLayer.masksToBounds = true
        backgroundLayer.addSublayer(titleLayer)

        // 2. set parent layer and video layer

        var parentLayer = CALayer()
        var videoLayer = CALayer()
        parentLayer.frame =  CGRect(x: 0, y: 0, width: renderWidth!, height: renderHeight!)
        videoLayer.frame =  CGRect(x: 0, y: 0, width: renderWidth!, height: renderHeight!)

        parentLayer.addSublayer(backgroundLayer)
        parentLayer.addSublayer(videoLayer)

        // 3. make animation

        themeVideoComposition.animationTool = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer: videoLayer, in: parentLayer)

        // Remove the file if it already exists (merger does not overwrite)

        let fileManager = FileManager.default
        try fileManager.removeItem(at: outputUrl as URL)

        // export to output url

        var exporter = AVAssetExportSession(asset: mergeComposition, presetName: AVAssetExportPresetHighestQuality)
        exporter?.outputURL = outputUrl as URL
        exporter?.videoComposition = themeVideoComposition
        exporter?.outputFileType = AVFileTypeQuickTimeMovie
        exporter?.shouldOptimizeForNetworkUse = true
        exporter?.exportAsynchronously(completionHandler: {
            if (exporter?.error != nil) {
                print("Error")
                print(exporter?.error)
                print("Description")
                print(exporter?.description)
            }
            completionHandler((exporter?.status.rawValue)!)
        })
    }
        catch{

        }

    }