slo-mo慢动作视频处理

slo-mo慢动作视频处理

slo-mo模式是iOS系统拍摄模式的一种,跟其他模式在存储方式上面和有着本质的区别,如下慢动作演示
https://test.noizztv.com/article/share?id=9208285652934558845&s_bucket=na
<iframe
height=450
width=800 src="https://test.noizztv.com/article/share?id=9208285652934558845&s_bucket=na"
frameborder=0
allowfullscreen>
</iframe>
文章主要解决以下问题

  1. 慢动作视频时长
  2. 慢动作视频播放原理
  3. 导出视频
  4. 慢动作视频的编辑更新

iOS系统相册的bug

系统相册有个bug,慢动作视频时长显示的是其拍摄时长,而非视频真实播放时长


实际时长为8秒

iOS系统如何播放慢动作视频的

slo-mo模式的视频在相册中存储的是正常拍摄的原视频,其在播放的时候读取本地拍摄时存储的慢拍信息,控制播放速度,而不是生成慢动作视频,这样做猜测是为了播放和展示效率,因为视频导出是很慢的,ps:重要的事高亮说。

iOS系统如何编辑慢拍视频

普通的视频只有裁剪的功能,而slo-mo慢动作模式的视频支持慢放区域的选取,这个影响真实时长,但是不会合成为新的慢拍视频,只是更新慢拍信息,控制播放时候播放速率

image.png

有个认知前提,iOS目前获取视频获取相册都是通过PHFetchResult

获取所有相册


/**
 获取所有内容不为空的的相册
 */
- (NSArray <PHAssetCollection *> *)_PHFetchAllUserAlbum:(YYPLFetchOptions *)options{
    NSMutableArray *assetAlbums = [NSMutableArray array];
    PHFetchResult *result = nil;
    if (options.disableSmartAlbum) {
        // 相机胶卷
        result = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeSmartAlbumUserLibrary options:nil];
        
    } else {
        // 所有智能相册
        result = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAny options:nil];
    }
    // 系统相册+用户创建
    for (id elem in result) {
        if ([elem isKindOfClass:[PHAssetCollection class]]) {
            [assetAlbums addObject:elem];
        }
    }
    PHFetchResult *userCreate = [PHCollectionList fetchTopLevelUserCollectionsWithOptions:nil];
    for (id elem in userCreate) {
        if ([elem isKindOfClass:[PHAssetCollection class]]) {
            [assetAlbums addObject:elem];
        }
    }
    return [assetAlbums copy];
}

检测慢拍动作的视频,99%的人都会犯的错

requestAVAssetForVideo回调过来的asset绝大多数时候都是AVURLAsset,
所谓AVURLAsset就是可以根据URL直接索引到这个视频,然而当有慢动作这类型的视频的时候就不是这种类型了,而是AVComposition模式。

问题来了,为什么多数时候都不会报错呢,如果是类型不匹配早就崩溃了,
这是因为options.version = PHVideoRequestOptionsVersionCurrent;这句话,大多数时候都是写成options.version = PHVideoRequestOptionsVersionOriginal; 直接取的原视频,原图片,而不是编辑之后的,因为相机拍完一定是存了一个原视频的,根据这个original去取一定会返回urlasset的。

- (NSString *)PH_requestRealFilePath
{
    __block NSString *filePath = nil;
    PHImageManager *imageManager = [PHImageManager defaultManager];
    switch (self.PH_asset.mediaType) {
        case PHAssetMediaTypeImage:
        {
            PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
            options.version = PHImageRequestOptionsVersionOriginal;
            options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
            options.synchronous = YES; // use synchronous request
            [imageManager requestImageDataForAsset:self.PH_asset options:options resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) {
                NSURL *fileURL = info[@"PHImageFileURLKey"];
                if (fileURL) {
                    filePath = [fileURL absoluteString];
                }
            }];
            return filePath;
        }
        
        case PHAssetMediaTypeVideo:
        {
            dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
            PHVideoRequestOptions *options = [[PHVideoRequestOptions alloc] init];
            options.version =  PHVideoRequestOptionsVersionCurrent;
//            options.version =  PHVideoRequestOptionsVersionOriginal;
            @weakify(self);
            
          [imageManager cancelImageRequest:self.phReqId];
           __block PHImageRequestID   reqid = 0;
            reqid =   [imageManager requestAVAssetForVideo:self.PH_asset options:options resultHandler:^(AVAsset * _Nullable asset, AVAudioMix * _Nullable audioMix, NSDictionary * _Nullable info) {
                @strongify(self)
                if ([[asset class] isSubclassOfClass:[AVURLAsset class]]) {
                    filePath = [[(AVURLAsset *)asset URL] absoluteString];
                    self.AVURL_asset = (AVURLAsset*)asset;
                    dispatch_semaphore_signal(semaphore);
                }
                else if ([[asset class] isSubclassOfClass:[AVComposition class]]){
                   
                   reqid = [YYPLAsset getVideoPathFromPHAsset:self.asset  Complete:^(AVURLAsset *avurlAsset,NSString *path, NSString *fileName) {
                        LogInfo(@"YYPLAsset+PH", @"慢节奏视频:%@", path);
                        filePath = path;
                        self.AVURL_asset = avurlAsset;
                        [self PH_duration];
                        dispatch_semaphore_signal(semaphore);
                   } progressblock:^(float progress) {
                       
                   }];
                    self.phReqId = reqid;
                }
            }];
            self.phReqId = reqid;
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            return filePath;
        }
            
        default:
        {
            [YYLogger error:@"YYPLAsset+PH" message:@"self.PH_asset.mediaType is out of switch %zd",self.PH_asset.mediaType];
        }
            break;
    }
    return nil;
}

