Make time-lapse export cancellable
This commit is contained in:
parent
7e5028efa7
commit
c9f7923ed1
2 changed files with 38 additions and 59 deletions
|
@ -60,7 +60,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations {
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func exportTimelapseAction(_ sender: Any) {
|
@IBAction func exportTimelapseAction(_ sender: Any) {
|
||||||
let document = NSApplication.shared.keyWindow?.windowController?.document as? Document;
|
guard let originalWindow = NSApplication.shared.keyWindow else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let document = originalWindow.windowController?.document as? Document;
|
||||||
|
|
||||||
let savePanel = NSSavePanel()
|
let savePanel = NSSavePanel()
|
||||||
savePanel.title = "Save Timelapse"
|
savePanel.title = "Save Timelapse"
|
||||||
|
@ -74,10 +78,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations {
|
||||||
let directory = NSTemporaryDirectory()
|
let directory = NSTemporaryDirectory()
|
||||||
|
|
||||||
let mixComposition = AVMutableComposition()
|
let mixComposition = AVMutableComposition()
|
||||||
|
|
||||||
var duration = CMTime.zero
|
|
||||||
|
|
||||||
var instructions: [AVMutableVideoCompositionLayerInstruction] = []
|
var instructions: [AVMutableVideoCompositionLayerInstruction] = []
|
||||||
|
var duration = CMTime.zero
|
||||||
|
|
||||||
for entry in archive.makeIterator() {
|
for entry in archive.makeIterator() {
|
||||||
if entry.path.contains(VideoPath) {
|
if entry.path.contains(VideoPath) {
|
||||||
|
@ -90,60 +92,52 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations {
|
||||||
|
|
||||||
let asset = AVAsset(url: fullURL)
|
let asset = AVAsset(url: fullURL)
|
||||||
|
|
||||||
guard
|
guard let track = mixComposition.addMutableTrack(withMediaType: .video, preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) else {
|
||||||
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try? track.insertTimeRange(CMTimeRangeMake(start: .zero, duration: asset.duration),
|
||||||
|
of: asset.tracks(withMediaType: .video)[0],
|
||||||
|
at: duration)
|
||||||
|
|
||||||
duration = CMTimeAdd(duration, asset.duration)
|
duration = CMTimeAdd(duration, asset.duration)
|
||||||
|
|
||||||
let firstInstruction = AVMutableVideoCompositionLayerInstruction(
|
let opacityInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: track)
|
||||||
assetTrack: firstTrack)
|
opacityInstruction.setOpacity(0.0, at: duration + asset.duration)
|
||||||
firstInstruction.setOpacity(0.0, at: duration + asset.duration)
|
|
||||||
|
|
||||||
instructions.append(firstInstruction)
|
instructions.append(opacityInstruction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mainInstruction = AVMutableVideoCompositionInstruction()
|
let mainInstruction = AVMutableVideoCompositionInstruction()
|
||||||
mainInstruction.timeRange = CMTimeRangeMake(
|
mainInstruction.timeRange = CMTimeRangeMake(start: .zero, duration: duration)
|
||||||
start: .zero,
|
|
||||||
duration: duration)
|
|
||||||
mainInstruction.layerInstructions = instructions
|
mainInstruction.layerInstructions = instructions
|
||||||
|
|
||||||
let mainComposition = AVMutableVideoComposition()
|
let mainComposition = AVMutableVideoComposition()
|
||||||
mainComposition.instructions = [mainInstruction]
|
mainComposition.instructions = [mainInstruction]
|
||||||
mainComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
|
mainComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
|
||||||
mainComposition.renderSize = CGSize(
|
mainComposition.renderSize = CGSize(width: document!.info.videoFrame.0, height: document!.info.videoFrame.1)
|
||||||
width: document!.info.videoFrameWidth,
|
|
||||||
height: document!.info.videoFrameHeight)
|
|
||||||
|
|
||||||
self.exporter = AVAssetExportSession(
|
|
||||||
asset: mixComposition,
|
|
||||||
presetName: AVAssetExportPresetHighestQuality)
|
|
||||||
|
|
||||||
|
self.exporter = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality)
|
||||||
self.exporter?.outputURL = savePanel.url!
|
self.exporter?.outputURL = savePanel.url!
|
||||||
self.exporter?.outputFileType = AVFileType.mp4
|
self.exporter?.outputFileType = AVFileType.mp4
|
||||||
self.exporter?.shouldOptimizeForNetworkUse = true
|
|
||||||
self.exporter?.videoComposition = mainComposition
|
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 {
|
self.exporter?.exportAsynchronously {
|
||||||
|
if self.exporter?.status != .cancelled {
|
||||||
dump(self.exporter?.error?.localizedDescription);
|
DispatchQueue.main.sync {
|
||||||
|
alert.window.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,8 +42,7 @@ struct SilicaDocument {
|
||||||
|
|
||||||
var layers: [SilicaLayer] = []
|
var layers: [SilicaLayer] = []
|
||||||
|
|
||||||
var videoFrameWidth: Int = 0
|
var videoFrame: (Int, Int) = (0, 0)
|
||||||
var videoFrameHeight: Int = 0
|
|
||||||
|
|
||||||
lazy var nsSize = {
|
lazy var nsSize = {
|
||||||
return NSSize(width: width, height: height)
|
return NSSize(width: width, height: height)
|
||||||
|
@ -376,18 +375,7 @@ class Document: NSDocument {
|
||||||
let frameSizeClassID = getClassID(id: frameSizeClassKey)
|
let frameSizeClassID = getClassID(id: frameSizeClassKey)
|
||||||
let frameSize = objectsArray[frameSizeClassID] as! String
|
let frameSize = objectsArray[frameSizeClassID] as! String
|
||||||
|
|
||||||
// frameSize
|
info.videoFrame = parsePairString(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)
|
||||||
|
@ -437,12 +425,9 @@ class Document: NSDocument {
|
||||||
let sizeClassID = getClassID(id: sizeClassKey)
|
let sizeClassID = getClassID(id: sizeClassKey)
|
||||||
let sizeString = objectsArray[sizeClassID] as! String
|
let sizeString = objectsArray[sizeClassID] as! String
|
||||||
|
|
||||||
let sizeComponents = sizeString.replacingOccurrences(of: "{", with: "").replacingOccurrences(of: "}", with: "").components(separatedBy: ", ")
|
let (width, height) = parsePairString(sizeString)!
|
||||||
let width = Int(sizeComponents[0])
|
info.width = width
|
||||||
let height = Int(sizeComponents[1])
|
info.height = height
|
||||||
|
|
||||||
info.width = width!
|
|
||||||
info.height = height!
|
|
||||||
|
|
||||||
columns = Int(ceil(Float(info.width) / Float(info.tileSize)))
|
columns = Int(ceil(Float(info.width) / Float(info.tileSize)))
|
||||||
rows = Int(ceil(Float(info.height) / Float(info.tileSize))) + 1 // TODO: lol why
|
rows = Int(ceil(Float(info.height) / Float(info.tileSize))) + 1 // TODO: lol why
|
||||||
|
|
Reference in a new issue