前几个月的时候自己动手写了一个相册选择器,如下(UI仿抖音):
首先说一下我获取视频的过程,使用PHImageManager类进行获取视频资源AVAsset,然后使用AVAssetExportSession类进行导出视频,导出操作之前可以告诉AVAssetExportSession实例一些导出的规格,将这些规格封装在AVVideoComposition实例中,并传递给AVAssetExportSession实例的videoComposition属性。这里我在videoComposition的instructions中提供了旋转角度(参考了TZImagePickerController代码)。
获取相册资源代码如下:
[SVProgressHUD show];
PHVideoRequestOptions* options = [[PHVideoRequestOptions alloc] init];
options.version = PHVideoRequestOptionsVersionOriginal;
options.deliveryMode = PHVideoRequestOptionsDeliveryModeAutomatic;
options.networkAccessAllowed = YES;
__weak typeof(self) weakSelf = self;
options.progressHandler = ^(double progress, NSError * _Nullable error, BOOL * _Nonnull stop, NSDictionary * _Nullable info) {
dispatch_async(dispatch_get_main_queue(), ^{
[SVProgressHUD showProgress:progress status:@"iCloud同步中"];
});
};
[[PHImageManager defaultManager] requestAVAssetForVideo:phAsset options:options resultHandler:^(AVAsset * _Nullable asset, AVAudioMix * _Nullable audioMix, NSDictionary * _Nullable info) {
if (asset) {
AVURLAsset *urlAsset = (AVURLAsset *)asset;
[weakSelf startExportVideoWithVideoAsset:urlAsset presetName:AVAssetExportPresetHighestQuality success:^(NSString *outputPath) {
[SVProgressHUD dismiss];
[weakSelf dismissViewControllerAnimated:YES completion:nil];
if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(didFinishPickVideoUrl:)]) {
[weakSelf.delegate didFinishPickVideoUrl:[NSURL fileURLWithPath:outputPath]];
}
} failure:^(NSString *errorMessage, NSError *error) {
[SVProgressHUD showErrorWithStatus:errorMessage];
}];
}else {
[SVProgressHUD dismiss];
}
}];
封装AVVideoComposition实例,并赋值给AVAssetExportSession实例的videoComposition属性,如下:
- (void)startExportVideoWithVideoAsset:(AVURLAsset *)videoAsset presetName:(NSString *)presetName success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure {
NSArray *presets = [AVAssetExportSession exportPresetsCompatibleWithAsset:videoAsset];
if ([presets containsObject:presetName]) {
AVAssetExportSession *session = [[AVAssetExportSession alloc] initWithAsset:videoAsset presetName:presetName];
NSDateFormatter *formater = [[NSDateFormatter alloc] init];
[formater setDateFormat:@"yyyy-MM-dd-HH:mm:ss-SSS"];
NSString *outputPath = [NSHomeDirectory() stringByAppendingFormat:@"/tmp/video-%@.mp4", [formater stringFromDate:[NSDate date]]];
if (videoAsset.URL && videoAsset.URL.lastPathComponent) {
outputPath = [outputPath stringByReplacingOccurrencesOfString:@".mp4" withString:[NSString stringWithFormat:@"-%@", videoAsset.URL.lastPathComponent]];
}
NSLog(@"video outputPath = %@",outputPath);
session.outputURL = [NSURL fileURLWithPath:outputPath];
// Optimize for network use.
session.shouldOptimizeForNetworkUse = true;
NSArray *supportedTypeArray = session.supportedFileTypes;
if ([supportedTypeArray containsObject:AVFileTypeMPEG4]) {
session.outputFileType = AVFileTypeMPEG4;
} else if (supportedTypeArray.count == 0) {
if (failure) {
failure(@"该视频类型暂不支持导出", nil);
}
NSLog(@"No supported file types 视频类型暂不支持导出");
return;
} else {
session.outputFileType = [supportedTypeArray objectAtIndex:0];
}
if (![[NSFileManager defaultManager] fileExistsAtPath:[NSHomeDirectory() stringByAppendingFormat:@"/tmp"]]) {
[[NSFileManager defaultManager] createDirectoryAtPath:[NSHomeDirectory() stringByAppendingFormat:@"/tmp"] withIntermediateDirectories:YES attributes:nil error:nil];
}
AVMutableVideoComposition *videoComposition = [self fixedCompositionWithAsset:videoAsset];
if (videoComposition.renderSize.width) {
// 修正视频转向
session.videoComposition = videoComposition;
}
// Begin to export video to the output path asynchronously.
[session exportAsynchronouslyWithCompletionHandler:^(void) {
dispatch_async(dispatch_get_main_queue(), ^{
switch (session.status) {
case AVAssetExportSessionStatusUnknown: {
NSLog(@"AVAssetExportSessionStatusUnknown");
} break;
case AVAssetExportSessionStatusWaiting: {
NSLog(@"AVAssetExportSessionStatusWaiting");
} break;
case AVAssetExportSessionStatusExporting: {
NSLog(@"AVAssetExportSessionStatusExporting");
} break;
case AVAssetExportSessionStatusCompleted: {
NSLog(@"AVAssetExportSessionStatusCompleted");
if (success) {
success(outputPath);
}
} break;
case AVAssetExportSessionStatusFailed: {
NSLog(@"AVAssetExportSessionStatusFailed");
if (failure) {
failure(@"视频导出失败", session.error);
}
} break;
case AVAssetExportSessionStatusCancelled: {
NSLog(@"AVAssetExportSessionStatusCancelled");
if (failure) {
failure(@"导出任务已被取消", nil);
}
} break;
default: break;
}
});
}];
} else {
if (failure) {
NSString *errorMessage = [NSString stringWithFormat:@"当前设备不支持该预设:%@", presetName];
failure(errorMessage, nil);
}
}
}
通过上述操作,使用AVAssetExportSession导出视频就会失败(AVAssetExportSessionStatusFailed),然后我在stackoverflow上搜索关键字AVAssetExportSession和 mp4。找到这个链接,这个人遇到的问题跟我一模一样,但是并没有人提出一个正确的解决方案。后来又找这个链接,这个人的解决方案是把视频资源AVURLAsset提取音频轨道和视频轨道,重新构建AVComposition,然后在使用AVAssetExportSession导出,代码如下:
- (void)startExportMP4VideoWithVideoAsset:(AVURLAsset *)videoAsset presetName:(NSString *)presetName success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure {
AVMutableComposition *composition = [AVMutableComposition composition];
AVMutableCompositionTrack *compositionVideoTrack = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
AVMutableCompositionTrack *compositionAudioTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
AVAssetTrack *sourceVideoTrack = [videoAsset tracksWithMediaType:AVMediaTypeVideo].firstObject;
AVAssetTrack *sourceAudioTrack = [videoAsset tracksWithMediaType:AVMediaTypeAudio].firstObject;
[compositionVideoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.duration) ofTrack:sourceVideoTrack atTime:kCMTimeZero error:nil];
[compositionAudioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.duration) ofTrack:sourceAudioTrack atTime:kCMTimeZero error:nil];
NSArray *presets = [AVAssetExportSession exportPresetsCompatibleWithAsset:composition];
if ([presets containsObject:presetName]) {
AVAssetExportSession *session = [[AVAssetExportSession alloc] initWithAsset:composition presetName:presetName];
// 输出地址
NSDateFormatter *formater = [[NSDateFormatter alloc] init];
[formater setDateFormat:@"yyyy-MM-dd-HH:mm:ss-SSS"];
NSString *outputPath = [NSHomeDirectory() stringByAppendingFormat:@"/tmp/video-%@.mp4", [formater stringFromDate:[NSDate date]]];
session.outputURL = [NSURL fileURLWithPath:outputPath];
// 优化
session.shouldOptimizeForNetworkUse = YES;
NSArray *supportedTypeArray = session.supportedFileTypes;
if ([supportedTypeArray containsObject:AVFileTypeMPEG4]) {
session.outputFileType = AVFileTypeMPEG4;
} else if (supportedTypeArray.count == 0) {
if (failure) {
failure(@"该视频类型暂不支持导出", nil);
}
NSLog(@"No supported file types 视频类型暂不支持导出");
return;
} else {
session.outputFileType = [supportedTypeArray objectAtIndex:0];
}
if (![[NSFileManager defaultManager] fileExistsAtPath:[NSHomeDirectory() stringByAppendingFormat:@"/tmp"]]) {
[[NSFileManager defaultManager] createDirectoryAtPath:[NSHomeDirectory() stringByAppendingFormat:@"/tmp"] withIntermediateDirectories:YES attributes:nil error:nil];
}
AVMutableVideoComposition *videoComposition = [self fixedCompositionWithAsset:videoAsset];
if (videoComposition.renderSize.width) {
// 修正视频转向
session.videoComposition = videoComposition;
}
// 合成完毕
[session exportAsynchronouslyWithCompletionHandler:^{
dispatch_async(dispatch_get_main_queue(), ^{
switch (session.status) {
case AVAssetExportSessionStatusUnknown: {
NSLog(@"AVAssetExportSessionStatusUnknown");
} break;
case AVAssetExportSessionStatusWaiting: {
NSLog(@"AVAssetExportSessionStatusWaiting");
} break;
case AVAssetExportSessionStatusExporting: {
NSLog(@"AVAssetExportSessionStatusExporting");
} break;
case AVAssetExportSessionStatusCompleted: {
NSLog(@"AVAssetExportSessionStatusCompleted");
if (success) {
success(outputPath);
}
} break;
case AVAssetExportSessionStatusFailed: {
NSLog(@"AVAssetExportSessionStatusFailed");
if (failure) {
failure(@"视频导出失败", session.error);
}
} break;
case AVAssetExportSessionStatusCancelled: {
NSLog(@"AVAssetExportSessionStatusCancelled");
if (failure) {
failure(@"导出任务已被取消", nil);
}
} break;
default: break;
}
});
}];
} else {
if (failure) {
NSString *errorMessage = [NSString stringWithFormat:@"当前设备不支持该预设:%@", presetName];
failure(errorMessage, nil);
}
}
}
这时候可以顺利导出这个有问题的MP4文件,但是出现了另一个问题,就是iPhone自己录制的MP4文件在选择后,导出的视频逆时针旋转了90度,就在那一瞬间,整个世界晴转多云。
问题就在于以下这部分代码:
AVMutableVideoComposition *videoComposition = [self fixedCompositionWithAsset:videoAsset];
if (videoComposition.renderSize.width) {
// 修正视频转向
session.videoComposition = videoComposition;
}
fixedCompositionWithAsset方法内部实现是这样的:
/// 获取优化后的视频转向信息
- (AVMutableVideoComposition *)fixedCompositionWithAsset:(AVAsset *)videoAsset {
AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition];
// 视频转向
int degrees = [self degressFromVideoFileWithAsset:videoAsset];
if (degrees != 0) {
CGAffineTransform translateToCenter;
CGAffineTransform mixedTransform;
videoComposition.frameDuration = CMTimeMake(1, 30);
NSArray *tracks = [videoAsset tracksWithMediaType:AVMediaTypeVideo];
AVAssetTrack *videoTrack = [tracks objectAtIndex:0];
AVMutableVideoCompositionInstruction *roateInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
roateInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, [videoAsset duration]);
AVMutableVideoCompositionLayerInstruction *roateLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoTrack];
if (degrees == 90) {
// 顺时针旋转90°
translateToCenter = CGAffineTransformMakeTranslation(videoTrack.naturalSize.height, 0.0);
mixedTransform = CGAffineTransformRotate(translateToCenter,M_PI_2);
videoComposition.renderSize = CGSizeMake(videoTrack.naturalSize.height,videoTrack.naturalSize.width);
[roateLayerInstruction setTransform:mixedTransform atTime:kCMTimeZero];
} else if(degrees == 180){
// 顺时针旋转180°
translateToCenter = CGAffineTransformMakeTranslation(videoTrack.naturalSize.width, videoTrack.naturalSize.height);
mixedTransform = CGAffineTransformRotate(translateToCenter,M_PI);
videoComposition.renderSize = CGSizeMake(videoTrack.naturalSize.width,videoTrack.naturalSize.height);
[roateLayerInstruction setTransform:mixedTransform atTime:kCMTimeZero];
} else if(degrees == 270){
// 顺时针旋转270°
translateToCenter = CGAffineTransformMakeTranslation(0.0, videoTrack.naturalSize.width);
mixedTransform = CGAffineTransformRotate(translateToCenter,M_PI_2*3.0);
videoComposition.renderSize = CGSizeMake(videoTrack.naturalSize.height,videoTrack.naturalSize.width);
[roateLayerInstruction setTransform:mixedTransform atTime:kCMTimeZero];
}
roateInstruction.layerInstructions = @[roateLayerInstruction];
// 加入视频方向信息
videoComposition.instructions = @[roateInstruction];
}
return videoComposition;
}
/// 获取视频角度
- (int)degressFromVideoFileWithAsset:(AVAsset *)asset {
int degress = 0;
NSArray *tracks = [asset tracksWithMediaType:AVMediaTypeVideo];
if([tracks count] > 0) {
AVAssetTrack *videoTrack = [tracks objectAtIndex:0];
CGAffineTransform t = videoTrack.preferredTransform;
if(t.a == 0 && t.b == 1.0 && t.c == -1.0 && t.d == 0){
// Portrait
degress = 90;
} else if(t.a == 0 && t.b == -1.0 && t.c == 1.0 && t.d == 0){
// PortraitUpsideDown
degress = 270;
} else if(t.a == 1.0 && t.b == 0 && t.c == 0 && t.d == 1.0){
// LandscapeRight
degress = 0;
} else if(t.a == -1.0 && t.b == 0 && t.c == 0 && t.d == -1.0){
// LandscapeLeft
degress = 180;
}
}
return degress;
}
因为我们要根据视频资源AVURLAsset重新构建新实例,构建结果为AVMutableComposition实例,而需要修正角度的应该是AVMutableComposition实例,而不是之前的AVURLAsset实例。AVMutableComposition实例的角度应该是AVURLAsset的角度,所以代码如下:
- (void)startExportMP4VideoWithVideoAsset:(AVURLAsset *)videoAsset presetName:(NSString *)presetName success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure {
AVMutableComposition *composition = [AVMutableComposition composition];
AVMutableCompositionTrack *compositionVideoTrack = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
AVMutableCompositionTrack *compositionAudioTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
AVAssetTrack *sourceVideoTrack = [videoAsset tracksWithMediaType:AVMediaTypeVideo].firstObject;
AVAssetTrack *sourceAudioTrack = [videoAsset tracksWithMediaType:AVMediaTypeAudio].firstObject;
[compositionVideoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.duration) ofTrack:sourceVideoTrack atTime:kCMTimeZero error:nil];
[compositionAudioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.duration) ofTrack:sourceAudioTrack atTime:kCMTimeZero error:nil];
NSArray *presets = [AVAssetExportSession exportPresetsCompatibleWithAsset:composition];
if ([presets containsObject:presetName]) {
AVAssetExportSession *session = [[AVAssetExportSession alloc] initWithAsset:composition presetName:presetName];
// 输出地址
NSDateFormatter *formater = [[NSDateFormatter alloc] init];
[formater setDateFormat:@"yyyy-MM-dd-HH:mm:ss-SSS"];
NSString *outputPath = [NSHomeDirectory() stringByAppendingFormat:@"/tmp/video-%@.mp4", [formater stringFromDate:[NSDate date]]];
session.outputURL = [NSURL fileURLWithPath:outputPath];
// 优化
session.shouldOptimizeForNetworkUse = YES;
NSArray *supportedTypeArray = session.supportedFileTypes;
if ([supportedTypeArray containsObject:AVFileTypeMPEG4]) {
session.outputFileType = AVFileTypeMPEG4;
} else if (supportedTypeArray.count == 0) {
if (failure) {
failure(@"该视频类型暂不支持导出", nil);
}
NSLog(@"No supported file types 视频类型暂不支持导出");
return;
} else {
session.outputFileType = [supportedTypeArray objectAtIndex:0];
}
if (![[NSFileManager defaultManager] fileExistsAtPath:[NSHomeDirectory() stringByAppendingFormat:@"/tmp"]]) {
[[NSFileManager defaultManager] createDirectoryAtPath:[NSHomeDirectory() stringByAppendingFormat:@"/tmp"] withIntermediateDirectories:YES attributes:nil error:nil];
}
AVMutableVideoComposition *videoComposition = [self fixedCompositionWithAsset:composition degrees:[self degressFromVideoFileWithAsset:videoAsset]];
if (videoComposition.renderSize.width) {
// 修正视频转向
session.videoComposition = videoComposition;
}
// 合成完毕
[session exportAsynchronouslyWithCompletionHandler:^{
dispatch_async(dispatch_get_main_queue(), ^{
switch (session.status) {
case AVAssetExportSessionStatusUnknown: {
NSLog(@"AVAssetExportSessionStatusUnknown");
} break;
case AVAssetExportSessionStatusWaiting: {
NSLog(@"AVAssetExportSessionStatusWaiting");
} break;
case AVAssetExportSessionStatusExporting: {
NSLog(@"AVAssetExportSessionStatusExporting");
} break;
case AVAssetExportSessionStatusCompleted: {
NSLog(@"AVAssetExportSessionStatusCompleted");
if (success) {
success(outputPath);
}
} break;
case AVAssetExportSessionStatusFailed: {
NSLog(@"AVAssetExportSessionStatusFailed");
if (failure) {
failure(@"视频导出失败", session.error);
}
} break;
case AVAssetExportSessionStatusCancelled: {
NSLog(@"AVAssetExportSessionStatusCancelled");
if (failure) {
failure(@"导出任务已被取消", nil);
}
} break;
default: break;
}
});
}];
} else {
if (failure) {
NSString *errorMessage = [NSString stringWithFormat:@"当前设备不支持该预设:%@", presetName];
failure(errorMessage, nil);
}
}
}
/// 获取优化后的视频转向信息
- (AVMutableVideoComposition *)fixedCompositionWithAsset:(AVAsset *)videoAsset degrees:(int)degrees {
AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition];
if (degrees != 0) {
CGAffineTransform translateToCenter;
CGAffineTransform mixedTransform;
videoComposition.frameDuration = CMTimeMake(1, 30);
NSArray *tracks = [videoAsset tracksWithMediaType:AVMediaTypeVideo];
AVAssetTrack *videoTrack = [tracks objectAtIndex:0];
AVMutableVideoCompositionInstruction *roateInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
roateInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, [videoAsset duration]);
AVMutableVideoCompositionLayerInstruction *roateLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoTrack];
if (degrees == 90) {
// 顺时针旋转90°
translateToCenter = CGAffineTransformMakeTranslation(videoTrack.naturalSize.height, 0.0);
mixedTransform = CGAffineTransformRotate(translateToCenter,M_PI_2);
videoComposition.renderSize = CGSizeMake(videoTrack.naturalSize.height,videoTrack.naturalSize.width);
[roateLayerInstruction setTransform:mixedTransform atTime:kCMTimeZero];
} else if(degrees == 180){
// 顺时针旋转180°
translateToCenter = CGAffineTransformMakeTranslation(videoTrack.naturalSize.width, videoTrack.naturalSize.height);
mixedTransform = CGAffineTransformRotate(translateToCenter,M_PI);
videoComposition.renderSize = CGSizeMake(videoTrack.naturalSize.width,videoTrack.naturalSize.height);
[roateLayerInstruction setTransform:mixedTransform atTime:kCMTimeZero];
} else if(degrees == 270){
// 顺时针旋转270°
translateToCenter = CGAffineTransformMakeTranslation(0.0, videoTrack.naturalSize.width);
mixedTransform = CGAffineTransformRotate(translateToCenter,M_PI_2*3.0);
videoComposition.renderSize = CGSizeMake(videoTrack.naturalSize.height,videoTrack.naturalSize.width);
[roateLayerInstruction setTransform:mixedTransform atTime:kCMTimeZero];
}
roateInstruction.layerInstructions = @[roateLayerInstruction];
// 加入视频方向信息
videoComposition.instructions = @[roateInstruction];
}
return videoComposition;
}
表述的好像不是很清楚,有不清楚的同学可以私信我。