Skip to content

Commit

Permalink
Merge pull request #3 from mzp/gui
Browse files Browse the repository at this point in the history
OSX GUI Application
  • Loading branch information
mzp committed Feb 13, 2016
2 parents 9c37154 + d2ac0c3 commit 98b2ac9
Show file tree
Hide file tree
Showing 23 changed files with 1,391 additions and 11 deletions.
27 changes: 27 additions & 0 deletions LoveLiver-osx/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// AppDelegate.swift
// LoveLiver-osx
//
// Created by BAN Jun on 2016/02/08.
// Copyright © 2016 mzp. All rights reserved.
//

import Cocoa
import AVFoundation
import AVKit
import NorthLayout
import Ikemen


@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationOpenUntitledFile(sender: NSApplication) -> Bool {
openDocument(sender)
return true
}

@objc private func openDocument(sender: AnyObject?) {
NSDocumentController.sharedDocumentController().openDocument(sender)
}
}

68 changes: 68 additions & 0 deletions LoveLiver-osx/Assets.xcassets/AppIcon.appiconset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"images" : [
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "icon16.png",
"scale" : "1x"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "[email protected]",
"scale" : "2x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "icon32.png",
"scale" : "1x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "[email protected]",
"scale" : "2x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "icon128.png",
"scale" : "1x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "[email protected]",
"scale" : "2x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "icon256.png",
"scale" : "1x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "[email protected]",
"scale" : "2x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "icon512.png",
"scale" : "1x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "[email protected]",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions LoveLiver-osx/Assets.xcassets/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}
667 changes: 667 additions & 0 deletions LoveLiver-osx/Base.lproj/MainMenu.xib

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions LoveLiver-osx/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeExtensions</key>
<array/>
<key>CFBundleTypeMIMETypes</key>
<array/>
<key>CFBundleTypeName</key>
<string>Movie</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSItemContentTypes</key>
<array>
<string>public.movie</string>
</array>
<key>LSTypeIsPackage</key>
<integer>0</integer>
<key>NSDocumentClass</key>
<string>LoveLiver.MovieDocument</string>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2016 mzp. All rights reserved.</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
23 changes: 23 additions & 0 deletions LoveLiver-osx/MovieDocument.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// MovieDocument.swift
// LoveLiver
//
// Created by BAN Jun on 2016/02/09.
// Copyright © 2016 mzp. All rights reserved.
//

import Cocoa


class MovieDocument: NSDocument {
override func readFromURL(url: NSURL, ofType typeName: String) throws {
NSLog("%@", "opening \(url)")
}

override func makeWindowControllers() {
let vc = MovieDocumentViewController(movieURL: fileURL!)
let window = NSWindow(contentViewController: vc)
let wc = NSWindowController(window: window)
addWindowController(wc)
}
}
195 changes: 195 additions & 0 deletions LoveLiver-osx/MovieDocumentViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
//
// MovieDocumentViewController.swift
// LoveLiver
//
// Created by BAN Jun on 2016/02/09.
// Copyright © 2016 mzp. All rights reserved.
//

import Cocoa
import AVFoundation
import AVKit
import NorthLayout
import Ikemen


private let outputDir = NSURL(fileURLWithPath: NSHomeDirectory()).URLByAppendingPathComponent("Pictures/LoveLiver")


class MovieDocumentViewController: NSViewController {
private let player: AVPlayer
private let playerItem: AVPlayerItem
private let imageGenerator: AVAssetImageGenerator
private var exportSession: AVAssetExportSession?
private var posterFrameTime: CMTime?

private let playerView: AVPlayerView = AVPlayerView() { v in
v.controlsStyle = .Floating
v.showsFrameSteppingButtons = true
}
private let posterFrameView = NSImageView() { v in
v.imageScaling = .ScaleProportionallyUpOrDown
v.wantsLayer = true
v.layer?.backgroundColor = NSColor.blackColor().CGColor
v.setContentCompressionResistancePriority(NSLayoutPriorityFittingSizeCompression, forOrientation: .Horizontal)
v.setContentCompressionResistancePriority(NSLayoutPriorityFittingSizeCompression, forOrientation: .Vertical)
}
private lazy var posterFrameButton: NSButton = NSButton() { b in
b.title = "Poster Frame ->>"
b.setButtonType(.MomentaryLightButton)
b.bezelStyle = .RoundedBezelStyle
b.target = self
b.action = "capturePosterFrame:"
}

private lazy var positionsLabel: NSTextField = NSTextField() { tf in
tf.bezeled = false
tf.editable = false
tf.drawsBackground = false
tf.textColor = NSColor.grayColor()
}

private lazy var createLivePhotoButton: NSButton = NSButton() { b in
b.title = "Create Live Photo"
b.setButtonType(.MomentaryLightButton)
b.bezelStyle = .RoundedBezelStyle
b.target = self
b.action = "createLivePhoto:"
}

init!(movieURL: NSURL) {
playerItem = AVPlayerItem(URL: movieURL)
player = AVPlayer(playerItem: playerItem)
playerView.player = player
imageGenerator = AVAssetImageGenerator(asset: playerItem.asset) {
$0.requestedTimeToleranceBefore = kCMTimeZero
$0.requestedTimeToleranceAfter = kCMTimeZero
}
super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func loadView() {
view = NSView(frame: NSRect(x: 0, y: 0, width: 500, height: 500))

let autolayout = view.northLayoutFormat(["p": 20], [
"player": playerView,
"posterButton": posterFrameButton,
"posterView": posterFrameView,
"createLivePhoto": createLivePhotoButton,
"positionsLabel": positionsLabel,
])
autolayout("H:|-p-[player]-p-[posterView(==player)]-p-|")
autolayout("H:|-p-[posterButton(==player)]-p-[createLivePhoto(<=player)]-p-|")
autolayout("H:[positionsLabel(==createLivePhoto)]-p-|")
autolayout("V:|-p-[player]-p-[posterButton]")
autolayout("V:|-p-[posterView]-p-[posterButton]")
autolayout("V:[posterButton]-p-|")
autolayout("V:[posterView][positionsLabel][createLivePhoto]-p-|")

setupAspectRatioConstraints()
updateViews()
}

private func setupAspectRatioConstraints() {
// wait until movie is loaded
guard playerView.videoBounds.width > 0 && playerView.videoBounds.height > 0 else {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(1 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) {
self.setupAspectRatioConstraints()
}
return
}

self.playerView.addConstraint(NSLayoutConstraint(
item: self.playerView, attribute: .Width, relatedBy: .Equal,
toItem: self.playerView, attribute: .Height, multiplier: self.playerView.videoBounds.width / self.playerView.videoBounds.height, constant: 0))
}

private func updateViews() {
createLivePhotoButton.enabled = (posterFrameView.image != nil && exportSession == nil)
positionsLabel.stringValue = positionsLabelText
}

private var positionsLabelText: String {
guard let time = posterFrameTime else { return "" }
let duration = CMTimeGetSeconds(time)
let minutes = Int(floor(duration / 60))
let seconds = Int(floor(duration - Double(minutes) * 60))
let milliseconds = Int((duration - floor(duration)) * 100)
let timeString = String(format: "%02d:%02d.%02d", minutes, seconds, milliseconds)
return "Poster Frame: \(timeString)"
}

@objc private func capturePosterFrame(sender: AnyObject?) {
guard let cgImage = try? imageGenerator.copyCGImageAtTime(player.currentTime(), actualTime: nil) else { return }
let image = NSImage(CGImage: cgImage, size: CGSize(width: CGImageGetWidth(cgImage), height: CGImageGetHeight(cgImage)))
posterFrameView.image = image
posterFrameTime = player.currentTime()
updateViews()
}

@objc private func createLivePhoto(sender: AnyObject?) {
guard let image = posterFrameView.image else { return }

guard let _ = try? NSFileManager.defaultManager().createDirectoryAtPath(outputDir.path!, withIntermediateDirectories: true, attributes: nil) else { return }

let assetIdentifier = NSUUID().UUIDString
let tmpImagePath = NSURL(fileURLWithPath: NSTemporaryDirectory()).URLByAppendingPathComponent("\(assetIdentifier).tiff").path!
let tmpMoviePath = NSURL(fileURLWithPath: NSTemporaryDirectory()).URLByAppendingPathComponent("\(assetIdentifier).mov").path!
let imagePath = outputDir.URLByAppendingPathComponent("\(assetIdentifier).JPG").path!
let moviePath = outputDir.URLByAppendingPathComponent("\(assetIdentifier).MOV").path!
let paths = [tmpImagePath, tmpMoviePath, imagePath, moviePath]

for path in paths {
guard !NSFileManager.defaultManager().fileExistsAtPath(path) else { return }
}

guard image.TIFFRepresentation?.writeToFile(tmpImagePath, atomically: true) == true else { return }
// create AVAssetExportSession each time because it cannot be reused after export completion
guard let session = AVAssetExportSession(asset: playerItem.asset, presetName: AVAssetExportPresetPassthrough) else { return }
session.outputFileType = "com.apple.quicktime-movie"
session.outputURL = NSURL(fileURLWithPath: tmpMoviePath)
session.timeRange = CMTimeRange(start: player.currentTime(), duration: CMTime(value: 3*600, timescale: 600))
session.exportAsynchronouslyWithCompletionHandler {
dispatch_async(dispatch_get_main_queue()) {
switch session.status {
case .Completed:
JPEG(path: tmpImagePath).write(imagePath, assetIdentifier: assetIdentifier)
NSLog("%@", "LivePhoto JPEG created: \(imagePath)")

QuickTimeMov(path: tmpMoviePath).write(moviePath, assetIdentifier: assetIdentifier)
NSLog("%@", "LivePhoto MOV created: \(moviePath)")

self.showInFinderAndOpenInPhotos([imagePath, moviePath].map{NSURL(fileURLWithPath: $0)})
case .Cancelled, .Exporting, .Failed, .Unknown, .Waiting:
NSLog("%@", "exportAsynchronouslyWithCompletionHandler = \(session.status)")
}

for path in [tmpImagePath, tmpMoviePath] {
let _ = try? NSFileManager.defaultManager().removeItemAtPath(path)
}
self.exportSession = nil
self.updateViews()
}
}
exportSession = session
updateViews()
}

private func showInFinderAndOpenInPhotos(fileURLs: [NSURL]) {
NSWorkspace.sharedWorkspace().activateFileViewerSelectingURLs(fileURLs)

// wait until Finder is active or timed out,
// to avoid openURLs overtaking Finder activation
dispatch_async(dispatch_get_global_queue(0, 0)) {
let start = NSDate()
while NSWorkspace.sharedWorkspace().frontmostApplication?.bundleIdentifier != "com.apple.finder" && NSDate().timeIntervalSinceDate(start) < 5 {
NSThread.sleepForTimeInterval(0.1)
}
NSWorkspace.sharedWorkspace().openURLs(fileURLs, withAppBundleIdentifier: "com.apple.Photos", options: [], additionalEventParamDescriptor: nil, launchIdentifiers: nil)
}
}
}
Loading

0 comments on commit 98b2ac9

Please sign in to comment.