如何取得视频的真实时长

有几种认识的层次,

  1. 直接从phasset中去取时长,
  2. phasset中的是不准确的
  3. 从导出之后的视频中去获取
  4. 或许还有其他的办法取得
    经过前面的铺垫想必你一定能达到第三个层次了,因为相册中的duration是不准确的,那只是拍摄时候的时长,但是从导出的视频中获取未免也太久了,因为视频导出时间很耗时 ,同上,况且时长而已嘛,一定没有那么复杂,另外微信可以直接读取真实的慢拍时长,这个时间几乎忽略不计,于是还得再找找相关的方法,于是找到了这个

- (void)PH_getRealDuration:(void (^)(double dur))resultBlock
{
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    PHVideoRequestOptions *options = [[PHVideoRequestOptions alloc] init];
    //FIXME:选择使用非均匀速率视频需要改回来
    options.version =  PHVideoRequestOptionsVersionCurrent;
    __block double dur = 0;
    [[PHImageManager defaultManager] requestPlayerItemForVideo:self.PH_asset options:options resultHandler:^(AVPlayerItem * _Nullable playerItem, NSDictionary * _Nullable info) {
        dur = CMTimeGetSeconds(playerItem.duration);
        //慢拍视频 playerItem.asset 是一个AVComposition的类
        NSLog(@"获取真实文件时长:%.2f",dur);
        dispatch_semaphore_signal(semaphore);
    }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    if (!isnan(dur) && dur>0) {
        resultBlock(dur);
        return;
    }
    dur =  CMTimeGetSeconds(self.AVURL_asset.duration);
    if (isnan(dur) || !dur) {
        dur =  self.PH_asset.duration;
    }
    resultBlock(dur);
}

requestPlayerItemForVideo api可以获取playerItem,里面的duration可以转换为真实时长,此方法并不耗时。

导出视频

慢拍真实视频是不存在的,必须要通过自有的api导出,其原理是读取当时慢拍保存的信息tracks等进行分步合成。

 
static int getPHAssetRandomKey(){
    static int curRandomKey;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        curRandomKey = arc4random()%1000;
    });
    return curRandomKey;
}

typedef void(^ResultPath)(AVURLAsset *avurlAsset,NSString *filePath, NSString *fileName);

