Initial commit

This commit is contained in:
Zhigang Fang
2017-05-01 11:00:30 +08:00
commit ece19dd362
15 changed files with 735 additions and 0 deletions

1
.swift-version Normal file
View File

@@ -0,0 +1 @@
3.0

17
OSCKit.podspec Normal file
View File

@@ -0,0 +1,17 @@
Pod::Spec.new do |s|
s.name = "OSCKit"
s.version = "0.0.1"
s.summary = "Richo OSCKit"
s.description = "Richo OSCKit with Promise"
s.homepage = "https://theta360.com/"
s.license = "MIT"
s.author = { "Zhigang Fang" => "zhigang1992@gmail.com" }
s.platform = :ios, "9.0"
s.source = { :git => "https://github.com/tappollo/OSCKit.git" }
s.source_files = "Source/*.swift"
s.frameworks = "SystemConfiguration"
s.dependency 'SwiftyyJSON'
s.dependency 'PromiseKit'
s.dependency 'AwaitKit'
end

109
Source/Command.swift Normal file
View File

@@ -0,0 +1,109 @@
//
// Command.swift
// ThreeSixtyCamera
//
// Created by Zhigang Fang on 4/18/17.
// Copyright © 2017 Tappollo Inc. All rights reserved.
//
import Foundation
import SwiftyyJSON
// swiftlint:disable identifier_name
// We want to keep it as close to API as possible
enum Command {
case startSession
case updateSession(sessionId: String)
case closeSession(sessionId: String)
case _finishWlan
case takePicture(sessionId: String)
case _startCapture(sessionId: String, mode: VideoCaptureMode)
case _stopCapture(sessionId: String)
case listImages
case _listAll(entryCount: Int, detail: Bool)
case delete
case getImage(fileUri: String, _type: DownloadType)
case _getVideo(fileUri: String, _type: DownloadType)
case _getLivePreview(sessionId: String)
case getMetadata
case getOptions
case setOptions(options: [Option], sessionId: String)
case _getMySetting
case _setMySetting
case _stopSelfTimer
}
// swiftlint:enable identifier_name
extension Command {
var name: String {
switch self {
case .startSession: return "camera.startSession"
case .updateSession: return "camera.updateSession"
case .closeSession: return "camera.closeSession"
case ._finishWlan: return "camera._finishWlan"
case .takePicture: return "camera.takePicture"
case ._startCapture: return "camera._startCapture"
case ._stopCapture: return "camera._stopCapture"
case .listImages: return "camera.listImages"
case ._listAll: return "camera._listAll"
case .delete: return "camera.delete"
case .getImage: return "camera.getImage"
case ._getVideo: return "camera._getVideo"
case ._getLivePreview: return "camera._getLivePreview"
case .getMetadata: return "camera.getMetadata"
case .getOptions: return "camera.getOptions"
case .setOptions: return "camera.setOptions"
case ._getMySetting: return "camera._getMySetting"
case ._setMySetting: return "camera._setMySetting"
case ._stopSelfTimer: return "camera._stopSelfTimer"
}
}
var defaultJSON: JSON {
return ["name": self.name]
}
func with(params: [String: Any]) -> JSON {
return [
"name": self.name,
"parameters": params
]
}
var json: JSON {
switch self {
case .updateSession(sessionId: let id): return with(params: ["sessionId": id])
case .takePicture(sessionId: let id): return with(params: ["sessionId": id])
case let .getImage(fileUri: fileUri, _type: _type):
return with(params: [
"fileUri": fileUri,
"_type": _type.rawValue
])
case let ._getVideo(fileUri: fileUri, _type: _type):
return with(params: [
"fileUri": fileUri,
"_type": _type.rawValue
])
case let ._startCapture(sessionId: id, mode: mode):
return with(params: [
"sessionId": id,
"mode": mode.rawValue
])
case ._stopCapture(sessionId: let id): return with(params: ["sessionId": id])
case let ._listAll(entryCount: count, detail: detail):
return with(params: [
"entryCount": count,
"detail": detail
])
case let .setOptions(options: options, sessionId: id):
var json: JSON = [:]
options.forEach({ json[$0.key] = $0.value })
return with(params: [
"sessionId": id,
"options": json.value ?? NSNull()
])
case ._getLivePreview(sessionId: let id): return with(params: ["sessionId": id])
default: return defaultJSON
}
}
}

