banchichen/TZImagePickerController

视频导出失败

jiangyuan0336 opened this issue · 35 comments

提bug前必看
请先回答下列三个问题,否则不允处理,谢谢配合。
1、我最新的Demo是否有这个bug?【如果Demo没问题,请升级新版
答:有

2、你用的是什么版本?升级到最新版后是否正常?
答:version 3.2.0,已经是最新的

3、是否有改动过我库内部的代码?【如有,请告诉我你改了什么
答:没有改

bug内容描述
就是相册里面有两个视频,可以选取,但是如果导出的话就出现视频导出失败,TZImagePickerController[54670:16431685] 视频导出失败:视频导出失败,error:Error Domain=AVFoundationErrorDomain Code=-11800 "这项操作无法完成" UserInfo={NSLocalizedFailureReason=发生未知错误(-12780), NSLocalizedDescription=这项操作无法完成, NSUnderlyingError=0x2830ea9a0 {Error Domain=NSOSStatusErrorDomain Code=-12780 "(null)"}}

我如何复现这个bug?
我可以把视频给你

截图

其它说明
有没有其它要补充的?比如你的初始化TZImagePickerController的代码

IMG_3964.mp4.zip
IMG_3966.mp4.zip
这是导出失败的两个视频,麻烦您有时间看看。

@jiangyuan0336 已复现,没啥时间看...
求PR呀...

PR是什么?

@jiangyuan0336 Pull Request哈,你fork TZ这个仓库,修复这个bug,再提交PR到TZ这个仓库,我验证后merge下发个版,以后大家就都可以使用你修复后的版本咯

Lee5s commented

同样遇到了这个问题,
我在getVideoOutputPathWithAsset: presetName: success: failure: 里将presetName 改为了AVAssetExportPresetLowQuality|AVAssetExportPresetMediumQuality 视频可以正常导出, IMG_3964.mp4.zip 也可正常导出 是否会引起其他问题还未验证

视频导出失败:视频导出失败,error:Error Domain=AVFoundationErrorDomain Code=-11800 "这项操作无法完成" UserInfo={NSLocalizedFailureReason=发生未知错误(-12780), NSLocalizedDescription=这项操作无法完成, NSUnderlyingError=0x28146f930 {Error Domain=NSOSStatusErrorDomain Code=-12780 "(null)"}}

只有把presetName 变成AVAssetExportPresetLowQuality才可以

我打印了一下AVAssetExportPreset的枚举值,发现这个视频包含AVAssetExportPresetHigh这个类型,但是导出确实存在问题的

搜了一圈好像挺多人都遇到这个错误,但是都没有合适的解法...

presetName设置为AVAssetExportPresetLowQuality后IMG_3964.mp4确实可以导出了,原因不明...

如果导出压缩失败了 是不是可以直接把这个资源文件写入呢 不通过压缩

- (void)getVideoOutputPathWithAsset:(PHAsset *)asset presetName:(NSString *)presetName success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure {
    PHVideoRequestOptions* options = [[PHVideoRequestOptions alloc] init];
    options.version = PHVideoRequestOptionsVersionOriginal;
    options.deliveryMode = PHVideoRequestOptionsDeliveryModeAutomatic;
    options.networkAccessAllowed = YES;
    [[PHImageManager defaultManager] requestAVAssetForVideo:asset options:options resultHandler:^(AVAsset* avasset, AVAudioMix* audioMix, NSDictionary* info){
        // NSLog(@"Info:\n%@",info);
        AVURLAsset *videoAsset = (AVURLAsset*)avasset;
        //在处理120帧无声音视频的时候,直接崩溃,原因是AVURLAsset *videoAsset = (AVURLAsset *)asset;因为asset是个基类,它有时会是AVComposition而不是AVURLAsset,这样就得不到视频的url,从而导致程序崩溃
        if ([videoAsset isKindOfClass:[AVURLAsset class]]) {
            if (videoAsset.URL){
                NSString *outputPath = self.outPutFile;
                NSData *data = [NSData dataWithContentsOfURL:videoAsset.URL];
                if (data){
                    [data writeToFile:outputPath atomically:YES];
                    success(outputPath);
                }else{
                    failure(@"导出失败", nil);
                }
            }else{
                failure(@"导出失败", nil);
            }
        }else{
            failure(@"导出失败", nil);
        }
    }];
//        [self startExportVideoWithVideoAsset:videoAsset presetName:presetName success:success failure:failure];
}