+ (PHImageRequestID)getVideoPathFromPHAsset:(PHAsset *)asset
                       Complete:(ResultPath)result
                  progressblock:(void (^)(float progress))progressblock
{
    NSArray *assetResources = [PHAssetResource assetResourcesForAsset:asset];
    PHAssetResource *resource;
    
    for (PHAssetResource *assetRes in assetResources) {
        if (@available(iOS 9.1, *)) {
            if (assetRes.type == PHAssetResourceTypePairedVideo ||
                assetRes.type == PHAssetResourceTypeVideo) {
                resource = assetRes;
            }
        } else {
            // Fallback on earlier versions
        }
    }
    NSString *fileName = @"tempAssetVideo.mov";
    if (resource.originalFilename) {
        fileName = resource.originalFilename;
    }
   
    if (@available(iOS 9.1, *)) {
        if (asset.mediaType == PHAssetMediaTypeVideo ||
            asset.mediaSubtypes == PHAssetMediaSubtypePhotoLive) {
            PHVideoRequestOptions *options = [[PHVideoRequestOptions alloc] init];
            options.version = PHImageRequestOptionsVersionCurrent;
            options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
             
        
            NSString *PATH_MOVIE_FILE = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"sol-mo-%d-%@",getPHAssetRandomKey(),fileName]];
            
            if ([[NSFileManager defaultManager] fileExistsAtPath:PATH_MOVIE_FILE]) {
                NSURL * url = [NSURL fileURLWithPath:PATH_MOVIE_FILE] ;
                NSDictionary *options = @{ AVURLAssetPreferPreciseDurationAndTimingKey : @YES };
                AVURLAsset *avasset = [[AVURLAsset alloc] initWithURL:url options:options];
                result(avasset,url.absoluteString,fileName);
                return 0;
            }
            [[NSFileManager defaultManager] removeItemAtPath:PATH_MOVIE_FILE error:nil];

            PHImageManager *manager = [PHImageManager defaultManager];
            PHImageRequestID reqid = [manager requestExportSessionForVideo:asset options:options exportPreset:AVAssetExportPreset960x540 resultHandler:^(AVAssetExportSession * _Nullable exportSession, NSDictionary * _Nullable info) {
                
                NSString *savePath = PATH_MOVIE_FILE;
                exportSession.outputURL = [NSURL fileURLWithPath:savePath];
                exportSession.shouldOptimizeForNetworkUse = NO;
                exportSession.outputFileType = AVFileTypeMPEG4;
                [exportSession exportAsynchronouslyWithCompletionHandler:^{
                    switch ([exportSession status]) {
                        case AVAssetExportSessionStatusCompleted:
                        {
                            NSURL * fileurl = [NSURL fileURLWithPath:PATH_MOVIE_FILE];
                            NSDictionary *options = @{ AVURLAssetPreferPreciseDurationAndTimingKey : @YES };
                            AVURLAsset *avasset = [[AVURLAsset alloc] initWithURL:fileurl options:options];
                            
                            result(avasset,fileurl.absoluteString,fileName);
                            break;
                        }
                        case AVAssetExportSessionStatusFailed:
                        case AVAssetExportSessionStatusCancelled:
                        default:
                        {
                            [[NSFileManager defaultManager] removeItemAtPath:PATH_MOVIE_FILE error:nil];
                            result(nil,nil, nil);
                            break;
                        }
                    }
                }];
                
            }];
            return reqid;
        } else {
            result(nil,nil, nil);
        }
    } else {
        // Fallback on earlier versions
    }
    return 0;
}

再次说一句,导出时长是比较漫长的,大概一分钟真实时长导出需要十多秒,这样子的情况测试是会提bug的,产品可能是不会接受的,那么如何优化呢

优化导出时长

由于涉及到系统的api使用,目前从导出时长上面是无法缩短到明显的地步的
但是,让用户感觉时长没那么久,那就可以做很多事情了
有以下思路

  1. 加进度,导出的进度没有回调,没有kvo监听,但是有个progress可以实时获取,这里可以给一个定时器实时去取AVAssetExportSession的progress
    苹果的api如是说,

/* Specifies the progress of the export on a scale from 0 to 1.0.  A value of 0 means the export has not yet begun, A value of 1.0 means the export is complete. This property is not key-value observable. */
@property (nonatomic, readonly) float progress;
  1. 后台去合成,不阻塞用户当前操作
    但是要同时处理合成成功和失败的时机,在业务上做对应处理

  2. 播放器直接支持AVCompont格式创建播放器
    这个方式几乎不用任何等待,直接进入下一步操作,与ios相册播放器一样处理,确保用户体验友好。但是代价大,需要重写你的播放器生成方式。

  3. 或许你还有其他的更好方式,欢迎留言

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生x阅读 15,967评论 3 119
  • 用到的组件 1、通过CocoaPods安装 2、第三方类库安装 3、第三方服务 友盟社会化分享组件 友盟用户反馈 ...
    SunnyLeong阅读 14,598评论 1 180
  • 小诺是我大学一直用的一部手机,这一学期开始的时候我便抛弃了充电不灵敏,样子也有些过时了的它,使得它一直沉睡在抽屉里...
    bff33734eee2阅读 265评论 0 0
  • 为什么要听古典音乐?这答案估计有成千上万个版本,假如说你是一门外汉,周围没有朋友在听古典音乐,工作也跟音乐无关,那...
    八卦海绵阅读 1,206评论 4 7
  • 我有一场梦 梦里连我也没有 只有零散的,那些成了碎片的熙攘场景 我是个不称职的造梦者 或许单纯地看成一场梦境,你我...
    四月的西风阅读 358评论 4 6