38
Source/DeviceInfo.swift Normal file
View File

@@ -0,0 +1,38 @@
//
// DeviceInfo.swift
// ThreeSixtyCamera
//
// Created by Zhigang Fang on 4/18/17.
// Copyright © 2017 Tappollo Inc. All rights reserved.
//
import Foundation
import SwiftyyJSON
import PromiseKit
import AwaitKit
public struct DeviceInfo {
public let model: String
public let serial: String
public let battery: Double
}
extension OSCKit {
public var deviceInfo: Promise<DeviceInfo> {
return async {
let info = try await(self.info)
let state = try await(self.state)
return DeviceInfo(
model: try info["model"].string !! SDKError.unableToParse(info),
serial: try info["serialNumber"].string !! SDKError.unableToParse(info),
battery: try state["state"]["batteryLevel"].double !! SDKError.unableToParse(state)
)
}
}
public var info: Promise<JSON> { return self.requestJSON(endPoint: .info) }
public var state: Promise<JSON> { return self.requestJSON(endPoint: .state) }
}

58
Source/Image.swift Normal file
View File

@@ -0,0 +1,58 @@
//
// Image.swift
// ThreeSixtyCamera
//
// Created by Zhigang Fang on 4/18/17.
// Copyright © 2017 Tappollo Inc. All rights reserved.
//
import Foundation
import SwiftyyJSON
import PromiseKit
import AwaitKit
public enum DownloadType: String {
case thumbnail = "thumb"
case full = "full"
}
extension OSCKit {
public func getImage(url: String, type: DownloadType = .full) -> Promise<UIImage> {
return async {
let url = try await(self.getImageLocalURL(url: url, type: type))
return try UIImage(contentsOfFile: url.path) !! SDKError.unableToFindImageAt(url)
}
}
public func getImageLocalURL(url: String, type: DownloadType = .full) -> Promise<URL> {
return async {
let device = try await(self.deviceInfo)
// Adding serial key in the begining
// To prevent cache collision between different devices
let cacheKey = try (device.serial + url).addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) !! SDKError.unableToCreateVideoCacheKey
let cacheFolder = try NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first.map({
URL(fileURLWithPath: $0)
}) !! SDKError.unableToFindCacheFolder
let fileURL = cacheFolder.appendingPathComponent(cacheKey)
if FileManager.default.fileExists(atPath: fileURL.path) {
return fileURL
}
let data = try await(self.requestData(command: .getImage(fileUri: url, _type: type)))
try data.write(to: fileURL)
return fileURL
}
}
public func takePicture() -> Promise<String> {
return async {
let session = try await(self.session)
try await(self.execute(command: .setOptions(options: [CaptureMode.image], sessionId: session.id)))
try await(self.execute(command: .setOptions(options: [FileFormat.smallImage], sessionId: session.id)))
let captureResponse = try await(self.execute(command: .takePicture(sessionId: session.id)))
let statusID = try captureResponse["id"].string !! SDKError.unableToParse(captureResponse)
let statusResponse = try await(self.waitForStatus(id: statusID))
return try statusResponse["results"]["fileUri"].string !! SDKError.unableToParse(statusResponse)
}
}
}

88
Source/LivePreview.swift Normal file
View File