我现在没有使用他给的指定presetName的方法 而是用的上面这个,就不会出现这样的问题了

NSArray *presets = [AVAssetExportSession exportPresetsCompatibleWithAsset:videoAsset];

NSNumber *size;
[videoAsset.URL getResourceValue:&size forKey:NSURLFileSizeKey error:nil];

if([size floatValue]<4194304){
    presetName= AVAssetExportPresetHighestQuality;
}
//视频压缩
if ([presets containsObject:presetName]) {


    NSError *error = nil;
    AVAssetTrack *assetVideoTrack = nil;
    AVAssetTrack *assetAudioTrack = nil;

    CMTime start = CMTimeMakeWithSeconds(0, videoAsset.duration.timescale);

    CMTimeRange range = CMTimeRangeMake(start, videoAsset.duration);

    // Check if the asset contains video and audio tracks
    if ([[videoAsset tracksWithMediaType:AVMediaTypeVideo] count] != 0) {
        assetVideoTrack = [videoAsset tracksWithMediaType:AVMediaTypeVideo][0];
    }

    if ([[videoAsset tracksWithMediaType:AVMediaTypeAudio] count] != 0) {
        assetAudioTrack = [videoAsset tracksWithMediaType:AVMediaTypeAudio][0];
    }

    CMTime insertionPoint = kCMTimeZero;

    AVMutableComposition *composition = [[AVMutableComposition alloc] init];

    // Insert the video and audio tracks from AVAsset
    if (assetVideoTrack != nil) {
        // 视频通道  工程文件中的轨道,有音频轨、视频轨等,里面可以插入各种对应的素材
        AVMutableCompositionTrack *compositionVideoTrack = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
        // 视频方向
        [compositionVideoTrack setPreferredTransform:assetVideoTrack.preferredTransform];
        // 把视频轨道数据加入到可变轨道中 这部分可以做视频裁剪TimeRange
        [compositionVideoTrack insertTimeRange:range ofTrack:assetVideoTrack atTime:insertionPoint error:&error];
    }


    if (assetAudioTrack != nil) {
        AVMutableCompositionTrack *compositionAudioTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
        compositionAudioTrack.preferredTransform = assetAudioTrack.preferredTransform;
        [compositionAudioTrack insertTimeRange:range ofTrack:assetAudioTrack atTime:insertionPoint error:&error];
    }


    NSDateFormatter *formater = [[NSDateFormatter alloc] init];
    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]];
    }

    AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:composition presetName:presetName];


    NSURL *url =  [NSURL fileURLWithPath:outputPath];


    NSFileManager *fm = [NSFileManager new];

    if ([fm fileExistsAtPath:outputPath]) {
       [fm removeItemAtPath:outputPath error:nil];
    }

    /** 删除原来剪辑的视频 */
    [fm removeItemAtPath:outputPath error:nil];

    exportSession.outputURL = url;

    NSLog(@"%@",outputPath);

    NSArray *supportedTypeArray = exportSession.supportedFileTypes;
    if ([supportedTypeArray containsObject:AVFileTypeMPEG4]) {
        exportSession.outputFileType = AVFileTypeMPEG4;
    } else if (supportedTypeArray.count == 0) {
        if (handler) handler(@"该视频类型暂不支持上传",nil);
        NSLog(@"No supported file types 视频类型暂不支持导出");
        return;
    } else {
        exportSession.outputFileType = [supportedTypeArray objectAtIndex:0];
    }

    if (![[NSFileManager defaultManager] fileExistsAtPath:[NSHomeDirectory() stringByAppendingFormat:@"/tmp"]]) {
        [[NSFileManager defaultManager] createDirectoryAtPath:[NSHomeDirectory() stringByAppendingFormat:@"/tmp"] withIntermediateDirectories:YES attributes:nil error:nil];
    }

    if (videoAsset.duration.timescale == 0 || exportSession == nil) {
        /** 这个情况AVAssetExportSession会卡死 */
        NSError *failError = [NSError errorWithDomain:@"LFVideoExportSessionError" code:(-100) userInfo:@{NSLocalizedDescriptionKey:@"exportSession init fail"}];
        if (handler) handler(@"视频压缩失败",failError);
        return;
    }

    WS(weakSelf)

    [exportSession exportAsynchronouslyWithCompletionHandler:^{

        dispatch_async(dispatch_get_main_queue(), ^{
            switch (exportSession.status) {
                case AVAssetExportSessionStatusFailed:
                    NSLog(@"Export failed: %@", [[exportSession error] localizedDescription]);
                    break;
                case AVAssetExportSessionStatusCancelled:
                    NSLog(@"Export canceled");
                    break;
                case AVAssetExportSessionStatusCompleted:
                    NSLog(@"Export completed");
                    break;
                default:
                    break;
            }
            if ([exportSession status] == AVAssetExportSessionStatusCompleted && [fm fileExistsAtPath:outputPath]) {
               //成功
                [weakSelf uploadVideo:outputPath success:success failure:handler];
            } else {
                if (handler) handler([[exportSession error] localizedDescription],exportSession.error);
            }
        });
    }];

}else{
      if (handler) handler(@"视频压缩失败",nil);
}

