mirror of
https://github.com/ruvnet/RuView.git
synced 2026-08-30 20:15:58 +00:00
feat(ios): stream compact LiDAR frames over websocket
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import Foundation
|
||||
|
||||
actor WebSocketStreamer {
|
||||
enum StreamError: Error {
|
||||
case invalidURL
|
||||
}
|
||||
|
||||
struct WirePacket: Codable {
|
||||
struct Depth: Codable {
|
||||
let width: Int
|
||||
let height: Int
|
||||
let encoding: String
|
||||
let millimetersBase64: String
|
||||
let confidenceBase64: String
|
||||
}
|
||||
|
||||
let type: String
|
||||
let intrinsics: RuViewLiDARFrame.Intrinsics
|
||||
let pose: RuViewLiDARFrame.Pose
|
||||
let depth: Depth
|
||||
let provenance: RuViewLiDARFrame.Provenance
|
||||
}
|
||||
|
||||
private var task: URLSessionWebSocketTask?
|
||||
private let encoder = JSONEncoder()
|
||||
private var lastSentNs: UInt64 = 0
|
||||
|
||||
func connect(to endpoint: String) throws {
|
||||
guard let url = URL(string: endpoint),
|
||||
url.scheme == "ws" || url.scheme == "wss" else {
|
||||
throw StreamError.invalidURL
|
||||
}
|
||||
task?.cancel(with: .goingAway, reason: nil)
|
||||
let socket = URLSession.shared.webSocketTask(with: url)
|
||||
socket.resume()
|
||||
task = socket
|
||||
}
|
||||
|
||||
func disconnect() {
|
||||
task?.cancel(with: .goingAway, reason: nil)
|
||||
task = nil
|
||||
}
|
||||
|
||||
func send(_ frame: RuViewLiDARFrame, maxFPS: UInt64 = 15, sampleStep: Int = 2) async throws {
|
||||
guard let task else { return }
|
||||
|
||||
let timestamp = frame.provenance.timestampNs
|
||||
let minDelta = 1_000_000_000 / max(1, maxFPS)
|
||||
guard timestamp >= lastSentNs + minDelta else { return }
|
||||
lastSentNs = timestamp
|
||||
|
||||
let packet = Self.makeWirePacket(frame, sampleStep: max(1, sampleStep))
|
||||
let data = try encoder.encode(packet)
|
||||
guard let string = String(data: data, encoding: .utf8) else { return }
|
||||
try await task.send(.string(string))
|
||||
}
|
||||
|
||||
static func makeWirePacket(_ frame: RuViewLiDARFrame, sampleStep: Int) -> WirePacket {
|
||||
let step = max(1, sampleStep)
|
||||
let sourceWidth = frame.depth.width
|
||||
let sourceHeight = frame.depth.height
|
||||
let width = (sourceWidth + step - 1) / step
|
||||
let height = (sourceHeight + step - 1) / step
|
||||
|
||||
var millimeters = Data(capacity: width * height * 2)
|
||||
var confidence = Data(capacity: width * height)
|
||||
|
||||
for y in stride(from: 0, to: sourceHeight, by: step) {
|
||||
for x in stride(from: 0, to: sourceWidth, by: step) {
|
||||
let index = y * sourceWidth + x
|
||||
let meters = frame.depth.meters[index]
|
||||
let mm = UInt16(clamping: Int((meters * 1000).rounded()))
|
||||
var littleEndian = mm.littleEndian
|
||||
withUnsafeBytes(of: &littleEndian) { millimeters.append(contentsOf: $0) }
|
||||
confidence.append(frame.depth.confidence[index])
|
||||
}
|
||||
}
|
||||
|
||||
return WirePacket(
|
||||
type: frame.type,
|
||||
intrinsics: frame.intrinsics,
|
||||
pose: frame.pose,
|
||||
depth: Depth(
|
||||
width: width,
|
||||
height: height,
|
||||
encoding: "u16le-mm+u8-confidence",
|
||||
millimetersBase64: millimeters.base64EncodedString(),
|
||||
confidenceBase64: confidence.base64EncodedString()
|
||||
),
|
||||
provenance: frame.provenance
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user