@@ -0,0 +1,88 @@
//
// LivePreview.swift
// ThreeSixtyCamera
//
// Created by Zhigang Fang on 4/18/17.
// Copyright © 2017 Tappollo Inc. All rights reserved.
//
import Foundation
import SwiftyyJSON
import PromiseKit
import AwaitKit
final class LivePreview: NSObject, URLSessionDataDelegate {
static let shared = LivePreview()
lazy var session: URLSession = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: nil)
enum Status {
case stopped
case loading
case playing
}
var status: Status = .stopped
private var receivedData: NSMutableData?
private var dataTask: URLSessionDataTask?
var callback: (UIImage) -> Void = { _ in }
private override init() {}
func play(request: URLRequest) {
self.receivedData = NSMutableData()
self.dataTask?.cancel()
self.dataTask = self.session.dataTask(with: request)
self.dataTask?.resume()
self.status = .loading
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
if let imageData = receivedData , imageData.length > 0,
let receivedImage = UIImage(data: imageData as Data) {
if status == .loading {
status = .playing
}
DispatchQueue.main.async {
self.callback(receivedImage)
}
}
receivedData = NSMutableData()
completionHandler(.allow)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
self.receivedData?.append(data)
}
func stop() {
self.dataTask?.cancel()
self.dataTask = nil
self.receivedData = nil
}
}
extension OSCKit {
public func startLivePreview(callback: @escaping (UIImage) -> Void) {
async {
let session = try await(OSCKit.shared.session)
try await(self.execute(command: .setOptions(options: [CaptureMode.image], sessionId: session.id)))
DispatchQueue.main.async(execute: {
LivePreview.shared.stop()
LivePreview.shared.callback = callback
let json = Command._getLivePreview(sessionId: session.id).json
let request = self.assembleRequest(endPoint: .execute, params: json)
LivePreview.shared.play(request: request)
})
}
}
public func stopLivePreview() {
DispatchQueue.main.async {
LivePreview.shared.stop()
}
}
}

71
Source/MediaItems.swift Normal file
View File

@@ -0,0 +1,71 @@
//
// Items.swift
// ThreeSixtyCamera
//
// Created by Zhigang Fang on 4/18/17.
// Copyright © 2017 Tappollo Inc. All rights reserved.
//
import Foundation
import SwiftyyJSON
import PromiseKit
import AwaitKit
public struct MediaItem {
public enum Kind {
case image
case video
}
public let name: String
public let url: String
public let size: Int?
public let date: String?
public let width: Int?
public let height: Int?
public var type: Kind {
if name.lowercased().hasSuffix(".mp4") {
return .video
}
return .image
}
init (json: JSON) throws {
self.name = try json["name"].string !! OSCKit.SDKError.unableToParse(json)
self.url = try json["uri"].string !! OSCKit.SDKError.unableToParse(json)
self.size = json["size"].int
self.date = json["dateTimeZone"].string
self.width = json["width"].int
self.height = json["height"].int
}
}
extension OSCKit {
public var listAllMediaItems: Promise<[MediaItem]> {
return async {
let all = try await(self.execute(command: ._listAll(entryCount: 100, detail: false)))
let entries = try all["results"]["entries"].array !! SDKError.unableToParse(all)
return try entries.map({try MediaItem(json: $0)})
}
}
public func getLatestMediaItem(timeout: TimeInterval = 0, withPredicate predicate: @escaping (MediaItem) -> Bool) -> Promise<MediaItem> {
return async {
if timeout < 0 {
throw SDKError.fetchTimeout
}
let all = try await(self.execute(command: ._listAll(entryCount: 1, detail: true)))
let json = try all["results"]["entries"].array !! SDKError.unableToParse(all)
if let first = json.first {
let item = try MediaItem(json: first)
if predicate(item) {
return item
}
}
try await(after(interval: 2))
return try await(self.getLatestMediaItem(timeout: timeout - 2, withPredicate: predicate))
}
}
}

19
Source/OSCKit-Swift.h Normal file
View File

@@ -0,0 +1,19 @@
//
// OSCKit-Swift.h
// OSCKit
//
// Created by Zhigang Fang on 4/18/17.
// Copyright © 2017 matrix. All rights reserved.
//
#import <UIKit/UIKit.h>
//! Project version number for OSCKit.
FOUNDATION_EXPORT double OSCKitKVersionNumber;
//! Project version string for OSCKit.
FOUNDATION_EXPORT const unsigned char OSCKitVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <OSCKit/PublicHeader.h>

44
Source/OSCKit.swift Normal file
View File