使用这个可以成功压缩上面的两个视频 具体原理还在研究

我也遇到了这个问题,即使使用 @mlp1995 的方法,但是只要是某些android上传的视频,使用iOS手机保存到相册,再去压缩导出依然不行。
后来我使用了七牛的框架(PLShortVideoKit)中的PLSAVAssetExportSession类替换了AVAssetExportSession,具体代码如下:

    NSMutableDictionary *outputSettings = [NSMutableDictionary dictionary];
    NSMutableDictionary *movieSettings = [NSMutableDictionary dictionary];
    
    outputSettings[PLSMovieSettingsKey] = movieSettings;
    
    movieSettings[PLSAssetKey] = videoAsset;
    movieSettings[PLSStartTimeKey] = [NSNumber numberWithFloat:0.f];
    movieSettings[PLSDurationKey] = [NSNumber numberWithFloat:CMTimeGetSeconds(videoAsset.duration)];
    movieSettings[PLSVolumeKey] = [NSNumber numberWithFloat:1.0f];
    
    PLSAVAssetExportSession *exportSession = [[PLSAVAssetExportSession alloc] initWithAsset:videoAsset];
    exportSession.outputFileType = PLSFileTypeMPEG4;
    exportSession.shouldOptimizeForNetworkUse = YES;
    exportSession.outputSettings = outputSettings;
    exportSession.isExportMovieToPhotosAlbum = NO;
    
    [exportSession setCompletionBlock:^(NSURL *url) {
        dispatch_async(dispatch_get_main_queue(), ^{
            if (success) {
                success(url.path);
            }
        });
    }];
    
    [exportSession setFailureBlock:^(NSError *error) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [SVProgressHUD showErrorWithStatus:@"合成视频失败"];
        });
    }];
    
    [exportSession exportAsynchronously];

上面的方法对我无效。

我发现视频导出失败的问题是在同时选择多个视频的时候出现的,而一次只选择一个视频时就没有问题。我的解决方法是,创建一个串行队列,将导出视频并上传的异步任务放到队列中,通过信号量来控制任务的执行,保证每次只导出一个视频,上传完成后再导出下一个,就没再出现这个问题。总之,这个问题应该是同时导出多个视频导致的。

希望能帮助到有需要的人。

上面的方法对我无效。

我发现视频导出失败的问题是在同时选择多个视频的时候出现的,而一次只选择一个视频时就没有问题。我的解决方法是,创建一个串行队列,将导出视频并上传的异步任务放到队列中,通过信号量来控制任务的执行,保证每次只导出一个视频,上传完成后再导出下一个,就没再出现这个问题。总之,这个问题应该是同时导出多个视频导致的。

希望能帮助到有需要的人。

