Skip to content

Commit a14b4a1

Browse files
committed
feat(ios): stream compact LiDAR frames over websocket
1 parent 0fbc36f commit a14b4a1

1 file changed

Lines changed: 93 additions & 0 deletions

File tree

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import Foundation
2+
3+
actor WebSocketStreamer {
4+
enum StreamError: Error {
5+
case invalidURL
6+
}
7+
8+
struct WirePacket: Codable {
9+
struct Depth: Codable {
10+
let width: Int
11+
let height: Int
12+
let encoding: String
13+
let millimetersBase64: String
14+
let confidenceBase64: String
15+
}
16+
17+
let type: String
18+
let intrinsics: RuViewLiDARFrame.Intrinsics
19+
let pose: RuViewLiDARFrame.Pose
20+
let depth: Depth
21+
let provenance: RuViewLiDARFrame.Provenance
22+
}
23+
24+
private var task: URLSessionWebSocketTask?
25+
private let encoder = JSONEncoder()
26+
private var lastSentNs: UInt64 = 0
27+
28+
func connect(to endpoint: String) throws {
29+
guard let url = URL(string: endpoint),
30+
url.scheme == "ws" || url.scheme == "wss" else {
31+
throw StreamError.invalidURL
32+
}
33+
task?.cancel(with: .goingAway, reason: nil)
34+
let socket = URLSession.shared.webSocketTask(with: url)
35+
socket.resume()
36+
task = socket
37+
}
38+
39+
func disconnect() {
40+
task?.cancel(with: .goingAway, reason: nil)
41+
task = nil
42+
}
43+
44+
func send(_ frame: RuViewLiDARFrame, maxFPS: UInt64 = 15, sampleStep: Int = 2) async throws {
45+
guard let task else { return }
46+
47+
let timestamp = frame.provenance.timestampNs
48+
let minDelta = 1_000_000_000 / max(1, maxFPS)
49+
guard timestamp >= lastSentNs + minDelta else { return }
50+
lastSentNs = timestamp
51+
52+
let packet = Self.makeWirePacket(frame, sampleStep: max(1, sampleStep))
53+
let data = try encoder.encode(packet)
54+
guard let string = String(data: data, encoding: .utf8) else { return }
55+
try await task.send(.string(string))
56+
}
57+
58+
static func makeWirePacket(_ frame: RuViewLiDARFrame, sampleStep: Int) -> WirePacket {
59+
let step = max(1, sampleStep)
60+
let sourceWidth = frame.depth.width
61+
let sourceHeight = frame.depth.height
62+
let width = (sourceWidth + step - 1) / step
63+
let height = (sourceHeight + step - 1) / step
64+
65+
var millimeters = Data(capacity: width * height * 2)
66+
var confidence = Data(capacity: width * height)
67+
68+
for y in stride(from: 0, to: sourceHeight, by: step) {
69+
for x in stride(from: 0, to: sourceWidth, by: step) {
70+
let index = y * sourceWidth + x
71+
let meters = frame.depth.meters[index]
72+
let mm = UInt16(clamping: Int((meters * 1000).rounded()))
73+
var littleEndian = mm.littleEndian
74+
withUnsafeBytes(of: &littleEndian) { millimeters.append(contentsOf: $0) }
75+
confidence.append(frame.depth.confidence[index])
76+
}
77+
}
78+
79+
return WirePacket(
80+
type: frame.type,
81+
intrinsics: frame.intrinsics,
82+
pose: frame.pose,
83+
depth: Depth(
84+
width: width,
85+
height: height,
86+
encoding: "u16le-mm+u8-confidence",
87+
millimetersBase64: millimeters.base64EncodedString(),
88+
confidenceBase64: confidence.base64EncodedString()
89+
),
90+
provenance: frame.provenance
91+
)
92+
}
93+
}

0 commit comments

Comments
 (0)