-
-
Save SheffieldKevin/c01789ccff2b2a87f5ea to your computer and use it in GitHub Desktop.
| // | |
| // main.swift | |
| // mutablecomposition | |
| // | |
| // Created by Kevin Meaney on 24/08/2015. | |
| // Copyright (c) 2015 Kevin Meaney. All rights reserved. | |
| // | |
| import Foundation | |
| // #!/usr/bin/env swift | |
| // If you want to run this file from the command line uncomment the above line | |
| // so that the '#' symbol is at the beginning of the line. | |
| // Created by Kevin Meaney on 20/11/2014. | |
| // Copyright (c) 2014 Kevin Meaney. All rights reserved. | |
| // The first part of the script is basically config options. | |
| import Cocoa | |
| import AVFoundation | |
| // Set the transition duration time to two seconds. | |
| let transDuration = CMTimeMake(2, 1) | |
| // The movies below have the same dimensions as the movie I want to generate | |
| let movieSize = CGSizeMake(576, 360) | |
| // This is the preset applied to the AVAssetExportSession. | |
| // If the passthrough preset is used then the created movie file has two video | |
| // tracks but the transitions between the segments in each track are lost. | |
| // Other presets will generate a file with a single video track with the | |
| // transitions applied before export happens. | |
| // let exportPreset = AVAssetExportPresetPassthrough | |
| let exportPreset = AVAssetExportPreset640x480 | |
| // Path and file name to where the generated movie file will be created. | |
| // If a previous file was at this location it will be deleted before the new | |
| // file is generated. BEWARE | |
| let exportFilePath:NSString = "~/Desktop/TransitionsMovie.mov" | |
| // Create the list of paths to movie files that generated movie will transition between. | |
| // The movies need to not have any copy protection. | |
| let movieFilePaths = [ | |
| "~/Movies/clips/410_clip1.mov", | |
| "~/Movies/clips/410_clip2.mov", | |
| "~/Movies/clips/410_clip3.mov", | |
| "~/Movies/clips/410_clip4.mov", | |
| "~/Movies/clips/410_clip5.mov", | |
| "~/Movies/clips/410_clip6.mov" | |
| ] | |
| // Convert the file paths into URLS after expanding any tildes in the path | |
| let urls = movieFilePaths.map({ (filePath) -> NSURL in | |
| let expandedPath = filePath.stringByExpandingTildeInPath; | |
| return NSURL(fileURLWithPath: expandedPath, isDirectory: false)! | |
| }) | |
| // Make movie assets from the URLs. | |
| let movieAssets:[AVURLAsset] = urls.map { AVURLAsset(URL:$0, options:.None)! } | |
| // Create the mutable composition that we are going to build up. | |
| var composition = AVMutableComposition() | |
| // Function to build the composition tracks. | |
| func buildCompositionTracks(#composition: AVMutableComposition, | |
| #transitionDuration: CMTime, | |
| #assetsWithVideoTracks: [AVURLAsset]) -> Void { | |
| let compositionTrackA = composition.addMutableTrackWithMediaType(AVMediaTypeVideo, | |
| preferredTrackID: CMPersistentTrackID(kCMPersistentTrackID_Invalid)) | |
| let compositionTrackB = composition.addMutableTrackWithMediaType(AVMediaTypeVideo, | |
| preferredTrackID: CMPersistentTrackID(kCMPersistentTrackID_Invalid)) | |
| let videoTracks = [compositionTrackA, compositionTrackB] | |
| var cursorTime = kCMTimeZero | |
| for (var i = 0 ; i < assetsWithVideoTracks.count ; ++i) { | |
| let trackIndex = i % 2 | |
| let currentTrack = videoTracks[trackIndex] | |
| let assetTrack = assetsWithVideoTracks[i].tracksWithMediaType(AVMediaTypeVideo)[0] as! AVAssetTrack | |
| let timeRange = CMTimeRangeMake(kCMTimeZero, assetsWithVideoTracks[i].duration) | |
| currentTrack.insertTimeRange( timeRange, | |
| ofTrack: assetTrack, | |
| atTime: cursorTime, | |
| error: nil) | |
| // Overlap clips by tranition duration // 4 | |
| cursorTime = CMTimeAdd(cursorTime, assetsWithVideoTracks[i].duration) | |
| cursorTime = CMTimeSubtract(cursorTime, transitionDuration) | |
| } | |
| // Currently leaving out voice overs and movie tracks. // 5 | |
| } | |
| // Function to calculate both the pass through time and the transition time ranges | |
| func calculateTimeRanges(#transitionDuration: CMTime, | |
| #assetsWithVideoTracks: [AVURLAsset]) | |
| -> (passThroughTimeRanges: [NSValue], transitionTimeRanges: [NSValue]) { | |
| var passThroughTimeRanges:[NSValue] = [NSValue]() | |
| var transitionTimeRanges:[NSValue] = [NSValue]() | |
| var cursorTime = kCMTimeZero | |
| for (var i = 0 ; i < assetsWithVideoTracks.count ; ++i) | |
| { | |
| let asset = assetsWithVideoTracks[i] | |
| var timeRange = CMTimeRangeMake(cursorTime, asset.duration) | |
| if i > 0 { | |
| timeRange.start = CMTimeAdd(timeRange.start, transDuration) | |
| timeRange.duration = CMTimeSubtract(timeRange.duration, transDuration) | |
| } | |
| if i + 1 < assetsWithVideoTracks.count { | |
| timeRange.duration = CMTimeSubtract(timeRange.duration, transDuration) | |
| } | |
| passThroughTimeRanges.append(NSValue(CMTimeRange: timeRange)) | |
| cursorTime = CMTimeAdd(cursorTime, asset.duration) | |
| cursorTime = CMTimeSubtract(cursorTime, transDuration) | |
| // println("cursorTime.value: \(cursorTime.value)") | |
| // println("cursorTime.timescale: \(cursorTime.timescale)") | |
| if i + 1 < assetsWithVideoTracks.count { | |
| timeRange = CMTimeRangeMake(cursorTime, transDuration) | |
| // println("timeRange start value: \(timeRange.start.value)") | |
| // println("timeRange start timescale: \(timeRange.start.timescale)") | |
| transitionTimeRanges.append(NSValue(CMTimeRange: timeRange)) | |
| } | |
| } | |
| return (passThroughTimeRanges, transitionTimeRanges) | |
| } | |
| // Build the video composition and instructions. | |
| func buildVideoCompositionAndInstructions( | |
| #composition: AVMutableComposition, | |
| #passThroughTimeRanges: [NSValue], | |
| #transitionTimeRanges: [NSValue], | |
| #renderSize: CGSize) -> AVMutableVideoComposition { | |
| // Create a mutable composition instructions object | |
| var compositionInstructions = [AVMutableVideoCompositionInstruction]() | |
| // Get the list of asset tracks and tell compiler they are a list of asset tracks. | |
| let tracks = composition.tracksWithMediaType(AVMediaTypeVideo) as! [AVAssetTrack] | |
| // Create a video composition object | |
| let videoComposition = AVMutableVideoComposition(propertiesOfAsset: composition) | |
| // Now create the instructions from the various time ranges. | |
| for (var i = 0 ; i < passThroughTimeRanges.count ; ++i) | |
| { | |
| let trackIndex = i % 2 | |
| let currentTrack = tracks[trackIndex] | |
| let instruction = AVMutableVideoCompositionInstruction() | |
| instruction.timeRange = passThroughTimeRanges[i].CMTimeRangeValue | |
| let layerInstruction = AVMutableVideoCompositionLayerInstruction( | |
| assetTrack: currentTrack) | |
| instruction.layerInstructions = [layerInstruction] | |
| compositionInstructions.append(instruction) | |
| if i < transitionTimeRanges.count { | |
| let instruction = AVMutableVideoCompositionInstruction() | |
| instruction.timeRange = transitionTimeRanges[i].CMTimeRangeValue | |
| // Determine the foreground and background tracks. | |
| let fgTrack = tracks[trackIndex] | |
| let bgTrack = tracks[1 - trackIndex] | |
| // Create the "from layer" instruction. | |
| let fLInstruction = AVMutableVideoCompositionLayerInstruction( | |
| assetTrack: fgTrack) | |
| // Make the opacity ramp and apply it to the from layer instruction. | |
| fLInstruction.setOpacityRampFromStartOpacity(1.0, toEndOpacity:0.0, | |
| timeRange: instruction.timeRange) | |
| // Create the "to layer" instruction. Do I need this? | |
| let tLInstruction = AVMutableVideoCompositionLayerInstruction( | |
| assetTrack: bgTrack) | |
| instruction.layerInstructions = [fLInstruction, tLInstruction] | |
| compositionInstructions.append(instruction) | |
| } | |
| } | |
| videoComposition.instructions = compositionInstructions | |
| videoComposition.renderSize = renderSize | |
| videoComposition.frameDuration = CMTimeMake(1, 30) | |
| // videoComposition.renderScale = 1.0 // This is a iPhone only option. | |
| return videoComposition | |
| } | |
| func makeExportSession(#preset: String, | |
| #videoComposition: AVMutableVideoComposition, | |
| #composition: AVMutableComposition) -> AVAssetExportSession { | |
| let session = AVAssetExportSession(asset: composition, presetName: preset) | |
| session.videoComposition = videoComposition.copy() as! AVVideoComposition | |
| // session.outputFileType = "com.apple.m4v-video" | |
| // session.outputFileType = AVFileTypeAppleM4V | |
| session.outputFileType = AVFileTypeQuickTimeMovie | |
| return session | |
| } | |
| // Now call the functions to do the preperation work for preparing a composition to export. | |
| // First create the tracks needed for the composition. | |
| buildCompositionTracks(composition: composition, | |
| transitionDuration: transDuration, | |
| assetsWithVideoTracks: movieAssets) | |
| // Create the passthru and transition time ranges. | |
| let timeRanges = calculateTimeRanges(transitionDuration: transDuration, | |
| assetsWithVideoTracks: movieAssets) | |
| // Create the instructions for which movie to show and create the video composition. | |
| let videoComposition = buildVideoCompositionAndInstructions( | |
| composition: composition, | |
| passThroughTimeRanges: timeRanges.passThroughTimeRanges, | |
| transitionTimeRanges: timeRanges.transitionTimeRanges, | |
| renderSize: movieSize) | |
| // Make the export session object that we'll use to export the transition movie | |
| let exportSession = makeExportSession(preset: exportPreset, | |
| videoComposition: videoComposition, | |
| composition: composition) | |
| // Make a expanded file path for export. Delete any previous generated file. | |
| let expandedFilePath = exportFilePath.stringByExpandingTildeInPath | |
| NSFileManager.defaultManager().removeItemAtPath(expandedFilePath, error: nil) | |
| // Assign the output URL built from the expanded output file path. | |
| exportSession.outputURL = NSURL(fileURLWithPath: expandedFilePath, isDirectory:false)! | |
| // Since export happens asyncrhonously then this command line tool can exit | |
| // before the export has completed unless we wait until the export has finished. | |
| let sessionWaitSemaphore = dispatch_semaphore_create(0) | |
| exportSession.exportAsynchronouslyWithCompletionHandler({ | |
| dispatch_semaphore_signal(sessionWaitSemaphore) | |
| return Void() | |
| }) | |
| dispatch_semaphore_wait(sessionWaitSemaphore, DISPATCH_TIME_FOREVER) | |
| println("Export finished") |
Audio not working after adding transition...
This code ignores audio. I believe at least one of the forks from this gist has added audio.
Thank you very much @SheffieldKevin for this. I updated the code for the latest Swift version (as of June 2018) https://gist.github.com/Tulakshana/01fe0c15b71180c2adf78d64fbaa44b5
This can be easily tested in an iOS project. However, the code will need to be modified if you need to run from the command line.
I also tried removing the AVMutableVideoCompositionLayerInstruction added to the background track. My experience was that without it the transition wasn't smooth.
I want to add the different transition between videos like IMovie.
func transitionInstructions(in videoComposition: AVVideoComposition?) -> [Any]? {
var transitionInstructions: [Any] = []
var layerInstructionIndex: Int = 1
let compositionInstructions = videoComposition?.instructions
for vci: AVMutableVideoCompositionInstruction? in compositionInstructions ?? [] {
if vci?.layerInstructions.count == 2 {
let instructions = VideoTransitionInstructions()
instructions.compositionInstruction = vci
instructions.fromLayerInstruction = vci?.layerInstructions[1 - layerInstructionIndex] as? AVMutableVideoCompositionLayerInstruction
instructions.toLayerInstruction = vci?.layerInstructions[layerInstructionIndex] as? AVMutableVideoCompositionLayerInstruction
transitionInstructions.append(instructions)
layerInstructionIndex = layerInstructionIndex == 1 ? 0 : 1
}
}
guard let transitions = self.videoEditManager?.transition else{
return transitionInstructions
}
assert(transitionInstructions.count == transitions.count, "Instruction count and transition count do not match.")
for i in 0..<transitionInstructions.count {
let tis = transitionInstructions[i] as? VideoTransitionInstructions
tis?.transition = self.videoEditManager?.transition[i]
}
return transitionInstructions
}
but I am getting this error
Cannot convert value of type '[AVVideoCompositionInstructionProtocol]?' to expected argument type '_?
Hi, Kevin.
Did you add the
AVAudioMixto videos?