emmmmm,不一定的,目前据我遇到的情况来看,无法导出的视频都是一些其他软件(可能是从安卓)合成的视频,用苹果下载到相册,AVAssetExportSession可能就会无法导出这些视频,上面的其他方法对我来说确实没用,不过七牛的那个SDK对我这边是生效的,所以建议使用AVAssetWriter和AVAssetReader来导出这些视频,可以自己写一个或者使用第三方的。
只是七牛的那个SDK需要授权才可以使用,所以我打印了PLSAVAssetExportSession的私有方法名,找到了他的私有类PLShortVideoExporter(应该是这个名字,我记不清了),继续打印了私有方法名,然后我发现PLShortVideoExporter的那些方法名和另一个我以前使用的三方库SDAVAssetExportSession几乎完全一样,我之前弃用SDAVAssetExportSession是因为存在一些其他的bug,有一些特殊的视频会导致-[AVAssetReaderOutput copyNextSampleBuffer]挂起,进度就始终卡在百分之99.几。但是七牛的没有这个问题,可能七牛的大佬对这个优化了一下直接拿来用了。
所以如果不想从头开始写AVAssetWriter和AVAssetReader,建议可以自己基于SDAVAssetExportSession做一些修改。
由于导出失败这个问题已经困扰了我很久,网上能查到的资料也不多,所以我暂时使用AVAssetWriter和AVAssetReader来辅助导出相册视频(大部分视频都可以使用AVAssetExportSession导出,当AVAssetExportSession导出失败是我再用AVAssetWriter和AVAssetReader来导出)。
以上就是我遇到的问题,和临时解决的方案,仅供参考,如果有不当的地方欢迎指出。
ps:有没有大佬比较了解AVVideoAverageBitRateKey怎么才能设置一个合适的值,因为使用AVAssetWriter和AVAssetReader的时候AVVideoAverageBitRateKey我没能找到合适的标准,设小了模糊,设大了视频偏大,请有经验的大佬指点一下。

@zisulu 官方推荐8Mbps,我觉得5Mbps足够

@zisulu 官方推荐8Mbps,我觉得5Mbps足够

恩。我现在用的就是5,可以接受。

不知楼主当前有好的办法解决吗?

安卓手机第三方app编辑过的视频,在苹果手机相册里可以打开,使用系统相册编辑压缩也是失败,但是微信上可以压缩,压缩后的视频可以正常使用压缩😂

- (void)getVideoOutputPathWithAsset:(PHAsset *)asset presetName:(NSString *)presetName success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure {
    PHVideoRequestOptions* options = [[PHVideoRequestOptions alloc] init];
    options.version = PHVideoRequestOptionsVersionOriginal;
    options.deliveryMode = PHVideoRequestOptionsDeliveryModeAutomatic;
    options.networkAccessAllowed = YES;
    [[PHImageManager defaultManager] requestAVAssetForVideo:asset options:options resultHandler:^(AVAsset* avasset, AVAudioMix* audioMix, NSDictionary* info){
        // NSLog(@"Info:\n%@",info);
        AVURLAsset *videoAsset = (AVURLAsset*)avasset;
        //在处理120帧无声音视频的时候,直接崩溃,原因是AVURLAsset *videoAsset = (AVURLAsset *)asset;因为asset是个基类,它有时会是AVComposition而不是AVURLAsset,这样就得不到视频的url,从而导致程序崩溃
        if ([videoAsset isKindOfClass:[AVURLAsset class]]) {
            if (videoAsset.URL){
                NSString *outputPath = self.outPutFile;
                NSData *data = [NSData dataWithContentsOfURL:videoAsset.URL];
                if (data){
                    [data writeToFile:outputPath atomically:YES];
                    success(outputPath);
                }else{
                    failure(@"导出失败", nil);
                }
            }else{
                failure(@"导出失败", nil);
            }
        }else{
            failure(@"导出失败", nil);
        }
    }];
//        [self startExportVideoWithVideoAsset:videoAsset presetName:presetName success:success failure:failure];
}

可以使用 亲测

yehot commented

TZImageManager.m 617 行:

    [self getVideoOutputPathWithAsset:asset presetName:AVAssetExportPreset640x480 success:success failure:failure];

