feat(wire): topology payload + tag

This commit is contained in:
clement
2026-06-30 21:49:37 +02:00
parent b2dab64aae
commit 6f941c52cb
3 changed files with 82 additions and 0 deletions
@@ -7,6 +7,7 @@ public enum FrameTag: UInt8 {
case hands = 4 case hands = 4
case face = 5 case face = 5
case skeleton2D = 6 case skeleton2D = 6
case topology = 7
} }
/// Fixed-size frame header. Layout (big-endian): /// Fixed-size frame header. Layout (big-endian):
@@ -195,3 +195,52 @@ public struct FacePayload: Equatable {
points = pts points = pts
} }
} }
/// Self-describing skeleton topology: joint names + parent indices.
/// Sent once per connection so consumers draw bones generically
/// (parent -> child) with no hardcoded joint mapping.
/// Layout (big-endian): u16 jointCount; per joint [u8 nameLen + utf8];
/// then per joint i16 parent (-1 = root).
public struct TopologyPayload: Equatable {
public var jointNames: [String]
public var parents: [Int16]
public init(jointNames: [String], parents: [Int16]) {
self.jointNames = jointNames
self.parents = parents
}
public func encoded() -> Data {
var d = Data()
d.appendBE(UInt16(min(jointNames.count, 65535)))
for name in jointNames {
let bytes = Array(name.utf8.prefix(255))
d.append(UInt8(bytes.count))
d.append(contentsOf: bytes)
}
for p in parents { d.appendBE(UInt16(bitPattern: p)) }
return d
}
public init?(decoding data: Data) {
let b = [UInt8](data)
guard b.count >= 2 else { return nil }
let n = Int(UInt16(bigEndianBytes: b[0..<2]))
var o = 2
var names: [String] = []
for _ in 0..<n {
guard o < b.count else { return nil }
let len = Int(b[o]); o += 1
guard o + len <= b.count else { return nil }
names.append(String(decoding: b[o..<o+len], as: UTF8.self))
o += len
}
guard o + n * 2 <= b.count else { return nil }
var ps: [Int16] = []
for _ in 0..<n {
ps.append(Int16(bitPattern: UInt16(bigEndianBytes: b[o..<o+2])))
o += 2
}
jointNames = names
parents = ps
}
}
@@ -0,0 +1,32 @@
import XCTest
@testable import AVLiveWire
final class TopologyPayloadTests: XCTestCase {
func testFrameTagTopology() {
XCTAssertEqual(FrameTag.topology.rawValue, 7)
}
func testRoundTrip() {
let names = ["root_joint", "hips_joint", "left_arm_joint"]
let parents: [Int16] = [-1, 0, 1]
let p = TopologyPayload(jointNames: names, parents: parents)
let r = TopologyPayload(decoding: p.encoded())
XCTAssertNotNil(r)
XCTAssertEqual(r!.jointNames, names)
XCTAssertEqual(r!.parents, parents)
}
func testRejectsTruncated() {
// Claims 3 joints but provides no body.
let bytes: [UInt8] = [0x00, 0x03]
XCTAssertNil(TopologyPayload(decoding: Data(bytes)))
}
func testEmpty() {
let p = TopologyPayload(jointNames: [], parents: [])
let r = TopologyPayload(decoding: p.encoded())
XCTAssertNotNil(r)
XCTAssertEqual(r!.jointNames, [])
XCTAssertEqual(r!.parents, [])
}
}