Now it's possible for the document title to be null, just like an author name. Also, the document title is used for the default filename when exporting.
150 lines
6.6 KiB
Swift
150 lines
6.6 KiB
Swift
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)
|
|
}
|
|
|
|
@IBAction func showTimelapseAction(_ sender: Any) {
|
|
NSApplication.shared.keyWindow?.contentViewController?.performSegue(withIdentifier: "showTimelapse", sender: self)
|
|
}
|
|
|
|
func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
|
|
// Show timelapse and show info buttons
|
|
if(item.tag == 67 || item.tag == 68 || item.tag == 100 || item.tag == 101 || item.tag == 102) {
|
|
return NSApplication.shared.keyWindow != nil
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
@IBAction func exportAction(_ sender: Any) {
|
|
let document = NSApplication.shared.keyWindow?.windowController?.document as? Document;
|
|
|
|
let savePanel = NSSavePanel()
|
|
savePanel.title = "Save"
|
|
savePanel.allowedFileTypes = ["public.png", "public.jpeg"]
|
|
savePanel.accessoryView = ExportAccessoryView.fromNib()
|
|
|
|
savePanel.begin { (result) in
|
|
if result.rawValue == NSApplication.ModalResponse.OK.rawValue {
|
|
let canvas = document?.makeComposite()
|
|
let canvasTiff = canvas?.tiffRepresentation
|
|
let bitmapImage = NSBitmapImageRep(data: canvasTiff!)
|
|
let canvasPng = bitmapImage!.representation(using: .png, properties: [:])
|
|
|
|
try? canvasPng?.write(to: savePanel.url!)
|
|
}
|
|
}
|
|
}
|
|
|
|
@IBAction func exportThumbnailAction(_ sender: Any) {
|
|
let document = NSApplication.shared.keyWindow?.windowController?.document as? Document;
|
|
|
|
let savePanel = NSSavePanel()
|
|
savePanel.title = "Save Thumbnail"
|
|
savePanel.allowedFileTypes = ["public.png"]
|
|
savePanel.begin { (result) in
|
|
if result.rawValue == NSApplication.ModalResponse.OK.rawValue {
|
|
let canvas = document?.makeThumbnail()
|
|
let canvasTiff = canvas?.tiffRepresentation
|
|
let bitmapImage = NSBitmapImageRep(data: canvasTiff!)
|
|
let canvasPng = bitmapImage!.representation(using: .png, properties: [:])
|
|
|
|
try? canvasPng?.write(to: savePanel.url!)
|
|
}
|
|
}
|
|
}
|
|
|
|
@IBAction func exportTimelapseAction(_ sender: Any) {
|
|
guard let originalWindow = NSApplication.shared.keyWindow else {
|
|
return
|
|
}
|
|
|
|
let document = originalWindow.windowController?.document as? Document;
|
|
|
|
let savePanel = NSSavePanel()
|
|
savePanel.title = "Save Timelapse"
|
|
savePanel.allowedFileTypes = ["public.mpeg-4"]
|
|
savePanel.nameFieldStringValue = (document?.getIdealFilename())!
|
|
|
|
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 instructions: [AVMutableVideoCompositionLayerInstruction] = []
|
|
var duration = CMTime.zero
|
|
|
|
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 track = mixComposition.addMutableTrack(withMediaType: .video, preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) else {
|
|
return
|
|
}
|
|
|
|
try? track.insertTimeRange(CMTimeRangeMake(start: .zero, duration: asset.duration),
|
|
of: asset.tracks(withMediaType: .video)[0],
|
|
at: duration)
|
|
|
|
duration = CMTimeAdd(duration, asset.duration)
|
|
|
|
let opacityInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: track)
|
|
opacityInstruction.setOpacity(0.0, at: duration + asset.duration)
|
|
|
|
instructions.append(opacityInstruction)
|
|
}
|
|
}
|
|
|
|
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.videoFrame.0, height: document!.info.videoFrame.1)
|
|
|
|
self.exporter = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality)
|
|
self.exporter?.outputURL = savePanel.url!
|
|
self.exporter?.outputFileType = AVFileType.mp4
|
|
self.exporter?.videoComposition = mainComposition
|
|
|
|
let alert = NSAlert()
|
|
alert.messageText = "Exporting timelapse..."
|
|
alert.addButton(withTitle: "Cancel")
|
|
alert.beginSheetModal(for: originalWindow) { (resonse) in
|
|
self.exporter?.cancelExport()
|
|
alert.window.close()
|
|
}
|
|
|
|
self.exporter?.exportAsynchronously {
|
|
if self.exporter?.status != .cancelled {
|
|
DispatchQueue.main.sync {
|
|
alert.window.close()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|