这里指定了 export 输出的格式是 640*480

目前测试出,如果原视频小于等于这个分辨率,一定会报错

@yehot 这个presetName可以从外面设置哈,你那边有测试出如何设置才确保成功吗?

感觉和视频没关系,我手机相册大部分视频都会失败,一模一样的错误。

@charlesYun 如果你是iOS14的话,那是iCloud的就会失败:#1397
非iOS14的和视频有挺大关系

@banchichen 那请问应该怎么解决呢?

@charlesYun 目前还没看到解法,这里讨论也比较多:ivpusic/react-native-image-crop-picker#1415 。可以关注一下

@banchichen 我换另一种导出方式,测试了一些视频暂时没有发现有失败的情况,需要的可以试试

- (void)getVideoOutputPathWithAsset:(PHAsset *)asset success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure {
//    [self getVideoOutputPathWithAsset:asset presetName:AVAssetExportPresetMediumQuality success:success failure:failure];
    // Option
    PHVideoRequestOptions *option = [[PHVideoRequestOptions alloc] init];
    option.version = PHVideoRequestOptionsVersionCurrent;
    option.deliveryMode = PHVideoRequestOptionsDeliveryModeAutomatic;
    option.networkAccessAllowed = YES;
    // Manager
    PHImageManager *manager = [PHImageManager defaultManager];
    [manager requestExportSessionForVideo:asset options:option exportPreset:AVAssetExportPresetHighestQuality resultHandler:^(AVAssetExportSession *_Nullable exportSession, NSDictionary *_Nullable info) {
        // Path
        NSString *outputPath = [CMFileManager documentDirectoryAtPath:@"CMVideo" documentDirectoryType:CMDocumentDirectoryTypeDocuments];
        outputPath = [outputPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.mp4", [[asset valueForKey:@"filename"] stringByDeletingPathExtension]]];

        if ([CMFileManager fileExistsAtPath:outputPath]) {
            dispatch_async(dispatch_get_main_queue(), ^{
                               if (success) {
                                   success(outputPath);
                               }
                           });
            return;
        }

        // Export
        exportSession.outputURL = [NSURL fileURLWithPath:outputPath];
        exportSession.shouldOptimizeForNetworkUse = NO;
        exportSession.outputFileType = AVFileTypeMPEG4;
        [exportSession exportAsynchronouslyWithCompletionHandler:^{
            dispatch_async(dispatch_get_main_queue(), ^{
                               switch ([exportSession status]) {
                                   case AVAssetExportSessionStatusFailed: {
                                       if (failure) {
                                           failure(@"视频导出失败", nil);
                                       }
                                   } break;
                                   case AVAssetExportSessionStatusCompleted: {
                                       if (success) {
                                           success(outputPath);
                                       }
                                   } break;
                                   default:
                                       break;
                               }
                           });
        }];
    }];
}

升级到3.5.3后,大家发现的会导出失败的视频,可以建一个共享相册,然后邀请我 736420282@qq.com
我来尝试解决哈

3.5.3版本修复了iOS14下iCloud视频导出失败的问题,感谢 @charlesYun 提供的方案👍

同时在TZImageManager里提供了一个新方法:

/// 得到视频原始文件地址
- (void)requestVideoURLWithAsset:(PHAsset *)asset success:(void (^)(NSURL *videoURL))success failure:(void (^)(NSDictionary* info))failure;

通过它可以读取到视频的原始地址URL,然后用文件操作的相关API可以访问视频原始数据(未经压缩),或者这样读也行:

NSData *data = [NSData dataWithContentsOfURL:videoURL];

大家若还发现视频导出失败问题,请建一个共享相册,然后邀请我 736420282@qq.com,开一个新issue(或者加群),我再来解决

然后3.5.3也修复了批量导出视频时,因导出路径重复导致的偶现失败

请问这个问题解决了吗?

@YaoWeiCheng 参考上条回复,你的问题是?

gxtai commented

image

导出视频还是会出错

@gxtai 参考上面的回复哈...最新版能复现的话,把这个视频共享给我下