Live Photo實(shí)況照片的預(yù)錄制設(shè)計(jì)開發(fā)思路

簡(jiǎn)介

首先我們需要了解在使用LivePhoto是會(huì)有這樣的效果,我們點(diǎn)擊拍照鍵烦味,系統(tǒng)會(huì)為我們展示一段視頻和一張封面圖。
如果只是視頻和圖片我們可簡(jiǎn)單地將其合稱為一段LivePhoto壁拉,但是最難實(shí)現(xiàn)的效果就是拐叉,系統(tǒng)為我們展示的3秒視頻,前面1.5秒是在你按拍照鍵之前的扇商。基于這一點(diǎn)我們要實(shí)現(xiàn)預(yù)錄制的功能宿礁。

實(shí)現(xiàn)方案

要實(shí)現(xiàn)這一效果案铺,我們通常會(huì)想到,在我點(diǎn)擊實(shí)況按鈕或者進(jìn)入我的相機(jī)App的時(shí)候就開始錄制視頻梆靖,點(diǎn)擊拍照鍵后1.5秒就結(jié)束錄制控汉,最終裁剪最后的三秒笔诵。這么做也未嘗不可,但是帶來是后果是如果在live頁(yè)面停留太久姑子,低內(nèi)存的機(jī)器的緩沖區(qū)會(huì)滿乎婿,造成崩潰。
我在stackoverflow上提問后街佑,有人告訴我谢翎,可以維護(hù)一個(gè)緩沖區(qū),緩沖區(qū)內(nèi)只保存最近的1.5s視頻沐旨,拍攝完成再進(jìn)行拼接森逮。

那么問題來了,緩沖區(qū)該如何設(shè)計(jì)呢磁携?怎么才能實(shí)現(xiàn)只保存最近1.5秒這一效果呢褒侧?

1.設(shè)置緩存隊(duì)列

我經(jīng)過思考,認(rèn)為谊迄,維護(hù)一個(gè)視頻隊(duì)列是最合適的闷供,也是較容易去實(shí)現(xiàn)的。
我在用戶點(diǎn)擊實(shí)況按鈕時(shí)统诺,開始錄制歪脏,但是所不同的是,我會(huì)設(shè)置一個(gè)計(jì)時(shí)器篙议,每過1.5秒就停止錄制唾糯,存到緩存區(qū)隊(duì)列,然后繼續(xù)錄制鬼贱。如果隊(duì)列的長(zhǎng)度超過2移怯,那么就把隊(duì)首的一段視頻刪除。
這樣就保證了緩沖區(qū)一定存在我拍照前1.5秒的視頻这难,也不會(huì)造成緩存區(qū)滿的狀況舟误。

2.點(diǎn)擊拍照鍵的操作

用戶點(diǎn)擊拍照鍵時(shí),立即停止錄制姻乓,保存嵌溢,然后開始錄制1.5秒的視頻。這樣就有了后面兩段視頻蹋岩。
我簡(jiǎn)單的畫了個(gè)示意圖:
字丑赖草,湊合著看:


IMG_20180904_144636.jpg

也就是說,我點(diǎn)擊拍照鍵的一瞬間剪个,肯定不是恰好的1.5秒視頻錄制結(jié)束的時(shí)候秧骑,比如是0.6秒,那么我把這三段視頻保存,三段視頻即可合成一段LivePhoto乎折。

所以绒疗,核心思想就是用隊(duì)列保存三段視頻,最后只對(duì)這三段視頻進(jìn)行裁切合并骂澄。

3.具體實(shí)現(xiàn)代碼

這里給出部分代碼吓蘑。項(xiàng)目來自于AppStore上線項(xiàng)目#折紙相機(jī)#,希望大家多多支持坟冲。
最新版本尚未更新預(yù)錄制livephoto功能磨镶。

變量定義

    //判斷是否正在拍攝livePhoto
    var isLivePhoto = false
    //第一段計(jì)時(shí)器
    var liveTimer:Timer?
    //第二段計(jì)時(shí)器
    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)
    }
    //倒計(jì)時(shí)控制
    @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
        }
    }
    
    /// 倒計(jì)時(shí)結(jié)束,結(jié)束錄制
    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)
        
    }
    
    
    
    //錄制完成,進(jìn)行合成和跳轉(zhuǎn)
    @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 = {
                            //將美顏狀態(tài)重置
                            if (weakSelf?.isBeauty)!{
                                weakSelf?.isBeauty = false
                                weakSelf?.defaultBottomView.beautyButton.isSelected = false
                            }
                            //使用閉包樱衷,在vc返回時(shí)將底部隱藏棋嘲,點(diǎn)擊切換時(shí)在取消隱藏
                            if weakSelf?.scaleRate != 0{
                                // weakSelf?.scaleRate = 0
                                weakSelf?.defaultBottomView.backgroundColor = UIColor.clear
                            }
                            //LivePhoto錄像狀態(tài)重置
                            
                        }
                        // 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的下標(biāo)
    /// - 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構(gòu)造前后截取位置的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第二段長(zhǎng)度n,需要用1.5 - 此長(zhǎng)度作為第一段需要減去的長(zhǎng)度
                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
                    //裁剪完第一段視頻后,開始進(jìn)行三段視頻的合成
                    store(video1Composition, storeUrl: newUrl, success: {
                        let mixCom = self.combineVideos()
                        success(mixCom)
                        
                    })
                    
                }
            }
        }
        
   
    }

存儲(chǔ)視頻

/**
     *  存儲(chǔ)合成的視頻
     *
     *  @param mixComposition mixComposition參數(shù)
     *  @param storeUrl       存儲(chǔ)的路徑
     *  @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)
        })
        
    }

好了矩桂,就到這里沸移。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市侄榴,隨后出現(xiàn)的幾起案子雹锣,更是在濱河造成了極大的恐慌,老刑警劉巖癞蚕,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蕊爵,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡桦山,警方通過查閱死者的電腦和手機(jī)攒射,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恒水,“玉大人会放,你說我怎么就攤上這事《ち瑁” “怎么了咧最?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)御雕。 經(jīng)常有香客問我矢沿,道長(zhǎng),這世上最難降的妖魔是什么酸纲? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任捣鲸,我火速辦了婚禮,結(jié)果婚禮上闽坡,老公的妹妹穿的比我還像新娘摄狱。我一直安慰自己脓诡,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布媒役。 她就那樣靜靜地躺著,像睡著了一般宪迟。 火紅的嫁衣襯著肌膚如雪酣衷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天次泽,我揣著相機(jī)與錄音穿仪,去河邊找鬼。 笑死意荤,一個(gè)胖子當(dāng)著我的面吹牛啊片,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播玖像,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼紫谷,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了捐寥?” 一聲冷哼從身側(cè)響起笤昨,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎握恳,沒想到半個(gè)月后瞒窒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡乡洼,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年崇裁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片束昵。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡拔稳,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出妻怎,到底是詐尸還是另有隱情壳炎,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布逼侦,位于F島的核電站匿辩,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏榛丢。R本人自食惡果不足惜铲球,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望晰赞。 院中可真熱鬧稼病,春花似錦选侨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至芍瑞,卻和暖如春晨仑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背拆檬。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工洪己, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人竟贯。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓答捕,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親屑那。 傳聞我的和親對(duì)象是個(gè)殘疾皇子拱镐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容