├── README.md ├── VideoHandleUtils.h └── VideoHandleUtils.m /README.md: -------------------------------------------------------------------------------- 1 | # VideoHandleUtils 2 | 3 | ### 相册导出MP4视频,*视频角度校正,AVMutableComposition处理 4 | ### 获取视频图片 5 | ### 摄像头麦克风权限判断 6 | ### 等 7 | -------------------------------------------------------------------------------- /VideoHandleUtils.h: -------------------------------------------------------------------------------- 1 | // 2 | // VideoHandleUtils.h 3 | // TaQu 4 | // 5 | // Created by Soldier on 2017/4/19. 6 | // Copyright © 2017年 Shaojie Hong. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | 13 | @interface VideoHandleUtils : NSObject 14 | 15 | + (BOOL)isMOVVideo:(NSString *)videoPath; 16 | 17 | //转换成MP4文件 18 | - (void)changeMovToMp4:(NSURL *)mediaURL 19 | targetPath:(NSString *)targetPath 20 | dataBlock:(void (^)(UIImage *movieImage))handler; 21 | 22 | //获取视频封面 23 | - (void)movieToImageHandler:(NSURL *)url 24 | handler:(void (^)(UIImage *movieImage))handler; 25 | 26 | /** 27 | * mov格式转mp4格式 28 | * sourceUrl 原始文件NSUrl 29 | * resultPath 输出文件路径 NSString documents路径 Appending file name 30 | * 方向纠正 31 | */ 32 | - (void)movFileTransformToMP4WithSourceUrl:(NSURL *)sourceUrl 33 | outputPath:(NSString *)outputPath 34 | completion:(void(^)(NSString *mp4FilePath, NSString *errorMsg))comepleteBlock; 35 | 36 | - (void)removeLocalVideoFile:(NSString *)filePath; 37 | 38 | + (NSString *)getMP4FilePath:(NSString *)fileName; 39 | 40 | + (NSString *)getMP4FileName; 41 | 42 | - (void)removeFile:(NSURL *)url; 43 | 44 | /** 45 | * 获取视频方向(角度) 46 | */ 47 | + (NSUInteger)degressFromVideoFileWithAsset:(AVAsset *)asset; 48 | 49 | + (void)requestCameraPermission:(void (^)(BOOL granted))completionBlock; 50 | 51 | + (void)requestMicrophonePermission:(void (^)(BOOL granted))completionBlock; 52 | 53 | + (BOOL)isFrontCameraAvailable; 54 | 55 | + (BOOL)isRearCameraAvailable; 56 | 57 | + (BOOL)isAVCaptureDeviceAuthorization; 58 | 59 | 60 | @end 61 | -------------------------------------------------------------------------------- /VideoHandleUtils.m: -------------------------------------------------------------------------------- 1 | // 2 | // VideoHandleUtils.m 3 | // TaQu 4 | // 5 | // Created by Soldier on 2017/4/19. 6 | // Copyright © 2017年 Shaojie Hong. All rights reserved. 7 | // 8 | 9 | #import "VideoHandleUtils.h" 10 | 11 | #define degreesToRadians(x) (M_PI * x / 180.0) 12 | #define radiansToDegrees(x) (180.0 * x / M_PI) 13 | 14 | @implementation VideoHandleUtils 15 | 16 | + (BOOL)isMOVVideo:(NSString *)videoPath { 17 | NSRange range = [videoPath rangeOfString:@"trim."];//匹配得到的下标 18 | if (range.length == 0) { 19 | return NO; 20 | } 21 | NSString *content = [videoPath substringFromIndex:range.location + 5]; 22 | //视频的后缀 23 | NSRange rangeSuffix = [content rangeOfString:@"."]; 24 | if (rangeSuffix.length == 0) { 25 | return NO; 26 | } 27 | NSString *suffixName = [content substringFromIndex:rangeSuffix.location + 1]; 28 | //如果视频是mov格式的则转为MP4的 29 | if ([suffixName isEqualToString:@"MOV"] || [suffixName isEqualToString:@"mov"]) { 30 | return YES; 31 | } 32 | return NO; 33 | } 34 | 35 | - (void)changeMovToMp4:(NSURL *)mediaURL 36 | targetPath:(NSString *)targetPath 37 | dataBlock:(void (^)(UIImage *movieImage))handler { 38 | AVAsset *video = [AVAsset assetWithURL:mediaURL]; 39 | AVAssetExportSession *exportSession = [AVAssetExportSession exportSessionWithAsset:video presetName:AVAssetExportPresetMediumQuality]; 40 | exportSession.shouldOptimizeForNetworkUse = YES; 41 | exportSession.outputFileType = AVFileTypeMPEG4; 42 | 43 | NSString *videoPath = [targetPath stringByAppendingPathComponent:targetPath]; 44 | exportSession.outputURL = [NSURL fileURLWithPath:videoPath]; 45 | [exportSession exportAsynchronouslyWithCompletionHandler:^{ 46 | NSURL *url = [NSURL fileURLWithPath:videoPath]; 47 | [self movieToImageHandler:url handler:handler]; 48 | }]; 49 | } 50 | 51 | //获取视频第一帧的图片 52 | - (void)movieToImageHandler:(NSURL *)url 53 | handler:(void (^)(UIImage *movieImage))handler { 54 | // NSURL *url = [NSURL fileURLWithPath:path]; 55 | AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:url options:nil]; 56 | AVAssetImageGenerator *generator = [[AVAssetImageGenerator alloc] initWithAsset:asset]; 57 | generator.appliesPreferredTrackTransform = TRUE; 58 | CMTime thumbTime = CMTimeMakeWithSeconds(0, 60); 59 | generator.apertureMode = AVAssetImageGeneratorApertureModeEncodedPixels; 60 | AVAssetImageGeneratorCompletionHandler generatorHandler = 61 | ^(CMTime requestedTime, CGImageRef im, CMTime actualTime, AVAssetImageGeneratorResult result, NSError *error){ 62 | if (result == AVAssetImageGeneratorSucceeded) { 63 | UIImage *thumbImg = [UIImage imageWithCGImage:im]; 64 | if (handler) { 65 | dispatch_async(dispatch_get_main_queue(), ^{ 66 | handler(thumbImg); 67 | }); 68 | } 69 | } else { 70 | if (handler) { 71 | dispatch_async(dispatch_get_main_queue(), ^{ 72 | handler(nil); 73 | }); 74 | } 75 | } 76 | }; 77 | [generator generateCGImagesAsynchronouslyForTimes: 78 | [NSArray arrayWithObject:[NSValue valueWithCMTime:thumbTime]] completionHandler:generatorHandler]; 79 | } 80 | 81 | /** 82 | * mov格式转mp4格式 83 | * sourceUrl 原始文件NSUrl 84 | * resultPath 输出文件路径 NSString documents路径 Appending file name 85 | * 方向纠正 86 | */ 87 | - (void)movFileTransformToMP4WithSourceUrl:(NSURL *)sourceUrl 88 | outputPath:(NSString *)outputPath 89 | completion:(void(^)(NSString *mp4FilePath, NSString *errorMsg))comepleteBlock { 90 | AVURLAsset *asset = [AVURLAsset URLAssetWithURL:sourceUrl options:nil]; 91 | 92 | AVMutableComposition *composition; 93 | AVMutableVideoComposition *videoComposition; 94 | AVMutableVideoCompositionInstruction *instruction; 95 | 96 | AVMutableVideoCompositionLayerInstruction *layerInstruction = nil; 97 | CGAffineTransform t1; 98 | CGAffineTransform t2; 99 | 100 | AVAssetTrack *assetVideoTrack = nil; 101 | AVAssetTrack *assetAudioTrack = nil; 102 | // Check if the asset contains video and audio tracks 103 | if ([[asset tracksWithMediaType:AVMediaTypeVideo] count] != 0) { 104 | assetVideoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]; 105 | } 106 | if ([[asset tracksWithMediaType:AVMediaTypeAudio] count] != 0) { 107 | assetAudioTrack = [[asset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0]; 108 | } 109 | CMTime insertionPoint = kCMTimeInvalid; 110 | NSError *error = nil; 111 | 112 | //composition 113 | composition = [AVMutableComposition composition]; 114 | if (assetVideoTrack != nil) { 115 | AVMutableCompositionTrack *compositionVideoTrack = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid]; 116 | [compositionVideoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, [asset duration]) ofTrack:assetVideoTrack atTime:insertionPoint error:&error]; 117 | } 118 | if (assetAudioTrack != nil) { 119 | AVMutableCompositionTrack *compositionAudioTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid]; 120 | [compositionAudioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, [asset duration]) ofTrack:assetAudioTrack atTime:insertionPoint error:&error]; 121 | } 122 | 123 | //方向校正 124 | float width = assetVideoTrack.naturalSize.width; 125 | float height = assetVideoTrack.naturalSize.height; 126 | float toDiagonal = sqrt(width * width + height * height); 127 | float toDiagonalAngle = radiansToDegrees(acosf(width / toDiagonal)); 128 | float toDiagonalAngle2 = 90 - radiansToDegrees(acosf(width/toDiagonal)); 129 | 130 | float toDiagonalAngleComple; 131 | float toDiagonalAngleComple2; 132 | float finalHeight; 133 | float finalWidth; 134 | 135 | NSInteger degrees = [self.class degressFromVideoFileWithAsset:asset]; 136 | 137 | if(degrees >= 0 && degrees <= 90){ 138 | toDiagonalAngleComple = toDiagonalAngle + degrees; 139 | toDiagonalAngleComple2 = toDiagonalAngle2 + degrees; 140 | 141 | finalHeight = ABS(toDiagonal * sinf(degreesToRadians(toDiagonalAngleComple))); 142 | finalWidth = ABS(toDiagonal * sinf(degreesToRadians(toDiagonalAngleComple2))); 143 | 144 | t1 = CGAffineTransformMakeTranslation(height * sinf(degreesToRadians(degrees)), 0.0); 145 | } 146 | else if(degrees > 90 && degrees <= 180){ 147 | 148 | float degrees2 = degrees - 90; 149 | 150 | toDiagonalAngleComple = toDiagonalAngle + degrees2; 151 | toDiagonalAngleComple2 = toDiagonalAngle2 + degrees2; 152 | 153 | finalHeight = ABS(toDiagonal * sinf(degreesToRadians(toDiagonalAngleComple2))); 154 | finalWidth = ABS(toDiagonal * sinf(degreesToRadians(toDiagonalAngleComple))); 155 | 156 | t1 = CGAffineTransformMakeTranslation(width * sinf(degreesToRadians(degrees2)) + height * cosf(degreesToRadians(degrees2)), height * sinf(degreesToRadians(degrees2))); 157 | } 158 | else if(degrees >= -90 && degrees < 0){ 159 | 160 | float degrees2 = degrees - 90; 161 | float degreesabs = ABS(degrees); 162 | 163 | toDiagonalAngleComple = toDiagonalAngle + degrees2; 164 | toDiagonalAngleComple2 = toDiagonalAngle2 + degrees2; 165 | 166 | finalHeight = ABS(toDiagonal * sinf(degreesToRadians(toDiagonalAngleComple2))); 167 | finalWidth = ABS(toDiagonal * sinf(degreesToRadians(toDiagonalAngleComple))); 168 | 169 | t1 = CGAffineTransformMakeTranslation(0, width * sinf(degreesToRadians(degreesabs))); 170 | 171 | } 172 | else if(degrees >= -180 && degrees < -90){ 173 | 174 | float degreesabs = ABS(degrees); 175 | float degreesplus = degreesabs - 90; 176 | 177 | toDiagonalAngleComple = toDiagonalAngle + degrees; 178 | toDiagonalAngleComple2 = toDiagonalAngle2 + degrees; 179 | 180 | finalHeight = ABS(toDiagonal * sinf(degreesToRadians(toDiagonalAngleComple))); 181 | finalWidth = ABS(toDiagonal * sinf(degreesToRadians(toDiagonalAngleComple2))); 182 | 183 | t1 = CGAffineTransformMakeTranslation(width * sinf(degreesToRadians(degreesplus)), height * sinf(degreesToRadians(degreesplus)) + width * cosf(degreesToRadians(degreesplus))); 184 | } 185 | 186 | t2 = CGAffineTransformRotate(t1, degreesToRadians(degrees)); 187 | 188 | //videoComposition 189 | videoComposition = [AVMutableVideoComposition videoComposition]; 190 | videoComposition.renderSize = CGSizeMake(finalWidth, finalHeight); 191 | videoComposition.frameDuration = CMTimeMake(1, 30); // 根据实际情况获取,帧率很好获取,写个方法就OK 192 | 193 | instruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction]; 194 | 195 | instruction.timeRange = CMTimeRangeMake(kCMTimeZero, [composition duration]); 196 | 197 | layerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:[composition.tracks objectAtIndex:0]]; 198 | [layerInstruction setTransform:t2 atTime:kCMTimeZero]; 199 | 200 | instruction.layerInstructions = [NSArray arrayWithObject:layerInstruction]; 201 | videoComposition.instructions = [NSArray arrayWithObject:instruction]; 202 | 203 | //exportSession 204 | // NSLog(@"origin: mp4 file size:%lf MB", [NSData dataWithContentsOfURL:sourceUrl].length/1024.f/1024.f); 205 | NSString *presetName = AVAssetExportPresetMediumQuality; 206 | NSArray *compatiblePresets = [AVAssetExportSession exportPresetsCompatibleWithAsset:composition]; 207 | if ([compatiblePresets containsObject:AVAssetExportPreset960x540]) { 208 | presetName = AVAssetExportPreset960x540; //16 : 9 209 | } 210 | 211 | AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:composition presetName:presetName] ; 212 | exportSession.outputURL = [NSURL fileURLWithPath:outputPath]; 213 | exportSession.outputFileType = AVFileTypeMPEG4; 214 | exportSession.videoComposition = videoComposition; 215 | exportSession.shouldOptimizeForNetworkUse = YES; 216 | exportSession.timeRange = CMTimeRangeMake(kCMTimeZero, asset.duration); 217 | 218 | [exportSession exportAsynchronouslyWithCompletionHandler:^(void) { 219 | // dispatch_async(dispatch_get_main_queue(), ^{ 220 | // 221 | // }); 222 | 223 | switch (exportSession.status) { 224 | case AVAssetExportSessionStatusCompleted:{ 225 | NSUInteger length = [NSData dataWithContentsOfURL:exportSession.outputURL].length /1024.f / 1024.f; 226 | NSString *errorMsg = nil; 227 | if (length > 15) { 228 | errorMsg = @"视频过大,无法发送哦"; 229 | } 230 | // NSLog(@"after mp4 file size:%lu MB", (unsigned long)length); 231 | comepleteBlock(outputPath, errorMsg); 232 | 233 | } 234 | break; 235 | 236 | case AVAssetExportSessionStatusUnknown: 237 | 238 | break; 239 | 240 | case AVAssetExportSessionStatusWaiting: 241 | 242 | break; 243 | 244 | case AVAssetExportSessionStatusExporting: 245 | 246 | break; 247 | 248 | case AVAssetExportSessionStatusFailed: 249 | 250 | break; 251 | 252 | case AVAssetExportSessionStatusCancelled: 253 | 254 | break; 255 | } 256 | }]; 257 | } 258 | 259 | - (void)removeLocalVideoFile:(NSString *)filePath { 260 | if (filePath.length > 0) { 261 | NSString *path = filePath; 262 | path = [path stringByReplacingOccurrencesOfString:@"file://" withString:@""]; 263 | if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { 264 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 265 | NSError *error; 266 | [[NSFileManager defaultManager] removeItemAtPath:path error:&error]; 267 | if (error) { 268 | // NSLog(@"file remove error: %@", error); 269 | } 270 | }); 271 | } 272 | } 273 | } 274 | 275 | + (NSString *)getMP4FilePath:(NSString *)fileName { 276 | NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 277 | NSString *path = [paths objectAtIndex:0]; 278 | 279 | NSString *filePath = [NSString stringWithFormat:@"%@.mp4", fileName]; 280 | return [path stringByAppendingPathComponent:filePath]; 281 | } 282 | 283 | + (NSString *)getMP4FileName { 284 | NSInteger random = arc4random(); 285 | random = random < 0 ? -random : random; 286 | NSString *fileName = [NSString stringWithFormat:@"taqu_ios_post_video_%ld_%.0f", (long)random, [[NSDate date] timeIntervalSince1970]]; 287 | return fileName; 288 | } 289 | 290 | - (void)removeFile:(NSURL *)url { 291 | if (!url) { 292 | return; 293 | } 294 | NSString *filePath = [NSString stringWithFormat:@"%@", url]; 295 | if (filePath.length > 0) { 296 | NSString *path = filePath; 297 | path = [path stringByReplacingOccurrencesOfString:@"file://" withString:@""]; 298 | if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { 299 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 300 | NSError *error; 301 | [[NSFileManager defaultManager] removeItemAtPath:path error:&error]; 302 | if (error) { 303 | // NSLog(@"file remove error: %@", error); 304 | } 305 | }); 306 | } 307 | } 308 | } 309 | 310 | /* 311 | * 解决录像保存角度问题 312 | * 有问题,微信录制的视频只有声音? 313 | */ 314 | - (AVMutableVideoComposition *)getVideoComposition:(AVAsset *)asset { 315 | NSArray *tracks = [asset tracksWithMediaType:AVMediaTypeVideo]; 316 | if (tracks.count <= 0) { 317 | return nil; 318 | } 319 | AVAssetTrack *videoTrack = [tracks objectAtIndex:0]; 320 | AVMutableComposition *composition = [AVMutableComposition composition]; 321 | AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition]; 322 | CGSize videoSize = videoTrack.naturalSize; 323 | NSUInteger degress = [self.class degressFromVideoFileWithAsset:asset]; 324 | if(degress == 90 || degress == 270) { 325 | // NSLog(@"video is portrait "); 326 | videoSize = CGSizeMake(videoSize.height, videoSize.width); 327 | } 328 | composition.naturalSize = videoSize; 329 | videoComposition.renderSize = videoSize; 330 | // videoComposition.renderSize = videoTrack.naturalSize; // 331 | videoComposition.frameDuration = CMTimeMakeWithSeconds(1 / videoTrack.nominalFrameRate, 600); 332 | 333 | AVMutableCompositionTrack *compositionVideoTrack; 334 | compositionVideoTrack = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid]; 335 | [compositionVideoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration) ofTrack:videoTrack atTime:kCMTimeZero error:nil]; 336 | AVMutableVideoCompositionLayerInstruction *layerInst; 337 | layerInst = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoTrack]; 338 | [layerInst setTransform:videoTrack.preferredTransform atTime:kCMTimeZero]; 339 | AVMutableVideoCompositionInstruction *inst = [AVMutableVideoCompositionInstruction videoCompositionInstruction]; 340 | inst.timeRange = CMTimeRangeMake(kCMTimeZero, asset.duration); 341 | inst.layerInstructions = [NSArray arrayWithObject:layerInst]; 342 | videoComposition.instructions = [NSArray arrayWithObject:inst]; 343 | return videoComposition; 344 | } 345 | 346 | /** 347 | * 获取视频方向(角度) 348 | */ 349 | + (NSUInteger)degressFromVideoFileWithAsset:(AVAsset *)asset{ 350 | NSUInteger degress = 0; 351 | // AVAsset *asset = [AVAsset assetWithURL:url]; 352 | NSArray *tracks = [asset tracksWithMediaType:AVMediaTypeVideo]; 353 | if([tracks count] > 0) { 354 | AVAssetTrack *videoTrack = [tracks objectAtIndex:0]; 355 | CGAffineTransform t = videoTrack.preferredTransform; 356 | 357 | degress = atan2(t.b, t.a) * 180 / M_PI; 358 | } 359 | return degress; 360 | } 361 | 362 | + (void)requestCameraPermission:(void (^)(BOOL granted))completionBlock { 363 | if ([AVCaptureDevice respondsToSelector:@selector(requestAccessForMediaType: completionHandler:)]) { 364 | [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { 365 | dispatch_async(dispatch_get_main_queue(), ^{ 366 | if(completionBlock) { 367 | completionBlock(granted); 368 | } 369 | }); 370 | }]; 371 | } else { 372 | completionBlock(YES); 373 | } 374 | } 375 | 376 | + (void)requestMicrophonePermission:(void (^)(BOOL granted))completionBlock { 377 | if([[AVAudioSession sharedInstance] respondsToSelector:@selector(requestRecordPermission:)]) { 378 | [[AVAudioSession sharedInstance] requestRecordPermission:^(BOOL granted) { 379 | // return to main thread 380 | dispatch_async(dispatch_get_main_queue(), ^{ 381 | if(completionBlock) { 382 | completionBlock(granted); 383 | } 384 | }); 385 | }]; 386 | } 387 | } 388 | 389 | + (BOOL)isFrontCameraAvailable { 390 | return [UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceFront]; 391 | } 392 | 393 | + (BOOL)isRearCameraAvailable { 394 | return [UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceRear]; 395 | } 396 | 397 | + (BOOL)isAVCaptureDeviceAuthorization { 398 | AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; 399 | if (authStatus == AVAuthorizationStatusRestricted || authStatus == AVAuthorizationStatusDenied) { 400 | return NO; 401 | } else { 402 | return YES; 403 | } 404 | } 405 | 406 | @end 407 | --------------------------------------------------------------------------------