How to do Slow Motion video in IOS
You could scale video using AVFoundation and CoreMedia frameworks.Take a look at the AVMutableCompositionTrack method:
- (void)scaleTimeRange:(CMTimeRange)timeRange toDuration:(CMTime)duration;
Sample:
AVURLAsset* videoAsset = nil; //self.inputAsset;//create mutable compositionAVMutableComposition *mixComposition = [AVMutableComposition composition];AVMutableCompositionTrack *compositionVideoTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];NSError *videoInsertError = nil;BOOL videoInsertResult = [compositionVideoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.duration) ofTrack:[[videoAsset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0] atTime:kCMTimeZero error:&videoInsertError];if (!videoInsertResult || nil != videoInsertError) { //handle error return;}//slow down whole video by 2.0double videoScaleFactor = 2.0;CMTime videoDuration = videoAsset.duration;[compositionVideoTrack scaleTimeRange:CMTimeRangeMake(kCMTimeZero, videoDuration) toDuration:CMTimeMake(videoDuration.value*videoScaleFactor, videoDuration.timescale)];//exportAVAssetExportSession* assetExport = [[AVAssetExportSession alloc] initWithAsset:mixComposition presetName:AVAssetExportPresetLowQuality];
(Probably audio track from videoAsset should also be added to mixComposition)
Slower + Faster with or without audio track
I have tried and able to Slower the asset.
compositionVideoTrack?.scaleTimeRange(timeRange, toDuration: scaledVideoDuration)
did the trick.
I made a class which will help you to generate a slower
video from AVAsset
. + point is you can also make it faster
and another + point is it will handle the audio too.
Here is my custom class sample:
import UIKitimport AVFoundationenum SpeedoMode { case Slower case Faster}class VSVideoSpeeder: NSObject { /// Singleton instance of `VSVideoSpeeder` static var shared: VSVideoSpeeder = { return VSVideoSpeeder() }() /// Range is b/w 1x, 2x and 3x. Will not happen anything if scale is out of range. Exporter will be nil in case url is invalid or unable to make asset instance. func scaleAsset(fromURL url: URL, by scale: Int64, withMode mode: SpeedoMode, completion: @escaping (_ exporter: AVAssetExportSession?) -> Void) { /// Check the valid scale if scale < 1 || scale > 3 { /// Can not proceed, Invalid range completion(nil) return } /// Asset let asset = AVAsset(url: url) /// Video Tracks let videoTracks = asset.tracks(withMediaType: AVMediaType.video) if videoTracks.count == 0 { /// Can not find any video track completion(nil) return } /// Get the scaled video duration let scaledVideoDuration = (mode == .Faster) ? CMTimeMake(asset.duration.value / scale, asset.duration.timescale) : CMTimeMake(asset.duration.value * scale, asset.duration.timescale) let timeRange = CMTimeRangeMake(kCMTimeZero, asset.duration) /// Video track let videoTrack = videoTracks.first! let mixComposition = AVMutableComposition() let compositionVideoTrack = mixComposition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: kCMPersistentTrackID_Invalid) /// Audio Tracks let audioTracks = asset.tracks(withMediaType: AVMediaType.audio) if audioTracks.count > 0 { /// Use audio if video contains the audio track let compositionAudioTrack = mixComposition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: kCMPersistentTrackID_Invalid) /// Audio track let audioTrack = audioTracks.first! do { try compositionAudioTrack?.insertTimeRange(timeRange, of: audioTrack, at: kCMTimeZero) compositionAudioTrack?.scaleTimeRange(timeRange, toDuration: scaledVideoDuration) } catch _ { /// Ignore audio error } } do { try compositionVideoTrack?.insertTimeRange(timeRange, of: videoTrack, at: kCMTimeZero) compositionVideoTrack?.scaleTimeRange(timeRange, toDuration: scaledVideoDuration) /// Keep original transformation compositionVideoTrack?.preferredTransform = videoTrack.preferredTransform /// Initialize Exporter now let outputFileURL = URL(fileURLWithPath: "/Users/thetiger/Desktop/scaledVideo.mov") /// Note:- Please use directory path if you are testing with device. if FileManager.default.fileExists(atPath: outputFileURL.absoluteString) { try FileManager.default.removeItem(at: outputFileURL) } let exporter = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality) exporter?.outputURL = outputFileURL exporter?.outputFileType = AVFileType.mov exporter?.shouldOptimizeForNetworkUse = true exporter?.exportAsynchronously(completionHandler: { completion(exporter) }) } catch let error { print(error.localizedDescription) completion(nil) return } }}
I took 1x, 2x and 3x as a valid scale. Class contains the proper validation and handling. Below is the sample of how to use this function.
let url = Bundle.main.url(forResource: "1", withExtension: "mp4")!VSVideoSpeeder.shared.scaleAsset(fromURL: url, by: 3, withMode: SpeedoMode.Slower) { (exporter) in if let exporter = exporter { switch exporter.status { case .failed: do { print(exporter.error?.localizedDescription ?? "Error in exporting..") } case .completed: do { print("Scaled video has been generated successfully!") } case .unknown: break case .waiting: break case .exporting: break case .cancelled: break } } else { /// Error print("Exporter is not initialized.") }}
This line will handle the audio scaling
compositionAudioTrack?.scaleTimeRange(timeRange, toDuration: scaledVideoDuration)
I have achieved on adding slow motion to video including audio as well with proper output orientation.
- (void)SlowMotion:(NSURL *)URl { AVURLAsset* videoAsset = [AVURLAsset URLAssetWithURL:URl options:nil]; //self.inputAsset;AVAsset *currentAsset = [AVAsset assetWithURL:URl];AVAssetTrack *vdoTrack = [[currentAsset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];//create mutable compositionAVMutableComposition *mixComposition = [AVMutableComposition composition];AVMutableCompositionTrack *compositionVideoTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];AVMutableCompositionTrack *compositionAudioTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];NSError *videoInsertError = nil;BOOL videoInsertResult = [compositionVideoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.duration) ofTrack:[[videoAsset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0] atTime:kCMTimeZero error:&videoInsertError];if (!videoInsertResult || nil != videoInsertError) { //handle error return;}NSError *audioInsertError =nil;BOOL audioInsertResult =[compositionAudioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.duration) ofTrack:[[currentAsset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0] atTime:kCMTimeZero error:&audioInsertError];if (!audioInsertResult || nil != audioInsertError) { //handle error return;}CMTime duration =kCMTimeZero;duration=CMTimeAdd(duration, currentAsset.duration);//slow down whole video by 2.0double videoScaleFactor = 2.0;CMTime videoDuration = videoAsset.duration;[compositionVideoTrack scaleTimeRange:CMTimeRangeMake(kCMTimeZero, videoDuration) toDuration:CMTimeMake(videoDuration.value*videoScaleFactor, videoDuration.timescale)];[compositionAudioTrack scaleTimeRange:CMTimeRangeMake(kCMTimeZero, videoDuration) toDuration:CMTimeMake(videoDuration.value*videoScaleFactor, videoDuration.timescale)];[compositionVideoTrack setPreferredTransform:vdoTrack.preferredTransform]; NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *docsDir = [dirPaths objectAtIndex:0]; NSString *outputFilePath = [docsDir stringByAppendingPathComponent:[NSString stringWithFormat:@"slowMotion.mov"]]; if ([[NSFileManager defaultManager] fileExistsAtPath:outputFilePath]) [[NSFileManager defaultManager] removeItemAtPath:outputFilePath error:nil]; NSURL *_filePath = [NSURL fileURLWithPath:outputFilePath];//exportAVAssetExportSession* assetExport = [[AVAssetExportSession alloc] initWithAsset:mixComposition presetName:AVAssetExportPresetLowQuality];assetExport.outputURL=_filePath; assetExport.outputFileType = AVFileTypeQuickTimeMovie; exporter.shouldOptimizeForNetworkUse = YES; [assetExport exportAsynchronouslyWithCompletionHandler:^ { switch ([assetExport status]) { case AVAssetExportSessionStatusFailed: { NSLog(@"Export session faiied with error: %@", [assetExport error]); dispatch_async(dispatch_get_main_queue(), ^{ // completion(nil); }); } break; case AVAssetExportSessionStatusCompleted: { NSLog(@"Successful"); NSURL *outputURL = assetExport.outputURL; ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init]; if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:outputURL]) { [self writeExportedVideoToAssetsLibrary:outputURL]; } dispatch_async(dispatch_get_main_queue(), ^{ // completion(_filePath); }); } break; default: break; } }]; } - (void)writeExportedVideoToAssetsLibrary :(NSURL *)url {NSURL *exportURL = url;ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:exportURL]) { [library writeVideoAtPathToSavedPhotosAlbum:exportURL completionBlock:^(NSURL *assetURL, NSError *error){ dispatch_async(dispatch_get_main_queue(), ^{ if (error) { UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:[error localizedDescription] message:[error localizedRecoverySuggestion] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alertView show]; } if(!error) { // [activityView setHidden:YES]; UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Sucess" message:@"video added to gallery successfully" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alertView show]; } #if !TARGET_IPHONE_SIMULATOR [[NSFileManager defaultManager] removeItemAtURL:exportURL error:nil];#endif }); }];} else { NSLog(@"Video could not be exported to assets library.");}}