Add time-lapse export option
This commit is contained in:
parent
a801579bc1
commit
7e5028efa7
3 changed files with 137 additions and 2 deletions
|
@ -1,7 +1,11 @@
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import ZIPFoundation
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
@NSApplicationMain
|
@NSApplicationMain
|
||||||
class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations {
|
class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations {
|
||||||
|
var exporter: AVAssetExportSession?
|
||||||
|
|
||||||
@IBAction func showInfoAction(_ sender: Any) {
|
@IBAction func showInfoAction(_ sender: Any) {
|
||||||
NSApplication.shared.keyWindow?.contentViewController?.performSegue(withIdentifier: "showInfo", sender: self)
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="18122" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="19162" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="macosx"/>
|
<deployment identifier="macosx"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="18122"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19162"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<scenes>
|
<scenes>
|
||||||
|
@ -89,6 +89,11 @@
|
||||||
<action selector="exportThumbnailAction:" target="Voe-Tx-rLC" id="zki-Tz-dom"/>
|
<action selector="exportThumbnailAction:" target="Voe-Tx-rLC" id="zki-Tz-dom"/>
|
||||||
</connections>
|
</connections>
|
||||||
</menuItem>
|
</menuItem>
|
||||||
|
<menuItem title="Export Timelapse..." keyEquivalent="S" id="mOL-Am-v5d" userLabel="Export Timelapse...">
|
||||||
|
<connections>
|
||||||
|
<action selector="exportTimelapseAction:" target="Voe-Tx-rLC" id="U6F-oD-VRr"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
<menuItem isSeparatorItem="YES" id="m54-Is-iLE">
|
<menuItem isSeparatorItem="YES" id="m54-Is-iLE">
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="saveDocument:" target="Ady-hI-5gd" id="fGt-aM-WYN"/>
|
<action selector="saveDocument:" target="Ady-hI-5gd" id="fGt-aM-WYN"/>
|
||||||
|
|
|
@ -42,6 +42,9 @@ struct SilicaDocument {
|
||||||
|
|
||||||
var layers: [SilicaLayer] = []
|
var layers: [SilicaLayer] = []
|
||||||
|
|
||||||
|
var videoFrameWidth: Int = 0
|
||||||
|
var videoFrameHeight: Int = 0
|
||||||
|
|
||||||
lazy var nsSize = {
|
lazy var nsSize = {
|
||||||
return NSSize(width: width, height: height)
|
return NSSize(width: width, height: height)
|
||||||
}()
|
}()
|
||||||
|
@ -342,6 +345,19 @@ class Document: NSDocument {
|
||||||
return nil
|
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) {
|
func parseSilicaDocument(archive: Archive, dict: NSDictionary) {
|
||||||
let objectsArray = self.dict?["$objects"] as! NSArray
|
let objectsArray = self.dict?["$objects"] as! NSArray
|
||||||
|
|
||||||
|
@ -352,6 +368,27 @@ class Document: NSDocument {
|
||||||
info.flippedHorizontally = (dict[FlippedHorizontallyKey] as! NSNumber).boolValue
|
info.flippedHorizontally = (dict[FlippedHorizontallyKey] as! NSNumber).boolValue
|
||||||
info.flippedVertically = (dict[FlippedVerticallyKey] 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 colorProfileClassKey = dict["colorProfile"]
|
||||||
let colorProfileClassID = getClassID(id: colorProfileClassKey)
|
let colorProfileClassID = getClassID(id: colorProfileClassKey)
|
||||||
let colorProfile = objectsArray[colorProfileClassID] as! NSDictionary
|
let colorProfile = objectsArray[colorProfileClassID] as! NSDictionary
|
||||||
|
|
Reference in a new issue