149 lines
5.4 KiB
Swift
149 lines
5.4 KiB
Swift
|
|
import Foundation
|
||
|
|
import NIO
|
||
|
|
import NIOHTTP1
|
||
|
|
|
||
|
|
/// CNA-style HTTP simulator for Venice 2. Sony's Camera Network API family
|
||
|
|
/// is REST-shaped; paths in one place, correct against protocol docs later.
|
||
|
|
public enum SonySimPaths {
|
||
|
|
public static let cameraInfo = "/cna/v1/camera/info"
|
||
|
|
public static let status = "/cna/v1/camera/status"
|
||
|
|
public static let userLut = "/cna/v1/monitoring/lut"
|
||
|
|
}
|
||
|
|
|
||
|
|
public actor SonySimulator {
|
||
|
|
private var group: MultiThreadedEventLoopGroup?
|
||
|
|
private var channel: Channel?
|
||
|
|
public private(set) var boundPort: Int?
|
||
|
|
|
||
|
|
private var recording = false
|
||
|
|
private var clipName: String?
|
||
|
|
private var ei = 800
|
||
|
|
private var wb = 5500
|
||
|
|
private var tint = 0
|
||
|
|
private var fps = 24.0
|
||
|
|
private var timecode = "00:00:00:00"
|
||
|
|
public private(set) var uploadedLutCount = 0
|
||
|
|
|
||
|
|
public init() {}
|
||
|
|
|
||
|
|
public func setState(recording: Bool, clipName: String?, ei: Int, wb: Int, tint: Int, fps: Double, timecode: String) {
|
||
|
|
self.recording = recording
|
||
|
|
if let c = clipName { self.clipName = c }
|
||
|
|
self.ei = ei
|
||
|
|
self.wb = wb
|
||
|
|
self.tint = tint
|
||
|
|
self.fps = fps
|
||
|
|
self.timecode = timecode
|
||
|
|
}
|
||
|
|
|
||
|
|
func handle(method: HTTPMethod, uri: String, body: Data?) -> (status: HTTPResponseStatus, body: Data) {
|
||
|
|
switch (method, uri) {
|
||
|
|
case (.GET, SonySimPaths.cameraInfo):
|
||
|
|
return (.ok, json(["model": "VENICE 2 SIM", "serial": "SNY-0001"]))
|
||
|
|
case (.GET, SonySimPaths.status):
|
||
|
|
var obj: [String: Any] = [
|
||
|
|
"recording": recording,
|
||
|
|
"exposureIndex": ei,
|
||
|
|
"whiteBalance": wb,
|
||
|
|
"tint": tint,
|
||
|
|
"fps": fps,
|
||
|
|
"timecode": timecode,
|
||
|
|
]
|
||
|
|
if let c = clipName { obj["clipName"] = c }
|
||
|
|
return (.ok, json(obj))
|
||
|
|
case (.PUT, SonySimPaths.userLut):
|
||
|
|
guard let body,
|
||
|
|
let obj = try? JSONSerialization.jsonObject(with: body) as? [String: Any],
|
||
|
|
let size = obj["size"] as? Int,
|
||
|
|
let table = obj["table"] as? [Any],
|
||
|
|
table.count == size * size * size * 3 else {
|
||
|
|
return (.badRequest, json(["error": "bad LUT"]))
|
||
|
|
}
|
||
|
|
uploadedLutCount += 1
|
||
|
|
return (.ok, json(["status": "accepted"]))
|
||
|
|
default:
|
||
|
|
return (.notFound, json(["error": "unknown path"]))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private func json(_ obj: [String: Any]) -> Data {
|
||
|
|
(try? JSONSerialization.data(withJSONObject: obj, options: [.sortedKeys])) ?? Data()
|
||
|
|
}
|
||
|
|
|
||
|
|
public func start(port: Int) async throws {
|
||
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
||
|
|
self.group = group
|
||
|
|
let sim = self
|
||
|
|
let bootstrap = ServerBootstrap(group: group)
|
||
|
|
.serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
|
||
|
|
.childChannelInitializer { channel in
|
||
|
|
channel.pipeline.configureHTTPServerPipeline().flatMap {
|
||
|
|
channel.pipeline.addHandler(SonyHTTPHandler(sim: sim))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let channel = try await bootstrap.bind(host: "127.0.0.1", port: port).get()
|
||
|
|
self.channel = channel
|
||
|
|
boundPort = channel.localAddress?.port
|
||
|
|
}
|
||
|
|
|
||
|
|
public func stop() async throws {
|
||
|
|
try await channel?.close()
|
||
|
|
try await group?.shutdownGracefully()
|
||
|
|
channel = nil
|
||
|
|
group = nil
|
||
|
|
boundPort = nil
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
final class SonyHTTPHandler: ChannelInboundHandler, @unchecked Sendable {
|
||
|
|
typealias InboundIn = HTTPServerRequestPart
|
||
|
|
typealias OutboundOut = HTTPServerResponsePart
|
||
|
|
|
||
|
|
private let sim: SonySimulator
|
||
|
|
private var method: HTTPMethod = .GET
|
||
|
|
private var uri: String = "/"
|
||
|
|
private var bodyBuffer: ByteBuffer?
|
||
|
|
|
||
|
|
init(sim: SonySimulator) {
|
||
|
|
self.sim = sim
|
||
|
|
}
|
||
|
|
|
||
|
|
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
||
|
|
switch unwrapInboundIn(data) {
|
||
|
|
case .head(let head):
|
||
|
|
method = head.method
|
||
|
|
uri = head.uri
|
||
|
|
bodyBuffer = nil
|
||
|
|
case .body(var buf):
|
||
|
|
if bodyBuffer == nil {
|
||
|
|
bodyBuffer = buf
|
||
|
|
} else {
|
||
|
|
bodyBuffer?.writeBuffer(&buf)
|
||
|
|
}
|
||
|
|
case .end:
|
||
|
|
let body = bodyBuffer.map { Data($0.readableBytesView) }
|
||
|
|
let m = method
|
||
|
|
let u = uri
|
||
|
|
let channel = context.channel
|
||
|
|
let loop = context.eventLoop
|
||
|
|
Task {
|
||
|
|
let (status, respBody) = await self.sim.handle(method: m, uri: u, body: body)
|
||
|
|
loop.execute {
|
||
|
|
var headers = HTTPHeaders()
|
||
|
|
headers.add(name: "Content-Type", value: "application/json")
|
||
|
|
headers.add(name: "Content-Length", value: "\(respBody.count)")
|
||
|
|
headers.add(name: "Connection", value: "close")
|
||
|
|
let head = HTTPResponseHead(version: .http1_1, status: status, headers: headers)
|
||
|
|
channel.write(HTTPServerResponsePart.head(head), promise: nil)
|
||
|
|
var buf = channel.allocator.buffer(capacity: respBody.count)
|
||
|
|
buf.writeBytes(respBody)
|
||
|
|
channel.write(HTTPServerResponsePart.body(.byteBuffer(buf)), promise: nil)
|
||
|
|
channel.writeAndFlush(HTTPServerResponsePart.end(nil)).whenComplete { _ in
|
||
|
|
channel.close(promise: nil)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|