From 7e5028efa7923fb045c1c9bcdeb491b6c54955e8 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Wed, 29 Sep 2021 17:27:13 -0400 Subject: [PATCH] Add time-lapse export option --- SilicaViewer/AppDelegate.swift | 93 +++++++++++++++++++++++++ SilicaViewer/Base.lproj/Main.storyboard | 9 ++- SilicaViewer/Document.swift | 37 ++++++++++ 3 files changed, 137 insertions(+), 2 deletions(-) diff --git a/SilicaViewer/AppDelegate.swift b/SilicaViewer/AppDelegate.swift index 03608c2..65d742c 100644 --- a/SilicaViewer/AppDelegate.swift +++ b/SilicaViewer/AppDelegate.swift @@ -1,7 +1,11 @@ import Cocoa +import ZIPFoundation +import AVFoundation @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations { + var exporter: AVAssetExportSession? + @IBAction func showInfoAction(_ sender: Any) { NSApplication.shared.keyWindow?.contentViewController?.performSegue(withIdentifier: "showInfo", sender: self) } @@ -55,5 +59,94 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations { } } + @IBAction func exportTimelapseAction(_ sender: Any) { + let document = NSApplication.shared.keyWindow?.windowController?.document as? Document; + + let savePanel = NSSavePanel() + savePanel.title = "Save Timelapse" + savePanel.allowedFileTypes = ["public.mpeg-4"] + savePanel.begin { (result) in + if result.rawValue == NSApplication.ModalResponse.OK.rawValue { + guard let archive = Archive(data: (document?.data)!, accessMode: Archive.AccessMode.read) else { + return + } + + let directory = NSTemporaryDirectory() + + let mixComposition = AVMutableComposition() + + var duration = CMTime.zero + + var instructions: [AVMutableVideoCompositionLayerInstruction] = [] + + for entry in archive.makeIterator() { + if entry.path.contains(VideoPath) { + let fileName = NSUUID().uuidString + ".mp4" + + // This returns a URL? even though it is an NSURL class method + let fullURL = NSURL.fileURL(withPathComponents: [directory, fileName])! + + let _ = try? archive.extract(entry, to: fullURL) + + let asset = AVAsset(url: fullURL) + + guard + let firstTrack = mixComposition.addMutableTrack( + withMediaType: .video, + preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) + else { return } + + // 3 + do { + try firstTrack.insertTimeRange( + CMTimeRangeMake(start: .zero, duration: asset.duration), + of: asset.tracks(withMediaType: .video)[0], + at: duration) + } catch { + print("Failed to load first track") + return + } + + duration = CMTimeAdd(duration, asset.duration) + + let firstInstruction = AVMutableVideoCompositionLayerInstruction( + assetTrack: firstTrack) + firstInstruction.setOpacity(0.0, at: duration + asset.duration) + + instructions.append(firstInstruction) + } + } + + let mainInstruction = AVMutableVideoCompositionInstruction() + mainInstruction.timeRange = CMTimeRangeMake( + start: .zero, + duration: duration) + mainInstruction.layerInstructions = instructions + + let mainComposition = AVMutableVideoComposition() + mainComposition.instructions = [mainInstruction] + mainComposition.frameDuration = CMTimeMake(value: 1, timescale: 30) + mainComposition.renderSize = CGSize( + width: document!.info.videoFrameWidth, + height: document!.info.videoFrameHeight) + + self.exporter = AVAssetExportSession( + asset: mixComposition, + presetName: AVAssetExportPresetHighestQuality) + + self.exporter?.outputURL = savePanel.url! + self.exporter?.outputFileType = AVFileType.mp4 + self.exporter?.shouldOptimizeForNetworkUse = true + self.exporter?.videoComposition = mainComposition + + self.exporter?.exportAsynchronously { + + dump(self.exporter?.error?.localizedDescription); + } + + } + } + } + } diff --git a/SilicaViewer/Base.lproj/Main.storyboard b/SilicaViewer/Base.lproj/Main.storyboard index 64d5ca5..1008a81 100644 --- a/SilicaViewer/Base.lproj/Main.storyboard +++ b/SilicaViewer/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -89,6 +89,11 @@ + + + + + diff --git a/SilicaViewer/Document.swift b/SilicaViewer/Document.swift index 2c47245..a2d2c04 100644 --- a/SilicaViewer/Document.swift +++ b/SilicaViewer/Document.swift @@ -42,6 +42,9 @@ struct SilicaDocument { var layers: [SilicaLayer] = [] + var videoFrameWidth: Int = 0 + var videoFrameHeight: Int = 0 + lazy var nsSize = { return NSSize(width: width, height: height) }() @@ -342,6 +345,19 @@ class Document: NSDocument { return nil } + func parsePairString(_ str: String) -> (Int, Int)? { + let sizeComponents = str.replacingOccurrences(of: "{", with: "").replacingOccurrences(of: "}", with: "").components(separatedBy: ", ") + + if sizeComponents.count == 2 { + let width = Int(sizeComponents[0]) + let height = Int(sizeComponents[1]) + + return (width!, height!) + } else { + return nil + } + } + func parseSilicaDocument(archive: Archive, dict: NSDictionary) { let objectsArray = self.dict?["$objects"] as! NSArray @@ -351,6 +367,27 @@ class Document: NSDocument { info.orientation = (dict[OrientationKey] as! NSNumber).intValue info.flippedHorizontally = (dict[FlippedHorizontallyKey] as! NSNumber).boolValue info.flippedVertically = (dict[FlippedVerticallyKey] as! NSNumber).boolValue + + let videoResolutionClassKey = dict["SilicaDocumentVideoSegmentInfoKey"] + let videoResolutionClassID = getClassID(id: videoResolutionClassKey) + let videoResolution = objectsArray[videoResolutionClassID] as! NSDictionary + + let frameSizeClassKey = videoResolution["frameSize"] + let frameSizeClassID = getClassID(id: frameSizeClassKey) + let frameSize = objectsArray[frameSizeClassID] as! String + + // frameSize + //SilicaDocumentVideoSegmentInfoKey + // videoQualityKey + + dump(frameSize, indent: 5) + + guard let (frameWidth, frameHeight) = parsePairString(frameSize) else { + return + } + + info.videoFrameWidth = frameWidth + info.videoFrameHeight = frameHeight let colorProfileClassKey = dict["colorProfile"] let colorProfileClassID = getClassID(id: colorProfileClassKey)