Live Photo实况照片的预录制设计开发思路

简介

首先我们需要了解在使用LivePhoto是会有这样的效果,我们点击拍照键,系统会为我们展示一段视频和一张封面图。
如果只是视频和图片我们可简单地将其合称为一段LivePhoto,但是最难实现的效果就是,系统为我们展示的3秒视频,前面1.5秒是在你按拍照键之前的。基于这一点我们要实现预录制的功能。

实现方案

要实现这一效果,我们通常会想到,在我点击实况按钮或者进入我的相机App的时候就开始录制视频,点击拍照键后1.5秒就结束录制,最终裁剪最后的三秒。这么做也未尝不可,但是带来是后果是如果在live页面停留太久,低内存的机器的缓冲区会满,造成崩溃。
我在stackoverflow上提问后,有人告诉我,可以维护一个缓冲区,缓冲区内只保存最近的1.5s视频,拍摄完成再进行拼接。

那么问题来了,缓冲区该如何设计呢?怎么才能实现只保存最近1.5秒这一效果呢?

1.设置缓存队列

我经过思考,认为,维护一个视频队列是最合适的,也是较容易去实现的。
我在用户点击实况按钮时,开始录制,但是所不同的是,我会设置一个计时器,每过1.5秒就停止录制,存到缓存区队列,然后继续录制。如果队列的长度超过2,那么就把队首的一段视频删除。
这样就保证了缓冲区一定存在我拍照前1.5秒的视频,也不会造成缓存区满的状况。

2.点击拍照键的操作

用户点击拍照键时,立即停止录制,保存,然后开始录制1.5秒的视频。这样就有了后面两段视频。
我简单的画了个示意图:
字丑,凑合着看:


IMG_20180904_144636.jpg

也就是说,我点击拍照键的一瞬间,肯定不是恰好的1.5秒视频录制结束的时候,比如是0.6秒,那么我把这三段视频保存,三段视频即可合成一段LivePhoto。

所以,核心思想就是用队列保存三段视频,最后只对这三段视频进行裁切合并。

3.具体实现代码

这里给出部分代码。项目来自于AppStore上线项目#折纸相机#,希望大家多多支持。
最新版本尚未更新预录制livephoto功能。

变量定义

    //判断是否正在拍摄livePhoto
    var isLivePhoto = false
    //第一段计时器
    var liveTimer:Timer?
    //第二段计时器
    var liveTimer2:Timer?
    var liveCounter:Double = 0.5;
    var liveCounter2:Double = 0.5;
    var liveUrl:URL!
    var videoUrls = [URL]()
    var saveManager:SaveVieoManager?

具体核心代码

    func setLiveMode(){
        if !isLivePhoto{
            isLivePhoto = true
            setLiveStart()
        }else{
            isLivePhoto = false
            movieWriter?.finishRecording()
            liveTimer?.invalidate()
            liveTimer = nil
        }
    }
    
    /// 开始录制LivePhoto
    func setLiveStart(){
        //shotButton.isUserInteractionEnabled = false
        startRecord()
        videoUrls.append(videoUrl!)
        self.topView.liveCounter.isHidden = false
        topView.setCounter(text: "0")
        liveTimer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(updateLiveCounter), userInfo: nil, repeats: true)
       // liveTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateLiveCounter), userInfo: nil, repeats: true)
    }
    //倒计时控制
    @objc func updateLiveCounter(){
        liveCounter = liveCounter + 0.5
        print("正在拍摄LivePhoto: ",liveCounter)
        topView.setCounter(text: "\(liveCounter)")
//        if liveCounter == 3{
//            finishLiveRecord()
//        }
        
        if liveCounter == 1.5{
            movieWriter?.finishRecording()
            deleteLiveBuffer()
            startRecord()
            videoUrls.append(videoUrl!)
            liveCounter = 0
        }
    }
    
    /// 倒计时结束,结束录制
    func finishLiveRecord(){
        movieWriter?.finishRecording()
        shotButton.isUserInteractionEnabled = false
        liveTimer?.invalidate()
        startRecord()
        liveCounter2 = 0
        liveTimer2 = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(setIntervalFinish), userInfo: nil, repeats: true)
        
    }
    
    
    
    //录制完成,进行合成和跳转
    @objc func setIntervalFinish(){
        liveCounter2 = liveCounter2 + 0.5
        if liveCounter2 == 1.5{
            deleteAdditionalBuffer()
            shotButton.isUserInteractionEnabled = false
            movieWriter?.failureBlock = {
                Error in
                print(Error as Any)
            }
           // movieWriter?.completionBlock = {
                self.videoUrls.append(self.videoUrl!)
                self.movieWriter?.finishRecording()
                print("-------------:",self.videoUrls)
                self.topView.liveCounter.isHidden = true
                self.shotButton.isUserInteractionEnabled = true
                self.liveTimer = nil
                self.liveTimer2?.invalidate()
                self.liveTimer2 = nil
                self.liveCounter2 = 0
            setLiveStart()
            //视频合成
           // ProgressHUD.show("合成中")
            saveManager = SaveVieoManager(urls: videoUrls)
            let newUrl = URL(fileURLWithPath: "\(NSTemporaryDirectory())folder_all.mp4")
            unlink(newUrl.path)
            videoUrl = newUrl
            
            //视频裁剪以及合成
            saveManager?.combineLiveVideos(success: {
                com in
                self.saveManager?.store(com, storeUrl: newUrl, success:{
                    DispatchQueue.main.async {
                        let vc =  CheckViewController.init(image: nil, type: 2)
                        vc.videoUrl = newUrl
                        weak var weakSelf = self
                        vc.videoScale = weakSelf?.scaleRate
                        vc.willDismiss = {
                            //将美颜状态重置
                            if (weakSelf?.isBeauty)!{
                                weakSelf?.isBeauty = false
                                weakSelf?.defaultBottomView.beautyButton.isSelected = false
                            }
                            //使用闭包,在vc返回时将底部隐藏,点击切换时在取消隐藏
                            if weakSelf?.scaleRate != 0{
                                // weakSelf?.scaleRate = 0
                                weakSelf?.defaultBottomView.backgroundColor = UIColor.clear
                            }
                            //LivePhoto录像状态重置
                            
                        }
                        // ProgressHUD.showSuccess("合成成功")
                        //  weakSelf?.videoUrls.removeAll()
                        weakSelf?.present(vc, animated: true, completion: nil)
                        //self.setLiveStart()
                    }
                    
                })
            })
            
            
         
        }

    }
    
    func deleteLiveBuffer(){
        
        if videoUrls.count>=2{
            do {
                try FileManager.default.removeItem(atPath: (videoUrls.first!.path))
                videoUrls.removeFirst()
            } catch {
            }
        }
        
    }
    
    
    func deleteAdditionalBuffer(){
        while videoUrls.count>=3{
            do {
                try FileManager.default.removeItem(atPath: (videoUrls.first!.path))
                videoUrls.removeFirst()
            } catch {
            }
        }
    }

