├── .gitignore ├── LICENSE ├── README.md ├── SDAVAssetExportSession.h ├── SDAVAssetExportSession.m └── SDAVAssetExportSession.podspec /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | *.xcworkspace 13 | !default.xcworkspace 14 | xcuserdata 15 | profile 16 | *.moved-aside 17 | DerivedData 18 | .idea/ 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Olivier Poitrey 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SDAVAssetExportSession 2 | ====================== 3 | 4 | `AVAssetExportSession` drop-in replacement with customizable audio&video settings. 5 | 6 | You want the ease of use of `AVAssetExportSession` but default provided presets doesn't fit your needs? You then began to read documentation for `AVAssetWriter`, `AVAssetWriterInput`, `AVAssetReader`, `AVAssetReaderVideoCompositionOutput`, `AVAssetReaderAudioMixOutput`… and you went out of aspirin? `SDAVAssetExportSession` is a rewrite of `AVAssetExportSession` on top of `AVAssetReader*` and `AVAssetWriter*`. Unlike `AVAssetExportSession`, you are not limited to a set of presets – you have full access over audio and video settings. 7 | 8 | 9 | Usage Example 10 | ------------- 11 | 12 | ``` objective-c 13 | SDAVAssetExportSession *encoder = [SDAVAssetExportSession.alloc initWithAsset:anAsset]; 14 | encoder.outputFileType = AVFileTypeMPEG4; 15 | encoder.outputURL = outputFileURL; 16 | encoder.videoSettings = @ 17 | { 18 | AVVideoCodecKey: AVVideoCodecH264, 19 | AVVideoWidthKey: @1920, 20 | AVVideoHeightKey: @1080, 21 | AVVideoCompressionPropertiesKey: @ 22 | { 23 | AVVideoAverageBitRateKey: @6000000, 24 | AVVideoProfileLevelKey: AVVideoProfileLevelH264High40, 25 | }, 26 | }; 27 | encoder.audioSettings = @ 28 | { 29 | AVFormatIDKey: @(kAudioFormatMPEG4AAC), 30 | AVNumberOfChannelsKey: @2, 31 | AVSampleRateKey: @44100, 32 | AVEncoderBitRateKey: @128000, 33 | }; 34 | 35 | [encoder exportAsynchronouslyWithCompletionHandler:^ 36 | { 37 | if (encoder.status == AVAssetExportSessionStatusCompleted) 38 | { 39 | NSLog(@"Video export succeeded"); 40 | } 41 | else if (encoder.status == AVAssetExportSessionStatusCancelled) 42 | { 43 | NSLog(@"Video export cancelled"); 44 | } 45 | else 46 | { 47 | NSLog(@"Video export failed with error: %@ (%d)", encoder.error.localizedDescription, encoder.error.code); 48 | } 49 | }]; 50 | 51 | ``` 52 | 53 | Licenses 54 | -------- 55 | 56 | All source code is licensed under the [MIT-License](https://github.com/rs/SDAVAssetExportSession/blob/master/LICENSE). 57 | -------------------------------------------------------------------------------- /SDAVAssetExportSession.h: -------------------------------------------------------------------------------- 1 | // 2 | // SDAVAssetExportSession.h 3 | // 4 | // This file is part of the SDAVAssetExportSession package. 5 | // 6 | // Created by Olivier Poitrey on 13/03/13. 7 | // Copyright 2013 Olivier Poitrey. All rights servered. 8 | // 9 | // For the full copyright and license information, please view the LICENSE 10 | // file that was distributed with this source code. 11 | // 12 | 13 | #import 14 | #import 15 | 16 | @protocol SDAVAssetExportSessionDelegate; 17 | 18 | 19 | /** 20 | * An `SDAVAssetExportSession` object transcodes the contents of an AVAsset source object to create an output 21 | * of the form described by specified video and audio settings. It implements most of the API of Apple provided 22 | * `AVAssetExportSession` but with the capability to provide you own video and audio settings instead of the 23 | * limited set of Apple provided presets. 24 | * 25 | * After you have initialized an export session with the asset that contains the source media, video and audio 26 | * settings, and the output file type (outputFileType), you can start the export running by invoking 27 | * `exportAsynchronouslyWithCompletionHandler:`. Because the export is performed asynchronously, this method 28 | * returns immediately — you can observe progress to check on the progress. 29 | * 30 | * The completion handler you supply to `exportAsynchronouslyWithCompletionHandler:` is called whether the export 31 | * fails, completes, or is cancelled. Upon completion, the status property indicates whether the export has 32 | * completed successfully. If it has failed, the value of the error property supplies additional information 33 | * about the reason for the failure. 34 | */ 35 | 36 | @interface SDAVAssetExportSession : NSObject 37 | 38 | @property (nonatomic, weak) id delegate; 39 | 40 | /** 41 | * The asset with which the export session was initialized. 42 | */ 43 | @property (nonatomic, strong, readonly) AVAsset *asset; 44 | 45 | /** 46 | * Indicates whether video composition is enabled for export, and supplies the instructions for video composition. 47 | * 48 | * You can observe this property using key-value observing. 49 | */ 50 | @property (nonatomic, copy) AVVideoComposition *videoComposition; 51 | 52 | /** 53 | * Indicates whether non-default audio mixing is enabled for export, and supplies the parameters for audio mixing. 54 | */ 55 | @property (nonatomic, copy) AVAudioMix *audioMix; 56 | 57 | /** 58 | * The type of file to be written by the session. 59 | * 60 | * The value is a UTI string corresponding to the file type to use when writing the asset. 61 | * For a list of constants specifying UTIs for standard file types, see `AV Foundation Constants Reference`. 62 | * 63 | * You can observe this property using key-value observing. 64 | */ 65 | @property (nonatomic, copy) NSString *outputFileType; 66 | 67 | /** 68 | * The URL of the export session’s output. 69 | * 70 | * You can observe this property using key-value observing. 71 | */ 72 | @property (nonatomic, copy) NSURL *outputURL; 73 | 74 | /** 75 | * The settings used for input video track. 76 | * 77 | * The dictionary’s keys are from . 78 | */ 79 | @property (nonatomic, copy) NSDictionary *videoInputSettings; 80 | 81 | /** 82 | * The settings used for encoding the video track. 83 | * 84 | * A value of nil specifies that appended output should not be re-encoded. 85 | * The dictionary’s keys are from . 86 | */ 87 | @property (nonatomic, copy) NSDictionary *videoSettings; 88 | 89 | /** 90 | * The settings used for encoding the audio track. 91 | * 92 | * A value of nil specifies that appended output should not be re-encoded. 93 | * The dictionary’s keys are from . 94 | */ 95 | @property (nonatomic, copy) NSDictionary *audioSettings; 96 | 97 | /** 98 | * The time range to be exported from the source. 99 | * 100 | * The default time range of an export session is `kCMTimeZero` to `kCMTimePositiveInfinity`, 101 | * meaning that (modulo a possible limit on file length) the full duration of the asset will be exported. 102 | * 103 | * You can observe this property using key-value observing. 104 | * 105 | */ 106 | @property (nonatomic, assign) CMTimeRange timeRange; 107 | 108 | /** 109 | * Indicates whether the movie should be optimized for network use. 110 | * 111 | * You can observe this property using key-value observing. 112 | */ 113 | @property (nonatomic, assign) BOOL shouldOptimizeForNetworkUse; 114 | 115 | /** 116 | * The metadata to be written to the output file by the export session. 117 | */ 118 | @property (nonatomic, copy) NSArray *metadata; 119 | 120 | /** 121 | * Describes the error that occurred if the export status is `AVAssetExportSessionStatusFailed` 122 | * or `AVAssetExportSessionStatusCancelled`. 123 | * 124 | * If there is no error to report, the value of this property is nil. 125 | */ 126 | @property (nonatomic, strong, readonly) NSError *error; 127 | 128 | /** 129 | * The progress of the export on a scale from 0 to 1. 130 | * 131 | * 132 | * A value of 0 means the export has not yet begun, 1 means the export is complete. 133 | * 134 | * Unlike Apple provided `AVAssetExportSession`, this property can be observed using key-value observing. 135 | */ 136 | @property (nonatomic, assign, readonly) float progress; 137 | 138 | /** 139 | * The status of the export session. 140 | * 141 | * For possible values, see “AVAssetExportSessionStatus.” 142 | * 143 | * You can observe this property using key-value observing. (TODO) 144 | */ 145 | @property (nonatomic, assign, readonly) AVAssetExportSessionStatus status; 146 | 147 | /** 148 | * Returns an asset export session configured with a specified asset. 149 | * 150 | * @param asset The asset you want to export 151 | * @return An asset export session initialized to export `asset`. 152 | */ 153 | + (id)exportSessionWithAsset:(AVAsset *)asset; 154 | 155 | /** 156 | * Initializes an asset export session with a specified asset. 157 | * 158 | * @param asset The asset you want to export 159 | * @return An asset export session initialized to export `asset`. 160 | */ 161 | - (id)initWithAsset:(AVAsset *)asset; 162 | 163 | /** 164 | * Starts the asynchronous execution of an export session. 165 | * 166 | * This method starts an asynchronous export operation and returns immediately. status signals the terminal 167 | * state of the export session, and if a failure occurs, error describes the problem. 168 | * 169 | * If internal preparation for export fails, handler is invoked synchronously. The handler may also be called 170 | * asynchronously, after the method returns, in the following cases: 171 | * 172 | * 1. If a failure occurs during the export, including failures of loading, re-encoding, or writing media data to the output. 173 | * 2. If cancelExport is invoked. 174 | * 3. After the export session succeeds, having completely written its output to the outputURL. 175 | * 176 | * @param handler A block that is invoked when writing is complete or in the event of writing failure. 177 | */ 178 | - (void)exportAsynchronouslyWithCompletionHandler:(void (^)(void))handler; 179 | 180 | /** 181 | * Cancels the execution of an export session. 182 | * 183 | * You can invoke this method when the export is running. 184 | */ 185 | - (void)cancelExport; 186 | 187 | @end 188 | 189 | 190 | @protocol SDAVAssetExportSessionDelegate 191 | 192 | - (void)exportSession:(SDAVAssetExportSession *)exportSession renderFrame:(CVPixelBufferRef)pixelBuffer withPresentationTime:(CMTime)presentationTime toBuffer:(CVPixelBufferRef)renderBuffer; 193 | 194 | @end 195 | -------------------------------------------------------------------------------- /SDAVAssetExportSession.m: -------------------------------------------------------------------------------- 1 | // 2 | // SDAVAssetExportSession.m 3 | // 4 | // This file is part of the SDAVAssetExportSession package. 5 | // 6 | // Created by Olivier Poitrey on 13/03/13. 7 | // Copyright 2013 Olivier Poitrey. All rights servered. 8 | // 9 | // For the full copyright and license information, please view the LICENSE 10 | // file that was distributed with this source code. 11 | // 12 | 13 | 14 | #import "SDAVAssetExportSession.h" 15 | 16 | @interface SDAVAssetExportSession () 17 | 18 | @property (nonatomic, assign, readwrite) float progress; 19 | 20 | @property (nonatomic, strong) AVAssetReader *reader; 21 | @property (nonatomic, strong) AVAssetReaderVideoCompositionOutput *videoOutput; 22 | @property (nonatomic, strong) AVAssetReaderAudioMixOutput *audioOutput; 23 | @property (nonatomic, strong) AVAssetWriter *writer; 24 | @property (nonatomic, strong) AVAssetWriterInput *videoInput; 25 | @property (nonatomic, strong) AVAssetWriterInputPixelBufferAdaptor *videoPixelBufferAdaptor; 26 | @property (nonatomic, strong) AVAssetWriterInput *audioInput; 27 | @property (nonatomic, strong) dispatch_queue_t inputQueue; 28 | @property (nonatomic, strong) void (^completionHandler)(void); 29 | 30 | @end 31 | 32 | @implementation SDAVAssetExportSession 33 | { 34 | NSError *_error; 35 | NSTimeInterval duration; 36 | CMTime lastSamplePresentationTime; 37 | } 38 | 39 | + (id)exportSessionWithAsset:(AVAsset *)asset 40 | { 41 | return [SDAVAssetExportSession.alloc initWithAsset:asset]; 42 | } 43 | 44 | - (id)initWithAsset:(AVAsset *)asset 45 | { 46 | if ((self = [super init])) 47 | { 48 | _asset = asset; 49 | _timeRange = CMTimeRangeMake(kCMTimeZero, kCMTimePositiveInfinity); 50 | } 51 | 52 | return self; 53 | } 54 | 55 | - (void)exportAsynchronouslyWithCompletionHandler:(void (^)(void))handler 56 | { 57 | NSParameterAssert(handler != nil); 58 | [self cancelExport]; 59 | self.completionHandler = handler; 60 | 61 | if (!self.outputURL) 62 | { 63 | _error = [NSError errorWithDomain:AVFoundationErrorDomain code:AVErrorExportFailed userInfo:@ 64 | { 65 | NSLocalizedDescriptionKey: @"Output URL not set" 66 | }]; 67 | handler(); 68 | return; 69 | } 70 | 71 | NSError *readerError; 72 | self.reader = [AVAssetReader.alloc initWithAsset:self.asset error:&readerError]; 73 | if (readerError) 74 | { 75 | _error = readerError; 76 | handler(); 77 | return; 78 | } 79 | 80 | NSError *writerError; 81 | self.writer = [AVAssetWriter assetWriterWithURL:self.outputURL fileType:self.outputFileType error:&writerError]; 82 | if (writerError) 83 | { 84 | _error = writerError; 85 | handler(); 86 | return; 87 | } 88 | 89 | self.reader.timeRange = self.timeRange; 90 | self.writer.shouldOptimizeForNetworkUse = self.shouldOptimizeForNetworkUse; 91 | self.writer.metadata = self.metadata; 92 | 93 | NSArray *videoTracks = [self.asset tracksWithMediaType:AVMediaTypeVideo]; 94 | 95 | 96 | if (CMTIME_IS_VALID(self.timeRange.duration) && !CMTIME_IS_POSITIVE_INFINITY(self.timeRange.duration)) 97 | { 98 | duration = CMTimeGetSeconds(self.timeRange.duration); 99 | } 100 | else 101 | { 102 | duration = CMTimeGetSeconds(self.asset.duration); 103 | } 104 | // 105 | // Video output 106 | // 107 | if (videoTracks.count > 0) { 108 | self.videoOutput = [AVAssetReaderVideoCompositionOutput assetReaderVideoCompositionOutputWithVideoTracks:videoTracks videoSettings:self.videoInputSettings]; 109 | self.videoOutput.alwaysCopiesSampleData = NO; 110 | if (self.videoComposition) 111 | { 112 | self.videoOutput.videoComposition = self.videoComposition; 113 | } 114 | else 115 | { 116 | self.videoOutput.videoComposition = [self buildDefaultVideoComposition]; 117 | } 118 | if ([self.reader canAddOutput:self.videoOutput]) 119 | { 120 | [self.reader addOutput:self.videoOutput]; 121 | } 122 | 123 | // 124 | // Video input 125 | // 126 | self.videoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:self.videoSettings]; 127 | self.videoInput.expectsMediaDataInRealTime = NO; 128 | if ([self.writer canAddInput:self.videoInput]) 129 | { 130 | [self.writer addInput:self.videoInput]; 131 | } 132 | NSDictionary *pixelBufferAttributes = @ 133 | { 134 | (id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA), 135 | (id)kCVPixelBufferWidthKey: @(self.videoOutput.videoComposition.renderSize.width), 136 | (id)kCVPixelBufferHeightKey: @(self.videoOutput.videoComposition.renderSize.height), 137 | @"IOSurfaceOpenGLESTextureCompatibility": @YES, 138 | @"IOSurfaceOpenGLESFBOCompatibility": @YES, 139 | }; 140 | self.videoPixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:self.videoInput sourcePixelBufferAttributes:pixelBufferAttributes]; 141 | } 142 | 143 | // 144 | //Audio output 145 | // 146 | NSArray *audioTracks = [self.asset tracksWithMediaType:AVMediaTypeAudio]; 147 | if (audioTracks.count > 0) { 148 | self.audioOutput = [AVAssetReaderAudioMixOutput assetReaderAudioMixOutputWithAudioTracks:audioTracks audioSettings:nil]; 149 | self.audioOutput.alwaysCopiesSampleData = NO; 150 | self.audioOutput.audioMix = self.audioMix; 151 | if ([self.reader canAddOutput:self.audioOutput]) 152 | { 153 | [self.reader addOutput:self.audioOutput]; 154 | } 155 | } else { 156 | // Just in case this gets reused 157 | self.audioOutput = nil; 158 | } 159 | 160 | // 161 | // Audio input 162 | // 163 | if (self.audioOutput) { 164 | self.audioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:self.audioSettings]; 165 | self.audioInput.expectsMediaDataInRealTime = NO; 166 | if ([self.writer canAddInput:self.audioInput]) 167 | { 168 | [self.writer addInput:self.audioInput]; 169 | } 170 | } 171 | 172 | [self.writer startWriting]; 173 | [self.reader startReading]; 174 | [self.writer startSessionAtSourceTime:self.timeRange.start]; 175 | 176 | __block BOOL videoCompleted = NO; 177 | __block BOOL audioCompleted = NO; 178 | __weak typeof(self) wself = self; 179 | self.inputQueue = dispatch_queue_create("VideoEncoderInputQueue", DISPATCH_QUEUE_SERIAL); 180 | if (videoTracks.count > 0) { 181 | [self.videoInput requestMediaDataWhenReadyOnQueue:self.inputQueue usingBlock:^ 182 | { 183 | if (![wself encodeReadySamplesFromOutput:wself.videoOutput toInput:wself.videoInput]) 184 | { 185 | @synchronized(wself) 186 | { 187 | videoCompleted = YES; 188 | if (audioCompleted) 189 | { 190 | [wself finish]; 191 | } 192 | } 193 | } 194 | }]; 195 | } 196 | else { 197 | videoCompleted = YES; 198 | } 199 | 200 | if (!self.audioOutput) { 201 | audioCompleted = YES; 202 | } else { 203 | [self.audioInput requestMediaDataWhenReadyOnQueue:self.inputQueue usingBlock:^ 204 | { 205 | if (![wself encodeReadySamplesFromOutput:wself.audioOutput toInput:wself.audioInput]) 206 | { 207 | @synchronized(wself) 208 | { 209 | audioCompleted = YES; 210 | if (videoCompleted) 211 | { 212 | [wself finish]; 213 | } 214 | } 215 | } 216 | }]; 217 | } 218 | } 219 | 220 | - (BOOL)encodeReadySamplesFromOutput:(AVAssetReaderOutput *)output toInput:(AVAssetWriterInput *)input 221 | { 222 | while (input.isReadyForMoreMediaData) 223 | { 224 | CMSampleBufferRef sampleBuffer = [output copyNextSampleBuffer]; 225 | if (sampleBuffer) 226 | { 227 | BOOL handled = NO; 228 | BOOL error = NO; 229 | 230 | if (self.reader.status != AVAssetReaderStatusReading || self.writer.status != AVAssetWriterStatusWriting) 231 | { 232 | handled = YES; 233 | error = YES; 234 | } 235 | 236 | if (!handled && self.videoOutput == output) 237 | { 238 | // update the video progress 239 | lastSamplePresentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); 240 | lastSamplePresentationTime = CMTimeSubtract(lastSamplePresentationTime, self.timeRange.start); 241 | self.progress = duration == 0 ? 1 : CMTimeGetSeconds(lastSamplePresentationTime) / duration; 242 | 243 | if ([self.delegate respondsToSelector:@selector(exportSession:renderFrame:withPresentationTime:toBuffer:)]) 244 | { 245 | CVPixelBufferRef pixelBuffer = (CVPixelBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer); 246 | CVPixelBufferRef renderBuffer = NULL; 247 | CVPixelBufferPoolCreatePixelBuffer(NULL, self.videoPixelBufferAdaptor.pixelBufferPool, &renderBuffer); 248 | [self.delegate exportSession:self renderFrame:pixelBuffer withPresentationTime:lastSamplePresentationTime toBuffer:renderBuffer]; 249 | if (![self.videoPixelBufferAdaptor appendPixelBuffer:renderBuffer withPresentationTime:lastSamplePresentationTime]) 250 | { 251 | error = YES; 252 | } 253 | CVPixelBufferRelease(renderBuffer); 254 | handled = YES; 255 | } 256 | } 257 | if (!handled && ![input appendSampleBuffer:sampleBuffer]) 258 | { 259 | error = YES; 260 | } 261 | CFRelease(sampleBuffer); 262 | 263 | if (error) 264 | { 265 | return NO; 266 | } 267 | } 268 | else 269 | { 270 | [input markAsFinished]; 271 | return NO; 272 | } 273 | } 274 | 275 | return YES; 276 | } 277 | 278 | - (AVMutableVideoComposition *)buildDefaultVideoComposition 279 | { 280 | AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition]; 281 | AVAssetTrack *videoTrack = [[self.asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]; 282 | 283 | // get the frame rate from videoSettings, if not set then try to get it from the video track, 284 | // if not set (mainly when asset is AVComposition) then use the default frame rate of 30 285 | float trackFrameRate = 0; 286 | if (self.videoSettings) 287 | { 288 | NSDictionary *videoCompressionProperties = [self.videoSettings objectForKey:AVVideoCompressionPropertiesKey]; 289 | if (videoCompressionProperties) 290 | { 291 | NSNumber *frameRate = [videoCompressionProperties objectForKey:AVVideoAverageNonDroppableFrameRateKey]; 292 | if (frameRate) 293 | { 294 | trackFrameRate = frameRate.floatValue; 295 | } 296 | } 297 | } 298 | else 299 | { 300 | trackFrameRate = [videoTrack nominalFrameRate]; 301 | } 302 | 303 | if (trackFrameRate == 0) 304 | { 305 | trackFrameRate = 30; 306 | } 307 | 308 | videoComposition.frameDuration = CMTimeMake(1, trackFrameRate); 309 | CGSize targetSize = CGSizeMake([self.videoSettings[AVVideoWidthKey] floatValue], [self.videoSettings[AVVideoHeightKey] floatValue]); 310 | CGSize naturalSize = [videoTrack naturalSize]; 311 | CGAffineTransform transform = videoTrack.preferredTransform; 312 | // Workaround radar 31928389, see https://github.com/rs/SDAVAssetExportSession/pull/70 for more info 313 | if (transform.ty == -560) { 314 | transform.ty = 0; 315 | } 316 | 317 | if (transform.tx == -560) { 318 | transform.tx = 0; 319 | } 320 | 321 | CGFloat videoAngleInDegree = atan2(transform.b, transform.a) * 180 / M_PI; 322 | if (videoAngleInDegree == 90 || videoAngleInDegree == -90) { 323 | CGFloat width = naturalSize.width; 324 | naturalSize.width = naturalSize.height; 325 | naturalSize.height = width; 326 | } 327 | videoComposition.renderSize = naturalSize; 328 | // center inside 329 | { 330 | float ratio; 331 | float xratio = targetSize.width / naturalSize.width; 332 | float yratio = targetSize.height / naturalSize.height; 333 | ratio = MIN(xratio, yratio); 334 | 335 | float postWidth = naturalSize.width * ratio; 336 | float postHeight = naturalSize.height * ratio; 337 | float transx = (targetSize.width - postWidth) / 2; 338 | float transy = (targetSize.height - postHeight) / 2; 339 | 340 | CGAffineTransform matrix = CGAffineTransformMakeTranslation(transx / xratio, transy / yratio); 341 | matrix = CGAffineTransformScale(matrix, ratio / xratio, ratio / yratio); 342 | transform = CGAffineTransformConcat(transform, matrix); 343 | } 344 | 345 | // Make a "pass through video track" video composition. 346 | AVMutableVideoCompositionInstruction *passThroughInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction]; 347 | passThroughInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, self.asset.duration); 348 | 349 | AVMutableVideoCompositionLayerInstruction *passThroughLayer = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoTrack]; 350 | 351 | [passThroughLayer setTransform:transform atTime:kCMTimeZero]; 352 | 353 | passThroughInstruction.layerInstructions = @[passThroughLayer]; 354 | videoComposition.instructions = @[passThroughInstruction]; 355 | 356 | return videoComposition; 357 | } 358 | 359 | - (void)finish 360 | { 361 | // Synchronized block to ensure we never cancel the writer before calling finishWritingWithCompletionHandler 362 | if (self.reader.status == AVAssetReaderStatusCancelled || self.writer.status == AVAssetWriterStatusCancelled) 363 | { 364 | return; 365 | } 366 | 367 | if (self.writer.status == AVAssetWriterStatusFailed) 368 | { 369 | [self complete]; 370 | } 371 | else if (self.reader.status == AVAssetReaderStatusFailed) { 372 | [self.writer cancelWriting]; 373 | [self complete]; 374 | } 375 | else 376 | { 377 | [self.writer finishWritingWithCompletionHandler:^ 378 | { 379 | [self complete]; 380 | }]; 381 | } 382 | } 383 | 384 | - (void)complete 385 | { 386 | if (self.writer.status == AVAssetWriterStatusFailed || self.writer.status == AVAssetWriterStatusCancelled) 387 | { 388 | [NSFileManager.defaultManager removeItemAtURL:self.outputURL error:nil]; 389 | } 390 | 391 | if (self.completionHandler) 392 | { 393 | self.completionHandler(); 394 | self.completionHandler = nil; 395 | } 396 | } 397 | 398 | - (NSError *)error 399 | { 400 | if (_error) 401 | { 402 | return _error; 403 | } 404 | else 405 | { 406 | return self.writer.error ? : self.reader.error; 407 | } 408 | } 409 | 410 | - (AVAssetExportSessionStatus)status 411 | { 412 | switch (self.writer.status) 413 | { 414 | default: 415 | case AVAssetWriterStatusUnknown: 416 | return AVAssetExportSessionStatusUnknown; 417 | case AVAssetWriterStatusWriting: 418 | return AVAssetExportSessionStatusExporting; 419 | case AVAssetWriterStatusFailed: 420 | return AVAssetExportSessionStatusFailed; 421 | case AVAssetWriterStatusCompleted: 422 | return AVAssetExportSessionStatusCompleted; 423 | case AVAssetWriterStatusCancelled: 424 | return AVAssetExportSessionStatusCancelled; 425 | } 426 | } 427 | 428 | - (void)cancelExport 429 | { 430 | if (self.inputQueue) 431 | { 432 | dispatch_async(self.inputQueue, ^ 433 | { 434 | [self.writer cancelWriting]; 435 | [self.reader cancelReading]; 436 | [self complete]; 437 | [self reset]; 438 | }); 439 | } 440 | } 441 | 442 | - (void)reset 443 | { 444 | _error = nil; 445 | self.progress = 0; 446 | self.reader = nil; 447 | self.videoOutput = nil; 448 | self.audioOutput = nil; 449 | self.writer = nil; 450 | self.videoInput = nil; 451 | self.videoPixelBufferAdaptor = nil; 452 | self.audioInput = nil; 453 | self.inputQueue = nil; 454 | self.completionHandler = nil; 455 | } 456 | 457 | @end 458 | -------------------------------------------------------------------------------- /SDAVAssetExportSession.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "SDAVAssetExportSession" 3 | s.version = "0.0.4" 4 | s.summary = "AVAssetExportSession drop-in replacement with customizable audio&video settings" 5 | s.description = <<-DESC 6 | AVAssetExportSession drop-in remplacement with customizable audio&video settings. 7 | 8 | You want the ease of use of AVAssetExportSession but default provided presets doesn't fit your needs? You then began to read documentation for AVAssetWriter, AVAssetWriterInput, AVAssetReader, AVAssetReaderVideoCompositionOutput, AVAssetReaderAudioMixOutput… and you went out of aspirin? SDAVAssetExportSession is a rewrite of AVAssetExportSession on top of AVAssetReader* and AVAssetWriter*. Unlike AVAssetExportSession, you are not limited to a set of presets – you have full access over audio and video settings. 9 | DESC 10 | s.homepage = "https://github.com/rs/SDAVAssetExportSession" 11 | s.license = { :type => "MIT", :file => "LICENSE" } 12 | s.author = "Olivier Poitrey" 13 | s.platform = :ios, "6.0" 14 | s.source = { :git => "https://github.com/rs/SDAVAssetExportSession.git", :commit => "726f571" } 15 | s.source_files = "**/*.{h,m}" 16 | s.requires_arc = true 17 | end 18 | --------------------------------------------------------------------------------