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 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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,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>
|
||||
<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"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
|
@ -89,6 +89,11 @@
|
|||
<action selector="exportThumbnailAction:" target="Voe-Tx-rLC" id="zki-Tz-dom"/>
|
||||
</connections>
|
||||
</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">
|
||||
<connections>
|
||||
<action selector="saveDocument:" target="Ady-hI-5gd" id="fGt-aM-WYN"/>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -352,6 +368,27 @@ class Document: NSDocument {
|
|||
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)
|
||||
let colorProfile = objectsArray[colorProfileClassID] as! NSDictionary
|
||||
|
|
Reference in a new issue