视频裁剪和合成部分代码:

裁剪视频

 /// 剪辑视频
    ///
    /// - Parameters:
    ///   - frontOffset: 前面剪几秒
    ///   - endOffset: 后面剪几秒
    ///   - index: url的下标
    /// - Returns: 合成
    func cutLiveVideo(frontOffset:Float64,endOffset:Float64,index:Int)->AVMutableComposition{
        let composition = AVMutableComposition()
        // Create the video composition track.
        let compositionVideoTrack: AVMutableCompositionTrack? = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)
        // Create the audio composition track.
        let compositionAudioTrack: AVMutableCompositionTrack? = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)
        let pathUrl = videoUrls[index]
        let asset = AVURLAsset(url: pathUrl, options: nil)
        
        let videoTrack: AVAssetTrack = asset.tracks(withMediaType: .video)[0]
        let audioTrack: AVAssetTrack = asset.tracks(withMediaType: .audio)[0]
        compositionVideoTrack?.preferredTransform = videoTrack.preferredTransform
        
        // CMTime
        let trackDuration: CMTime = videoTrack.timeRange.duration
        let trackTimescale: CMTimeScale = trackDuration.timescale
        // 用timescale构造前后截取位置的CMTime
        let startTime: CMTime = CMTimeMakeWithSeconds(frontOffset, trackTimescale)
        let endTime: CMTime = CMTimeMakeWithSeconds(endOffset, trackTimescale)
        let intendedDuration: CMTime = CMTimeSubtract(asset.duration, CMTimeAdd(startTime, endTime))

        try? compositionVideoTrack?.insertTimeRange(CMTimeRangeMake(startTime, intendedDuration), of: videoTrack, at: kCMTimeZero)
        try? compositionAudioTrack?.insertTimeRange(CMTimeRangeMake(startTime, intendedDuration), of: audioTrack, at: kCMTimeZero)

        return composition
    
    }
    

合成视频

func combineLiveVideos(success:@escaping(_ mixComposition:AVMutableComposition)->()){

        for i in 0...videoUrls.count-1{
            //剪第一段
            if i == 0{
                //求liveVideo第二段长度n,需要用1.5 - 此长度作为第一段需要减去的长度
                if videoUrls.count >= 1{
                    var videoAsset2:AVURLAsset?
                    videoAsset2 = AVURLAsset.init(url: videoUrls[1])
                    let tt = videoAsset2!.duration
                    let getLengthOfVideo2 = Double(tt.value)/Double(tt.timescale)
                    //减掉开始 n 秒
                    let video1Composition = cutLiveVideo(frontOffset: getLengthOfVideo2, endOffset: 0.0, index: 0)
                    let newUrl = URL(fileURLWithPath: "\(NSTemporaryDirectory())foldercut_1.mp4")
                    unlink(newUrl.path)
                    videoUrls[0] = newUrl
                    //裁剪完第一段视频后,开始进行三段视频的合成
                    store(video1Composition, storeUrl: newUrl, success: {
                        let mixCom = self.combineVideos()
                        success(mixCom)
                        
                    })
                    
                }
            }
        }
        
   
    }

存储视频

/**
     *  存储合成的视频
     *
     *  @param mixComposition mixComposition参数
     *  @param storeUrl       存储的路径
     *  @param successBlock   successBlock
     *  @param failureBlcok   failureBlcok
     */
    func store(_ mixComposition:AVMutableComposition,storeUrl:URL,success successBlock:@escaping ()->()){
        //weak var weakSelf = self
        var assetExport: AVAssetExportSession? = nil
        assetExport = AVAssetExportSession.init(asset: mixComposition, presetName: AVAssetExportPreset640x480)
        assetExport?.outputFileType = AVFileType("com.apple.quicktime-movie")
        assetExport?.outputURL = storeUrl
        assetExport?.exportAsynchronously(completionHandler: {
            successBlock()
           // UISaveVideoAtPathToSavedPhotosAlbum((storeUrl.path), self,#selector(weakSelf?.saveVideo(videoPath:didFinishSavingWithError:contextInfo:)), nil)
        })
        
    }

好了,就到这里。

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

推荐阅读更多精彩内容