@@ -0,0 +1,44 @@
//
// OSCKit.swift
// ThreeSixtyCamera
//
// Created by Zhigang Fang on 4/17/17.
// Copyright © 2017 Tappollo Inc. All rights reserved.
//
import SwiftyyJSON
import SystemConfiguration.CaptiveNetwork
public class OSCKit {
public static let shared = OSCKit()
enum SDKError: Error {
case unableToParse(JSON)
case unableToFindImageAt(URL)
case fetchTimeout
case unableToFindCacheFolder
case unableToFindVideo
case unableToCreateVideoCacheKey
}
private init() { }
public func isConnectedToDeviceWiFi(withPrefix prefix: String) -> Bool {
return SSID.current?.hasPrefix(prefix) == true
}
}
struct SSID {
static var current: String? {
if let interfaces = CNCopySupportedInterfaces() {
for i in 0..<CFArrayGetCount(interfaces) {
let interfaceName: UnsafeRawPointer = CFArrayGetValueAtIndex(interfaces, i)
let rec = unsafeBitCast(interfaceName, to: AnyObject.self)
let unsafeInterfaceData = CNCopyCurrentNetworkInfo("\(rec)" as CFString)
if let interfaceData: NSDictionary = unsafeInterfaceData, let ssid = interfaceData["SSID"] as? String {
return ssid
}
}
}
return nil
}
}

View File

@@ -0,0 +1,30 @@
//
// OptionalThrow.swift
// ThreeSixtyCamera
//
// Created by Zhigang Fang on 4/18/17.
// Copyright © 2017 Tappollo Inc. All rights reserved.
//
import Foundation
infix operator !! : LogicalConjunctionPrecedence
func !!<T>(optional: Optional<T>, error: Error) throws -> T {
return try optional.someOrThrow(error)
}
extension Optional {
func someOrThrow(_ error: Error) throws -> Wrapped {
if let value = self {
return value
}
throw error
}
}
func const<T, V>(value: T) -> (V) -> T {
return { _ in
value
}
}

34
Source/Options.swift Normal file
View File

@@ -0,0 +1,34 @@
//
// Options.swift
// ThreeSixtyCamera
//
// Created by Zhigang Fang on 4/18/17.
// Copyright © 2017 Tappollo Inc. All rights reserved.
//
import Foundation
import SwiftyyJSON
protocol Option {
var key: String { get }
var value: JSON { get }
}
enum CaptureMode: String, Option {
case video = "_video"
case image = "image"
var key: String { return "captureMode" }
var value: JSON { return JSON(value: self.rawValue as NSObject) }
}
struct FileFormat: Option {
let type: String
let width: Int
let height: Int
var key: String { return "fileFormat" }
var value: JSON { return ["type": type, "width": width, "height": height] }
static let smallImage = FileFormat(type: "jpeg", width: 2048, height: 1024)
}

68
Source/Request.swift Normal file
View File

@@ -0,0 +1,68 @@
//
// Request.swift
// ThreeSixtyCamera
//
// Created by Zhigang Fang on 4/18/17.
// Copyright © 2017 Tappollo Inc. All rights reserved.
//
import Foundation
import SwiftyyJSON
import PromiseKit
import AwaitKit
enum Endpoint {
case info
case state
case execute
case status
var path: String {
switch self {
case .execute: return "/osc/commands/execute"
case .status: return "/osc/commands/status"
case .info: return "/osc/info"
case .state: return "/osc/state"
}
}
var method: String {
switch self {
case .execute, .state, .status: return "POST"
case .info: return "GET"
}
}
}
extension OSCKit {
func assembleRequest(endPoint: Endpoint = .execute, params json: JSON? = nil) -> URLRequest {
var request = URLRequest(url: URL(string: "http://192.168.1.1\(endPoint.path)")!)
request.httpMethod = endPoint.method
request.addValue("application/json;charset=utf-8", forHTTPHeaderField: "Content-Type")
if let json = json {
request.httpBody = json.encode()
}
return request
}
func requestJSON(endPoint: Endpoint = .execute, params json: JSON? = nil) -> Promise<JSON> {
var request = assembleRequest(endPoint: endPoint, params: json)
request.addValue("application/json", forHTTPHeaderField: "Accept")
return URLSession.shared.dataTask(with: request).then(execute: { data -> JSON in
let anyObject = try JSONSerialization.jsonObject(with: data, options: [])
return JSON(value: anyObject as? NSObject)
})
}
func requestData(command: Command) -> Promise<Data> {
return async {
let request = self.assembleRequest(params: command.json)
return try await(URLSession.shared.dataTask(with: request))
}
}
func execute(command: Command) -> Promise<JSON> {
return self.requestJSON(endPoint: .execute, params: command.json)
}
}

67
Source/Session.swift Normal file
View File

@@ -0,0 +1,67 @@
//
// Session.swift
// ThreeSixtyCamera
//
// Created by Zhigang Fang on 4/18/17.
// Copyright © 2017 Tappollo Inc. All rights reserved.
//
import Foundation
import SwiftyyJSON
import PromiseKit
import AwaitKit
public struct Session {
let id: String
let expires: Date
fileprivate static var currentSession: Session?
init (json: JSON) throws {
self.id = try json["results"]["sessionId"].string !! OSCKit.SDKError.unableToParse(json)
let expire = try json["results"]["timeout"].int !! OSCKit.SDKError.unableToParse(json)
self.expires = Date().addingTimeInterval(TimeInterval(expire))
}
var isExpired: Bool {
return self.expires < Date()
}
var wasJustedIssued: Bool {
return self.expires.addingTimeInterval(10) > Date()
}
}
extension OSCKit {
public var session: Promise<Session> {
if let currentSession = Session.currentSession {
if currentSession.wasJustedIssued {
return Promise(value: currentSession)
}
if currentSession.isExpired {
return startSession
}
return update(session: currentSession)
}
return startSession
}
var startSession: Promise<Session> {
return async {
let response = try await(self.execute(command: .startSession))
return try Session(json: response)
}
}
func update(session: Session) -> Promise<Session> {
return async {
do {
let response = try await(self.execute(command: .updateSession(sessionId: session.id)))
return try Session(json: response)
} catch {
return try await(self.startSession)
}
}
}
}

28
Source/Status.swift Normal file
View File

@@ -0,0 +1,28 @@
//
// Status.swift
// ThreeSixtyCamera
//
// Created by Zhigang Fang on 4/18/17.
// Copyright © 2017 Tappollo Inc. All rights reserved.
//
import Foundation
import AwaitKit
import PromiseKit
import SwiftyyJSON
extension OSCKit {
func waitForStatus(id: String) -> Promise<JSON> {
return async {
let json: JSON = [
"id": id
]
let response = try await(self.requestJSON(endPoint: .status, params: json))
if response["state"].string == "inProgress" {
try await(after(interval: 2))
return try await(self.waitForStatus(id: id))
}
return response
}
}
}

63
Source/Video.swift Normal file
View File

@@ -0,0 +1,63 @@
//
// Video.swift
// ThreeSixtyCamera
//
// Created by Zhigang Fang on 4/18/17.
// Copyright © 2017 Tappollo Inc. All rights reserved.
//
import Foundation
import SwiftyyJSON
import PromiseKit
import AwaitKit
public enum VideoCaptureMode: String {
case interval
case composite
case bracket
}
extension OSCKit {
public func startCapture(mode: VideoCaptureMode = .interval) -> Promise<JSON> {
return async {
let session = try await(self.session)
try await(self.execute(command: .setOptions(options: [CaptureMode.video], sessionId: session.id)))
return try await(self.execute(command: ._startCapture(sessionId: session.id, mode: mode)))
}
}
public func stopCapture() -> Promise<String> {
return async {
let session = try await(self.session)
// Saving first item before capturing video
// This is due to the face THETA API v2.0 does not return a file URL when capture finishes
// https://developers.theta360.com/en/docs/v2.0/api_reference/commands/camera._stop_capture.html
let lastItem = try await(self.getLatestMediaItem(withPredicate: const(value: true)))
try await(self.execute(command: ._stopCapture(sessionId: session.id)))
// After stop capturing video, wait until it returns a new item with type being .video
let mediaItem = try await(self.getLatestMediaItem(withPredicate: {
$0.url != lastItem.url && $0.type ~= .video
}))
return mediaItem.url
}
}
public func getVideo(url: String, type: DownloadType = .full) -> Promise<URL> {
return async {
let device = try await(self.deviceInfo)
let cacheKey = try (device.serial + url).addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) !! SDKError.unableToCreateVideoCacheKey
let cacheFolder = try NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first.map({
URL(fileURLWithPath: $0)
}) !! SDKError.unableToFindCacheFolder
let fileURL = cacheFolder.appendingPathComponent(cacheKey)
if FileManager.default.fileExists(atPath: fileURL.path) {
return fileURL
}
let data = try await(self.requestData(command: ._getVideo(fileUri: url, _type: type)))
try data.write(to: fileURL)
return fileURL
}
}
}