Full project rename, decided 2026-06-10: - Crates/binaries: punktfunk-core / punktfunk-host / punktfunk-client-rs. - C ABI: punktfunk_* symbols, Punktfunk* types, include/punktfunk_core.h, PUNKTFUNK_FEATURE_QUIC guard (header regenerated; cbindgen renames updated, incl. PUNKTFUNK_BTN_*/PUNKTFUNK_AXIS_* wire constants). - Protocol: punktfunk/1 — control-plane magic LMN1 → PKF1, nonce salt lmn1 → pkf1. WIRE BREAK: clients must be rebuilt from this revision. - Env knobs: PUNKTFUNK_VIDEO_SOURCE / PUNKTFUNK_COMPOSITOR / PUNKTFUNK_ZEROCOPY / …. - Host config dir: ~/.config/punktfunk (the box's dir was migrated in place — the persistent identity is unchanged, pinned fingerprints stay valid). - Swift package: PunktfunkKit + PunktfunkCore.xcframework + PunktfunkConnection (Sources/PunktfunkClient app + tests renamed with it); build-xcframework.sh updated. - scripts/: 60-punktfunk.rules, punktfunk-host.service; OpenAPI doc regenerated. Also: scripts/headless/run-headless-kde.sh — full headless Plasma bringup. Root cause of "desktop but no apps/settings" over the stream: plasmashell launched without XDG_MENU_PREFIX=plasma-, so the launcher resolved a nonexistent applications.menu and rendered an empty menu. The script sets the complete KDE session env (menu prefix, KDE_FULL_SESSION, session version) and rebuilds ksycoca before starting plasmashell. Gate: 97/97 tests, clippy -D warnings (both feature sets), fmt, C-ABI harness PASS, zero lumen references left outside .git. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,18 +1,18 @@
|
||||
# lumen Android client (later)
|
||||
# punktfunk Android client (later)
|
||||
|
||||
Kotlin UI + MediaCodec (decode) + a thin JNI layer over the `lumen-core` C ABI.
|
||||
Kotlin UI + MediaCodec (decode) + a thin JNI layer over the `punktfunk-core` C ABI.
|
||||
|
||||
## Wiring
|
||||
|
||||
1. Build the core as a shared library per Android ABI:
|
||||
```sh
|
||||
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
|
||||
cargo build -p lumen-core --release --target aarch64-linux-android # liblumen_core.so
|
||||
cargo build -p punktfunk-core --release --target aarch64-linux-android # libpunktfunk_core.so
|
||||
```
|
||||
(Use `cargo-ndk` to handle the NDK toolchain/linker.)
|
||||
2. JNI shim: small C/Rust glue mapping `lumen_*` to Kotlin `external fun`s, bundling
|
||||
`liblumen_core.so` into the APK's `jniLibs/`.
|
||||
3. Kotlin: client `LumenSession` → `lumen_client_poll_frame` on a decode thread → feed
|
||||
2. JNI shim: small C/Rust glue mapping `punktfunk_*` to Kotlin `external fun`s, bundling
|
||||
`libpunktfunk_core.so` into the APK's `jniLibs/`.
|
||||
3. Kotlin: client `PunktfunkSession` → `punktfunk_client_poll_frame` on a decode thread → feed
|
||||
`MediaCodec` → render to a `SurfaceView` aligned to the display refresh.
|
||||
|
||||
## Status
|
||||
|
||||
+11
-11
@@ -1,21 +1,21 @@
|
||||
// swift-tools-version: 5.9
|
||||
// LumenKit — Swift wrapper around the lumen-core C ABI (lumen/1 client connector) plus the
|
||||
// SwiftUI/VideoToolbox presentation layer. Build LumenCore.xcframework first:
|
||||
// PunktfunkKit — Swift wrapper around the punktfunk-core C ABI (punktfunk/1 client connector) plus the
|
||||
// SwiftUI/VideoToolbox presentation layer. Build PunktfunkCore.xcframework first:
|
||||
// bash ../../scripts/build-xcframework.sh (on a Mac; see README.md)
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "LumenKit",
|
||||
name: "PunktfunkKit",
|
||||
platforms: [.macOS(.v14), .iOS(.v17)],
|
||||
products: [
|
||||
.library(name: "LumenKit", targets: ["LumenKit"]),
|
||||
.executable(name: "LumenClient", targets: ["LumenClient"]),
|
||||
.library(name: "PunktfunkKit", targets: ["PunktfunkKit"]),
|
||||
.executable(name: "PunktfunkClient", targets: ["PunktfunkClient"]),
|
||||
],
|
||||
targets: [
|
||||
.binaryTarget(name: "LumenCore", path: "LumenCore.xcframework"),
|
||||
.binaryTarget(name: "PunktfunkCore", path: "PunktfunkCore.xcframework"),
|
||||
.target(
|
||||
name: "LumenKit",
|
||||
dependencies: ["LumenCore"],
|
||||
name: "PunktfunkKit",
|
||||
dependencies: ["PunktfunkCore"],
|
||||
linkerSettings: [
|
||||
// Rust staticlib system deps.
|
||||
.linkedFramework("Security"),
|
||||
@@ -23,8 +23,8 @@ let package = Package(
|
||||
.linkedLibrary("resolv"),
|
||||
]
|
||||
),
|
||||
// Development app shell (swift run LumenClient): connect form → stream + input.
|
||||
.executableTarget(name: "LumenClient", dependencies: ["LumenKit"]),
|
||||
.testTarget(name: "LumenKitTests", dependencies: ["LumenKit"]),
|
||||
// Development app shell (swift run PunktfunkClient): connect form → stream + input.
|
||||
.executableTarget(name: "PunktfunkClient", dependencies: ["PunktfunkKit"]),
|
||||
.testTarget(name: "PunktfunkKitTests", dependencies: ["PunktfunkKit"]),
|
||||
]
|
||||
)
|
||||
|
||||
+20
-20
@@ -1,31 +1,31 @@
|
||||
# lumen Apple client (SwiftUI)
|
||||
# punktfunk Apple client (SwiftUI)
|
||||
|
||||
The native macOS/iOS client for **`lumen/1`** (the post-GameStream protocol). All
|
||||
The native macOS/iOS client for **`punktfunk/1`** (the post-GameStream protocol). All
|
||||
networking/protocol work — QUIC control plane, UDP data plane, GF(2¹⁶) FEC, AES-GCM,
|
||||
input datagrams, Opus audio, cert pinning — lives in the shared Rust core (statically
|
||||
linked as `LumenCore.xcframework`); this package is the Swift shell: decode
|
||||
linked as `PunktfunkCore.xcframework`); this package is the Swift shell: decode
|
||||
(VideoToolbox), present (SwiftUI), input capture.
|
||||
|
||||
## Status — first light achieved (2026-06-10)
|
||||
|
||||
Validated live, Mac ↔ Linux box over the LAN: gamescope virtual output → NVENC HEVC →
|
||||
`lumen/1` (GF(2¹⁶) FEC + AES-GCM over UDP, QUIC control) → VideoToolbox →
|
||||
`punktfunk/1` (GF(2¹⁶) FEC + AES-GCM over UDP, QUIC control) → VideoToolbox →
|
||||
`AVSampleBufferDisplayLayer` on glass at 1280×720@60, with mouse/keyboard flowing back as
|
||||
QUIC datagrams into the host's gamescope EIS injector (thousands of events injected during
|
||||
the session). Headless variant of the same proof: `RemoteFirstLightTests` decoded 60/60
|
||||
received AUs spanning 983 ms of host capture clock.
|
||||
|
||||
The connector underneath (`lumen_core::client::NativeClient` over the C ABI) carries the
|
||||
The connector underneath (`punktfunk_core::client::NativeClient` over the C ABI) carries the
|
||||
full session: video AUs, **Opus audio** (`nextAudio()`), **rumble** (`nextRumble()`),
|
||||
input incl. gamepads, and **cert pinning + TOFU** (`pinSHA256:`/`hostFingerprint`) — see
|
||||
`m3.rs::tests::c_abi_connection_roundtrip` (three sequential sessions: TOFU, pinned
|
||||
reconnect, wrong-pin rejection). The host (`lumen-host m3-host`) is a persistent listener:
|
||||
reconnect, wrong-pin rejection). The host (`punktfunk-host m3-host`) is a persistent listener:
|
||||
reconnect at will during development.
|
||||
|
||||
What's here, all compiled and tested on macOS (Xcode 26.5 / Swift 6.3):
|
||||
|
||||
- **`LumenKit`** (library)
|
||||
- `LumenConnection.swift` — wrapper over the C ABI. AUs/audio are copied into `Data`
|
||||
- **`PunktfunkKit`** (library)
|
||||
- `PunktfunkConnection.swift` — wrapper over the C ABI. AUs/audio are copied into `Data`
|
||||
(the C pointer is only valid until the next call of the same kind). `close()` is safe
|
||||
from any thread: per-plane locks enforce the C contract ("never close with a
|
||||
`next_au`/`next_audio` in flight") instead of leaving it to callers. Pinning + TOFU
|
||||
@@ -39,7 +39,7 @@ What's here, all compiled and tested on macOS (Xcode 26.5 / Swift 6.3):
|
||||
`vk_to_evdev` consumes Windows VKs), with fractional-delta accumulation so sub-pixel
|
||||
motion isn't truncated away. Buttons use GameStream ids (1=left … 5=X2); scroll is
|
||||
WHEEL_DELTA(120)-scaled.
|
||||
- **`LumenClient`** (development app shell): connect form → stream + input, fps/Mb-s HUD.
|
||||
- **`PunktfunkClient`** (development app shell): connect form → stream + input, fps/Mb-s HUD.
|
||||
(Audio playback and gamepad capture are not wired into the app yet — the connector
|
||||
surface is there; see notes 5–6.)
|
||||
- **Tests** (`swift test`): byte-level Annex-B units; a real-codec round trip
|
||||
@@ -51,29 +51,29 @@ What's here, all compiled and tested on macOS (Xcode 26.5 / Swift 6.3):
|
||||
|
||||
```sh
|
||||
rustup target add aarch64-apple-darwin x86_64-apple-darwin
|
||||
bash scripts/build-xcframework.sh # → clients/apple/LumenCore.xcframework
|
||||
bash scripts/build-xcframework.sh # → clients/apple/PunktfunkCore.xcframework
|
||||
cd clients/apple
|
||||
swift build && swift test # loopback/remote tests self-skip without a host
|
||||
swift run LumenClient # the app; or open Package.swift in Xcode
|
||||
swift run PunktfunkClient # the app; or open Package.swift in Xcode
|
||||
|
||||
bash test-loopback.sh # full loopback proof: builds lumen-host
|
||||
bash test-loopback.sh # full loopback proof: builds punktfunk-host
|
||||
# (synthetic source — runs on macOS), streams
|
||||
# byte-verified frames into the Swift client
|
||||
|
||||
# against the real host (Linux box, see CLAUDE.md "Running on this box") — m3-host is a
|
||||
# persistent listener, reconnect at will:
|
||||
# LUMEN_COMPOSITOR=gamescope LUMEN_GAMESCOPE_APP=vkcube LUMEN_ZEROCOPY=1 \
|
||||
# cargo run -rp lumen-host -- m3-host --source virtual --seconds 60
|
||||
LUMEN_REMOTE_HOST=<box-ip> swift test --filter RemoteFirstLightTests # headless
|
||||
LUMEN_AUTOCONNECT=<box-ip> LUMEN_MODE=1280x720x60 swift run LumenClient # on glass
|
||||
# PUNKTFUNK_COMPOSITOR=gamescope PUNKTFUNK_GAMESCOPE_APP=vkcube PUNKTFUNK_ZEROCOPY=1 \
|
||||
# cargo run -rp punktfunk-host -- m3-host --source virtual --seconds 60
|
||||
PUNKTFUNK_REMOTE_HOST=<box-ip> swift test --filter RemoteFirstLightTests # headless
|
||||
PUNKTFUNK_AUTOCONNECT=<box-ip> PUNKTFUNK_MODE=1280x720x60 swift run PunktfunkClient # on glass
|
||||
```
|
||||
|
||||
## Notes for whoever picks this up next
|
||||
|
||||
1. **cbindgen import quirk** (the predicted "small compile fixes", now fixed): the
|
||||
C17-compatible header spells `LumenStatus`/`LumenInputKind` as integer typedefs while
|
||||
C17-compatible header spells `PunktfunkStatus`/`PunktfunkInputKind` as integer typedefs while
|
||||
the enum *constants* import into Swift as a distinct same-named type — bridge with
|
||||
`.rawValue` (see the top of `LumenConnection.swift`). Don't fight the generated header.
|
||||
`.rawValue` (see the top of `PunktfunkConnection.swift`). Don't fight the generated header.
|
||||
2. **ABI contract**: one video pump thread per connection, plus optionally one *separate*
|
||||
audio drain thread for `nextAudio()`/`nextRumble()` (the core keeps per-plane borrow
|
||||
slots, so the planes never alias); `send()` is enqueue-only and safe alongside all of
|
||||
@@ -91,7 +91,7 @@ LUMEN_AUTOCONNECT=<box-ip> LUMEN_MODE=1280x720x60 swift run LumenClient # on gla
|
||||
`AVAudioEngine` source node; conceal gaps (drop/dup) rather than blocking — the Rust
|
||||
side buffers 320 ms and drops the newest packet when the puller lags. Wall-clock
|
||||
`ptsNs` shares the host clock with video AUs for A/V sync. Wiring this into
|
||||
`LumenClient` is the next app-side task.
|
||||
`PunktfunkClient` is the next app-side task.
|
||||
6. **Gamepads**: `GCController` → `.gamepadButton(...)`/`.gamepadAxis(...)` events (wire
|
||||
contract documented on the constructors; the host accumulates them into a virtual
|
||||
Xbox 360 pad). Poll `nextRumble()` and feed `GCDeviceHaptics` for force feedback.
|
||||
@@ -99,7 +99,7 @@ LUMEN_AUTOCONNECT=<box-ip> LUMEN_MODE=1280x720x60 swift run LumenClient # on gla
|
||||
7. **Trust**: connect once with `pinSHA256: nil` (TOFU), persist `hostFingerprint` keyed
|
||||
by host, pass it on every later connect — a mismatch throws `.connectFailed`. The host
|
||||
logs its fingerprint at startup ("clients pin this fingerprint") for out-of-band
|
||||
verification UX; a PIN-style pairing ceremony is a later lumen-core task. `LumenClient`
|
||||
verification UX; a PIN-style pairing ceremony is a later punktfunk-core task. `PunktfunkClient`
|
||||
doesn't persist fingerprints yet — add it alongside the "add host" UX.
|
||||
8. **Input capture caveats** (stage 1): GC handlers only fire while the app has focus —
|
||||
on focus loss `InputCapture` auto-releases everything still held (keys + buttons) so
|
||||
|
||||
+13
-13
@@ -1,16 +1,16 @@
|
||||
// Connect form ⇄ live stream. Stage-1 UX: pick host + mode, see frames, type/aim.
|
||||
|
||||
import AppKit
|
||||
import LumenKit
|
||||
import PunktfunkKit
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@StateObject private var model = SessionModel()
|
||||
@AppStorage("lumen.host") private var host = "192.168.1.70"
|
||||
@AppStorage("lumen.port") private var port = 9777
|
||||
@AppStorage("lumen.width") private var width = 1920
|
||||
@AppStorage("lumen.height") private var height = 1080
|
||||
@AppStorage("lumen.hz") private var hz = 60
|
||||
@AppStorage("punktfunk.host") private var host = "192.168.1.70"
|
||||
@AppStorage("punktfunk.port") private var port = 9777
|
||||
@AppStorage("punktfunk.width") private var width = 1920
|
||||
@AppStorage("punktfunk.height") private var height = 1080
|
||||
@AppStorage("punktfunk.hz") private var hz = 60
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
@@ -24,17 +24,17 @@ struct ContentView: View {
|
||||
.onDisappear { model.disconnect() } // window closed mid-session (Cmd+N spawns more)
|
||||
}
|
||||
|
||||
/// Development hook: LUMEN_AUTOCONNECT=host[:port] connects immediately at the saved
|
||||
/// (or LUMEN_MODE=WxHxHz) mode — lets scripts drive first-light runs. (IPv4/hostname
|
||||
/// Development hook: PUNKTFUNK_AUTOCONNECT=host[:port] connects immediately at the saved
|
||||
/// (or PUNKTFUNK_MODE=WxHxHz) mode — lets scripts drive first-light runs. (IPv4/hostname
|
||||
/// only; an IPv6 literal would need bracket parsing.)
|
||||
private func autoConnectIfAsked() {
|
||||
guard let target = ProcessInfo.processInfo.environment["LUMEN_AUTOCONNECT"],
|
||||
guard let target = ProcessInfo.processInfo.environment["PUNKTFUNK_AUTOCONNECT"],
|
||||
!target.isEmpty, model.connection == nil, !model.connecting
|
||||
else { return }
|
||||
let parts = target.split(separator: ":")
|
||||
host = String(parts[0])
|
||||
if parts.count == 2, let p = Int(parts[1]) { port = p }
|
||||
if let mode = ProcessInfo.processInfo.environment["LUMEN_MODE"] {
|
||||
if let mode = ProcessInfo.processInfo.environment["PUNKTFUNK_MODE"] {
|
||||
let dims = mode.split(separator: "x").compactMap { Int($0) }
|
||||
if dims.count == 3 {
|
||||
width = dims[0]
|
||||
@@ -48,7 +48,7 @@ struct ContentView: View {
|
||||
hz: UInt32(clamping: hz))
|
||||
}
|
||||
|
||||
private func stream(_ conn: LumenConnection) -> some View {
|
||||
private func stream(_ conn: PunktfunkConnection) -> some View {
|
||||
StreamView(
|
||||
connection: conn,
|
||||
onFrame: { [meter = model.meter] au in meter.note(byteCount: au.data.count) },
|
||||
@@ -61,7 +61,7 @@ struct ContentView: View {
|
||||
.background(Color.black)
|
||||
}
|
||||
|
||||
private func hud(_ conn: LumenConnection) -> some View {
|
||||
private func hud(_ conn: PunktfunkConnection) -> some View {
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
Text("\(conn.width)×\(conn.height)@\(conn.refreshHz) \(model.fps) fps \(model.mbps, specifier: "%.1f") Mb/s")
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
@@ -76,7 +76,7 @@ struct ContentView: View {
|
||||
|
||||
private var connectForm: some View {
|
||||
VStack(spacing: 14) {
|
||||
Text("lumen").font(.largeTitle.weight(.semibold))
|
||||
Text("punktfunk").font(.largeTitle.weight(.semibold))
|
||||
Form {
|
||||
TextField("Host", text: $host)
|
||||
TextField("Port", value: $port, format: .number.grouping(.never))
|
||||
+3
-3
@@ -1,15 +1,15 @@
|
||||
// LumenClient — development app shell around LumenKit (swift run LumenClient).
|
||||
// PunktfunkClient — development app shell around PunktfunkKit (swift run PunktfunkClient).
|
||||
// Connect form → StreamView (AVSampleBufferDisplayLayer HEVC) + InputCapture.
|
||||
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct LumenClientApp: App {
|
||||
struct PunktfunkClientApp: App {
|
||||
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup("lumen") {
|
||||
WindowGroup("punktfunk") {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -2,7 +2,7 @@
|
||||
// pump-thread → main-actor stats relay.
|
||||
|
||||
import Foundation
|
||||
import LumenKit
|
||||
import PunktfunkKit
|
||||
import SwiftUI
|
||||
|
||||
/// Pump-thread-side frame counters; a 1 Hz main-actor timer drains them into @Published
|
||||
@@ -35,7 +35,7 @@ final class FrameMeter: @unchecked Sendable {
|
||||
|
||||
@MainActor
|
||||
final class SessionModel: ObservableObject {
|
||||
@Published var connection: LumenConnection?
|
||||
@Published var connection: PunktfunkConnection?
|
||||
@Published var connecting = false
|
||||
@Published var errorMessage: String?
|
||||
@Published var fps = 0
|
||||
@@ -51,8 +51,8 @@ final class SessionModel: ObservableObject {
|
||||
connecting = true
|
||||
errorMessage = nil
|
||||
Task.detached(priority: .userInitiated) {
|
||||
// LumenConnection.init blocks on the QUIC handshake — keep it off the main actor.
|
||||
let result = Result { try LumenConnection(
|
||||
// PunktfunkConnection.init blocks on the QUIC handshake — keep it off the main actor.
|
||||
let result = Result { try PunktfunkConnection(
|
||||
host: host, port: port, width: width, height: height, refreshHz: hz) }
|
||||
await MainActor.run { [weak self] in
|
||||
guard let self else { return }
|
||||
@@ -64,7 +64,7 @@ final class SessionModel: ObservableObject {
|
||||
self.startStatsTimer()
|
||||
case .failure:
|
||||
self.errorMessage = "Connection failed — is the host running? " +
|
||||
"(lumen-host m3-host on \(host):\(port))"
|
||||
"(punktfunk-host m3-host on \(host):\(port))"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ final class SessionModel: ObservableObject {
|
||||
errorMessage = "Session ended by host."
|
||||
}
|
||||
|
||||
private func startInput(_ conn: LumenConnection) {
|
||||
private func startInput(_ conn: PunktfunkConnection) {
|
||||
let capture = InputCapture(connection: conn)
|
||||
capture.start()
|
||||
inputCapture = capture
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
// Annex-B HEVC → CoreMedia plumbing.
|
||||
//
|
||||
// The lumen host emits Annex-B access units with in-band VPS/SPS/PPS on every IDR
|
||||
// The punktfunk host emits Annex-B access units with in-band VPS/SPS/PPS on every IDR
|
||||
// (deliberately — the client needs no out-of-band extradata). VideoToolbox wants the AVCC
|
||||
// flavor instead: a CMVideoFormatDescription built from the parameter sets, and sample
|
||||
// buffers whose NALs are 4-byte-length-prefixed. This file converts between the two.
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
// Input capture → lumen/1 datagrams, via the GameController framework.
|
||||
// Input capture → punktfunk/1 datagrams, via the GameController framework.
|
||||
//
|
||||
// GCMouse delivers RAW deltas (not the accelerated cursor) — exactly what the host-side
|
||||
// injector expects for relative motion. GCKeyboard gives HID keycodes which we map to the
|
||||
@@ -22,12 +22,12 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import GameController
|
||||
import LumenCore
|
||||
import PunktfunkCore
|
||||
|
||||
public final class InputCapture {
|
||||
private static weak var activeCapture: InputCapture?
|
||||
|
||||
private let connection: LumenConnection
|
||||
private let connection: PunktfunkConnection
|
||||
private var observers: [NSObjectProtocol] = []
|
||||
private var mice: [GCMouse] = []
|
||||
private var keyboards: [GCKeyboard] = []
|
||||
@@ -40,7 +40,7 @@ public final class InputCapture {
|
||||
private var pressedVKs: Set<UInt32> = []
|
||||
private var pressedButtons: Set<UInt32> = []
|
||||
|
||||
public init(connection: LumenConnection) {
|
||||
public init(connection: PunktfunkConnection) {
|
||||
self.connection = connection
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ public final class InputCapture {
|
||||
}
|
||||
|
||||
/// HID usage (GCKeyCode raw) → Windows VK (the host maps VK → evdev; every VK emitted
|
||||
/// here exists in lumen-host/src/inject.rs::vk_to_evdev — extend the two together).
|
||||
/// here exists in punktfunk-host/src/inject.rs::vk_to_evdev — extend the two together).
|
||||
static let hidToVK: [Int: UInt32] = {
|
||||
var m: [Int: UInt32] = [:]
|
||||
// a–z: HID 0x04..0x1D → VK 'A'..'Z'.
|
||||
+48
-48
@@ -1,6 +1,6 @@
|
||||
// Swift wrapper around the lumen-core C ABI's lumen/1 connection API.
|
||||
// Swift wrapper around the punktfunk-core C ABI's punktfunk/1 connection API.
|
||||
//
|
||||
// Threading contract (mirrors the C header): one LumenConnection is pumped from a single
|
||||
// Threading contract (mirrors the C header): one PunktfunkConnection is pumped from a single
|
||||
// video thread via nextAU(); nextAudio()/nextRumble() may each run on their own (single)
|
||||
// drain thread — the core keeps per-plane borrow slots, so the planes never alias;
|
||||
// send() is enqueue-only and safe alongside all of them. The pointers inside an AU/audio
|
||||
@@ -18,14 +18,14 @@
|
||||
// close, the pull methods throw `.closed` and the threads unwind on their own.
|
||||
|
||||
import Foundation
|
||||
import LumenCore
|
||||
import PunktfunkCore
|
||||
|
||||
// cbindgen's C17-compatible header spells the typedefs as plain integers
|
||||
// (`typedef int32_t LumenStatus`, `typedef uint8_t LumenInputKind`) while the enum
|
||||
// (`typedef int32_t PunktfunkStatus`, `typedef uint8_t PunktfunkInputKind`) while the enum
|
||||
// constants import as a distinct same-named Swift type — bridge by raw value once here.
|
||||
private let statusOK: Int32 = LUMEN_STATUS_OK.rawValue
|
||||
private let statusNoFrame: Int32 = LUMEN_STATUS_NO_FRAME.rawValue
|
||||
private let statusClosed: Int32 = LUMEN_STATUS_CLOSED.rawValue
|
||||
private let statusOK: Int32 = PUNKTFUNK_STATUS_OK.rawValue
|
||||
private let statusNoFrame: Int32 = PUNKTFUNK_STATUS_NO_FRAME.rawValue
|
||||
private let statusClosed: Int32 = PUNKTFUNK_STATUS_CLOSED.rawValue
|
||||
|
||||
/// One reassembled, FEC-recovered, decrypted access unit (Annex-B HEVC from the host).
|
||||
public struct AccessUnit: Sendable {
|
||||
@@ -43,7 +43,7 @@ public struct AudioPacket: Sendable {
|
||||
public let seq: UInt32
|
||||
}
|
||||
|
||||
public enum LumenClientError: Error {
|
||||
public enum PunktfunkClientError: Error {
|
||||
/// Connect failed — wrong host/port, timeout, or a certificate-pin mismatch.
|
||||
case connectFailed
|
||||
/// `pinSHA256` was non-nil but not exactly 32 bytes. Failing closed: connecting
|
||||
@@ -53,7 +53,7 @@ public enum LumenClientError: Error {
|
||||
case status(Int32)
|
||||
}
|
||||
|
||||
public final class LumenConnection {
|
||||
public final class PunktfunkConnection {
|
||||
private var handle: OpaquePointer?
|
||||
/// Set by close() before it contends for the plane locks: the pullers see it at their
|
||||
/// next poll boundary and exit, so close() can't be starved by back-to-back polls
|
||||
@@ -88,22 +88,22 @@ public final class LumenConnection {
|
||||
pinSHA256: Data? = nil,
|
||||
timeoutMs: UInt32 = 10_000
|
||||
) throws {
|
||||
if let pin = pinSHA256, pin.count != 32 { throw LumenClientError.invalidPin }
|
||||
if let pin = pinSHA256, pin.count != 32 { throw PunktfunkClientError.invalidPin }
|
||||
var observed = [UInt8](repeating: 0, count: 32)
|
||||
handle = host.withCString { cs in
|
||||
if let pin = pinSHA256 {
|
||||
return pin.withUnsafeBytes { p in
|
||||
lumen_connect(
|
||||
punktfunk_connect(
|
||||
cs, port, width, height, refreshHz,
|
||||
p.bindMemory(to: UInt8.self).baseAddress, &observed, timeoutMs)
|
||||
}
|
||||
}
|
||||
return lumen_connect(cs, port, width, height, refreshHz, nil, &observed, timeoutMs)
|
||||
return punktfunk_connect(cs, port, width, height, refreshHz, nil, &observed, timeoutMs)
|
||||
}
|
||||
guard handle != nil else { throw LumenClientError.connectFailed }
|
||||
guard handle != nil else { throw PunktfunkClientError.connectFailed }
|
||||
hostFingerprint = Data(observed)
|
||||
var w: UInt32 = 0, h: UInt32 = 0, hz: UInt32 = 0
|
||||
_ = lumen_connection_mode(handle, &w, &h, &hz)
|
||||
_ = punktfunk_connection_mode(handle, &w, &h, &hz)
|
||||
self.width = w
|
||||
self.height = h
|
||||
self.refreshHz = hz
|
||||
@@ -114,10 +114,10 @@ public final class LumenConnection {
|
||||
public func nextAU(timeoutMs: UInt32 = 100) throws -> AccessUnit? {
|
||||
pumpLock.lock()
|
||||
defer { pumpLock.unlock() }
|
||||
guard let h = liveHandle() else { throw LumenClientError.closed }
|
||||
guard let h = liveHandle() else { throw PunktfunkClientError.closed }
|
||||
|
||||
var frame = LumenFrame()
|
||||
let rc = lumen_connection_next_au(h, &frame, timeoutMs)
|
||||
var frame = PunktfunkFrame()
|
||||
let rc = punktfunk_connection_next_au(h, &frame, timeoutMs)
|
||||
switch rc {
|
||||
case statusOK:
|
||||
guard let base = frame.data, frame.len > 0 else { return nil }
|
||||
@@ -128,9 +128,9 @@ public final class LumenConnection {
|
||||
case statusNoFrame:
|
||||
return nil
|
||||
case statusClosed:
|
||||
throw LumenClientError.closed
|
||||
throw PunktfunkClientError.closed
|
||||
default:
|
||||
throw LumenClientError.status(rc)
|
||||
throw PunktfunkClientError.status(rc)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,10 +140,10 @@ public final class LumenConnection {
|
||||
public func nextAudio(timeoutMs: UInt32 = 100) throws -> AudioPacket? {
|
||||
audioLock.lock()
|
||||
defer { audioLock.unlock() }
|
||||
guard let h = liveHandle() else { throw LumenClientError.closed }
|
||||
guard let h = liveHandle() else { throw PunktfunkClientError.closed }
|
||||
|
||||
var pkt = LumenAudioPacket()
|
||||
let rc = lumen_connection_next_audio(h, &pkt, timeoutMs)
|
||||
var pkt = PunktfunkAudioPacket()
|
||||
let rc = punktfunk_connection_next_audio(h, &pkt, timeoutMs)
|
||||
switch rc {
|
||||
case statusOK:
|
||||
guard let base = pkt.data, pkt.len > 0 else { return nil }
|
||||
@@ -152,9 +152,9 @@ public final class LumenConnection {
|
||||
case statusNoFrame:
|
||||
return nil
|
||||
case statusClosed:
|
||||
throw LumenClientError.closed
|
||||
throw PunktfunkClientError.closed
|
||||
default:
|
||||
throw LumenClientError.status(rc)
|
||||
throw PunktfunkClientError.status(rc)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,30 +164,30 @@ public final class LumenConnection {
|
||||
public func nextRumble(timeoutMs: UInt32 = 0) throws -> (pad: UInt16, low: UInt16, high: UInt16)? {
|
||||
audioLock.lock()
|
||||
defer { audioLock.unlock() }
|
||||
guard let h = liveHandle() else { throw LumenClientError.closed }
|
||||
guard let h = liveHandle() else { throw PunktfunkClientError.closed }
|
||||
|
||||
var pad: UInt16 = 0, low: UInt16 = 0, high: UInt16 = 0
|
||||
let rc = lumen_connection_next_rumble(h, &pad, &low, &high, timeoutMs)
|
||||
let rc = punktfunk_connection_next_rumble(h, &pad, &low, &high, timeoutMs)
|
||||
switch rc {
|
||||
case statusOK:
|
||||
return (pad, low, high)
|
||||
case statusNoFrame:
|
||||
return nil
|
||||
case statusClosed:
|
||||
throw LumenClientError.closed
|
||||
throw PunktfunkClientError.closed
|
||||
default:
|
||||
throw LumenClientError.status(rc)
|
||||
throw PunktfunkClientError.status(rc)
|
||||
}
|
||||
}
|
||||
|
||||
/// Send one input event (delivered to the host as a QUIC datagram). Thread-safe;
|
||||
/// silently dropped after close.
|
||||
public func send(_ event: LumenInputEvent) {
|
||||
public func send(_ event: PunktfunkInputEvent) {
|
||||
var ev = event
|
||||
abiLock.lock()
|
||||
defer { abiLock.unlock() }
|
||||
guard let h = handle, !closeRequested else { return }
|
||||
_ = lumen_connection_send_input(h, &ev)
|
||||
_ = punktfunk_connection_send_input(h, &ev)
|
||||
}
|
||||
|
||||
/// Close the connection and free the handle. Safe from any thread, idempotent; waits
|
||||
@@ -205,7 +205,7 @@ public final class LumenConnection {
|
||||
audioLock.unlock()
|
||||
pumpLock.unlock()
|
||||
if let h {
|
||||
lumen_connection_close(h) // joins the connection's internal Rust threads
|
||||
punktfunk_connection_close(h) // joins the connection's internal Rust threads
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,46 +220,46 @@ public final class LumenConnection {
|
||||
}
|
||||
|
||||
// Convenience constructors for the wire input events (field semantics match
|
||||
// lumen_core::input::InputEvent; see lumen_core.h).
|
||||
public extension LumenInputEvent {
|
||||
// punktfunk_core::input::InputEvent; see punktfunk_core.h).
|
||||
public extension PunktfunkInputEvent {
|
||||
private static func make(
|
||||
_ kind: UInt32, code: UInt32, x: Int32, y: Int32, flags: UInt32 = 0
|
||||
) -> LumenInputEvent {
|
||||
LumenInputEvent(kind: UInt8(kind), _pad: (0, 0, 0), code: code, x: x, y: y, flags: flags)
|
||||
) -> PunktfunkInputEvent {
|
||||
PunktfunkInputEvent(kind: UInt8(kind), _pad: (0, 0, 0), code: code, x: x, y: y, flags: flags)
|
||||
}
|
||||
static func mouseMove(dx: Int32, dy: Int32) -> LumenInputEvent {
|
||||
make(LUMEN_INPUT_KIND_MOUSE_MOVE.rawValue, code: 0, x: dx, y: dy)
|
||||
static func mouseMove(dx: Int32, dy: Int32) -> PunktfunkInputEvent {
|
||||
make(PUNKTFUNK_INPUT_KIND_MOUSE_MOVE.rawValue, code: 0, x: dx, y: dy)
|
||||
}
|
||||
/// GameStream button ids: 1=left 2=middle 3=right 4=X1 5=X2 (host maps to evdev BTN_*).
|
||||
static func mouseButton(_ button: UInt32, down: Bool) -> LumenInputEvent {
|
||||
static func mouseButton(_ button: UInt32, down: Bool) -> PunktfunkInputEvent {
|
||||
make(
|
||||
(down ? LUMEN_INPUT_KIND_MOUSE_BUTTON_DOWN : LUMEN_INPUT_KIND_MOUSE_BUTTON_UP).rawValue,
|
||||
(down ? PUNKTFUNK_INPUT_KIND_MOUSE_BUTTON_DOWN : PUNKTFUNK_INPUT_KIND_MOUSE_BUTTON_UP).rawValue,
|
||||
code: button, x: 0, y: 0)
|
||||
}
|
||||
/// `vk` is a Windows virtual-key code (the host's vk_to_evdev table consumes these).
|
||||
static func key(_ vk: UInt32, down: Bool) -> LumenInputEvent {
|
||||
make((down ? LUMEN_INPUT_KIND_KEY_DOWN : LUMEN_INPUT_KIND_KEY_UP).rawValue, code: vk, x: 0, y: 0)
|
||||
static func key(_ vk: UInt32, down: Bool) -> PunktfunkInputEvent {
|
||||
make((down ? PUNKTFUNK_INPUT_KIND_KEY_DOWN : PUNKTFUNK_INPUT_KIND_KEY_UP).rawValue, code: vk, x: 0, y: 0)
|
||||
}
|
||||
/// WHEEL_DELTA(120)-scaled; positive = up (vertical) / right (horizontal) — the
|
||||
/// convention Moonlight/SDL use; the host maps onto the ei/wl axes.
|
||||
static func scroll(_ delta: Int32, horizontal: Bool = false) -> LumenInputEvent {
|
||||
make(LUMEN_INPUT_KIND_MOUSE_SCROLL.rawValue, code: horizontal ? 1 : 0, x: delta, y: 0)
|
||||
static func scroll(_ delta: Int32, horizontal: Bool = false) -> PunktfunkInputEvent {
|
||||
make(PUNKTFUNK_INPUT_KIND_MOUSE_SCROLL.rawValue, code: horizontal ? 1 : 0, x: delta, y: 0)
|
||||
}
|
||||
|
||||
// Gamepad (wire contract in lumen_core::input::gamepad): one transition per event,
|
||||
// Gamepad (wire contract in punktfunk_core::input::gamepad): one transition per event,
|
||||
// `pad` = controller index, accumulated host-side into a virtual Xbox 360 pad.
|
||||
|
||||
/// `button` is a GameStream buttonFlags bit (A=0x1000 B=0x2000 X=0x4000 Y=0x8000,
|
||||
/// dpad=0x1/2/4/8, start=0x10 back=0x20 LS=0x40 RS=0x80 LB=0x100 RB=0x200 guide=0x400).
|
||||
static func gamepadButton(_ button: UInt32, down: Bool, pad: UInt32 = 0) -> LumenInputEvent {
|
||||
static func gamepadButton(_ button: UInt32, down: Bool, pad: UInt32 = 0) -> PunktfunkInputEvent {
|
||||
make(
|
||||
LUMEN_INPUT_KIND_GAMEPAD_BUTTON.rawValue,
|
||||
PUNKTFUNK_INPUT_KIND_GAMEPAD_BUTTON.rawValue,
|
||||
code: button, x: down ? 1 : 0, y: 0, flags: pad)
|
||||
}
|
||||
|
||||
/// Axis ids: 0=LSX 1=LSY 2=RSX 3=RSY (−32768...32767, XInput convention: +y = UP —
|
||||
/// `GCControllerDirectionPad.yAxis` already matches, no flip), 4=LT 5=RT (0...255).
|
||||
static func gamepadAxis(_ axis: UInt32, value: Int32, pad: UInt32 = 0) -> LumenInputEvent {
|
||||
make(LUMEN_INPUT_KIND_GAMEPAD_AXIS.rawValue, code: axis, x: value, y: 0, flags: pad)
|
||||
static func gamepadAxis(_ axis: UInt32, value: Int32, pad: UInt32 = 0) -> PunktfunkInputEvent {
|
||||
make(PUNKTFUNK_INPUT_KIND_GAMEPAD_AXIS.rawValue, code: axis, x: value, y: 0, flags: pad)
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -1,4 +1,4 @@
|
||||
// SwiftUI presentation: AVSampleBufferDisplayLayer fed straight from the lumen/1 connection.
|
||||
// SwiftUI presentation: AVSampleBufferDisplayLayer fed straight from the punktfunk/1 connection.
|
||||
//
|
||||
// Stage-1 presenter (see README): the layer accepts *compressed* HEVC sample buffers and
|
||||
// does hardware decode + display itself — fastest path to pixels, IOSurface-backed
|
||||
@@ -13,13 +13,13 @@ import AVFoundation
|
||||
import SwiftUI
|
||||
|
||||
public struct StreamView: NSViewRepresentable {
|
||||
private let connection: LumenConnection
|
||||
private let connection: PunktfunkConnection
|
||||
private let onFrame: (@Sendable (AccessUnit) -> Void)?
|
||||
private let onSessionEnd: (@Sendable () -> Void)?
|
||||
|
||||
/// `onFrame`/`onSessionEnd` fire on the pump thread — hop to the main actor for UI.
|
||||
public init(
|
||||
connection: LumenConnection,
|
||||
connection: PunktfunkConnection,
|
||||
onFrame: (@Sendable (AccessUnit) -> Void)? = nil,
|
||||
onSessionEnd: (@Sendable () -> Void)? = nil
|
||||
) {
|
||||
@@ -67,7 +67,7 @@ public final class StreamLayerView: NSView {
|
||||
|
||||
private let displayLayer = AVSampleBufferDisplayLayer()
|
||||
private var token: PumpToken?
|
||||
public private(set) var connection: LumenConnection?
|
||||
public private(set) var connection: PunktfunkConnection?
|
||||
|
||||
public override init(frame: NSRect) {
|
||||
super.init(frame: frame)
|
||||
@@ -81,7 +81,7 @@ public final class StreamLayerView: NSView {
|
||||
/// Pump thread: pull AUs from the connection, wrap, enqueue. The first IDR yields the
|
||||
/// format description; non-IDR AUs before it are dropped (the host opens with an IDR).
|
||||
public func start(
|
||||
connection: LumenConnection,
|
||||
connection: PunktfunkConnection,
|
||||
onFrame: (@Sendable (AccessUnit) -> Void)? = nil,
|
||||
onSessionEnd: (@Sendable () -> Void)? = nil
|
||||
) {
|
||||
@@ -104,7 +104,7 @@ public final class StreamLayerView: NSView {
|
||||
if layer.status == .failed {
|
||||
// Decode wedged: flush and re-gate on the next in-band parameter
|
||||
// sets — resuming with a delta frame can't recover. (A
|
||||
// request-IDR channel on lumen/1 is a host-side TODO; with the
|
||||
// request-IDR channel on punktfunk/1 is a host-side TODO; with the
|
||||
// host's infinite GOP this may otherwise stay black until the
|
||||
// next recovery keyframe.)
|
||||
layer.flush()
|
||||
@@ -123,13 +123,13 @@ public final class StreamLayerView: NSView {
|
||||
}
|
||||
}
|
||||
}
|
||||
thread.name = "lumen-pump"
|
||||
thread.name = "punktfunk-pump"
|
||||
thread.qualityOfService = .userInteractive
|
||||
thread.start()
|
||||
}
|
||||
|
||||
/// Stop pumping (≤ one poll timeout). Does not close the connection — that stays with
|
||||
/// whoever owns it (LumenConnection.close() is safe alongside a draining pump).
|
||||
/// whoever owns it (PunktfunkConnection.close() is safe alongside a draining pump).
|
||||
public func stop() {
|
||||
token?.cancel()
|
||||
token = nil
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
// VideoToolboxRoundTripTests covers the real-bitstream path).
|
||||
|
||||
import XCTest
|
||||
@testable import LumenKit
|
||||
@testable import PunktfunkKit
|
||||
|
||||
final class AnnexBTests: XCTestCase {
|
||||
/// NAL with the given HEVC type in bits 1..6 of the first header byte.
|
||||
+8
-8
@@ -1,20 +1,20 @@
|
||||
// Integration: the Swift wrapper against a real lumen/1 host over QUIC + UDP on loopback —
|
||||
// the Swift twin of lumen-host's m3.rs::c_abi_connection_roundtrip, this time through the
|
||||
// Integration: the Swift wrapper against a real punktfunk/1 host over QUIC + UDP on loopback —
|
||||
// the Swift twin of punktfunk-host's m3.rs::c_abi_connection_roundtrip, this time through the
|
||||
// statically linked xcframework. Driven by clients/apple/test-loopback.sh, which builds and
|
||||
// starts `lumen-host m3-host --source synthetic` and sets LUMEN_LOOPBACK_PORT.
|
||||
// starts `punktfunk-host m3-host --source synthetic` and sets PUNKTFUNK_LOOPBACK_PORT.
|
||||
|
||||
import XCTest
|
||||
@testable import LumenKit
|
||||
@testable import PunktfunkKit
|
||||
|
||||
final class LoopbackIntegrationTests: XCTestCase {
|
||||
func testSyntheticStreamRoundTrip() throws {
|
||||
guard let portStr = ProcessInfo.processInfo.environment["LUMEN_LOOPBACK_PORT"],
|
||||
guard let portStr = ProcessInfo.processInfo.environment["PUNKTFUNK_LOOPBACK_PORT"],
|
||||
let port = UInt16(portStr)
|
||||
else {
|
||||
throw XCTSkip("needs a running m3-host — use clients/apple/test-loopback.sh")
|
||||
}
|
||||
|
||||
let conn = try LumenConnection(
|
||||
let conn = try PunktfunkConnection(
|
||||
host: "127.0.0.1", port: port, width: 1280, height: 720, refreshHz: 60)
|
||||
XCTAssertEqual(conn.width, 1280)
|
||||
XCTAssertEqual(conn.height, 720)
|
||||
@@ -49,7 +49,7 @@ final class LoopbackIntegrationTests: XCTestCase {
|
||||
|
||||
conn.close()
|
||||
XCTAssertThrowsError(try conn.nextAU(timeoutMs: 10)) { error in
|
||||
guard case LumenClientError.closed = error else {
|
||||
guard case PunktfunkClientError.closed = error else {
|
||||
return XCTFail("expected .closed, got \(error)")
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ final class LoopbackIntegrationTests: XCTestCase {
|
||||
func testConnectFailureThrows() {
|
||||
// Nothing listens on this port; connect must fail within its timeout, not hang.
|
||||
XCTAssertThrowsError(
|
||||
try LumenConnection(
|
||||
try PunktfunkConnection(
|
||||
host: "127.0.0.1", port: 9, width: 640, height: 480, refreshHz: 30,
|
||||
timeoutMs: 2000))
|
||||
}
|
||||
+7
-7
@@ -4,25 +4,25 @@
|
||||
// putting the layer on glass.
|
||||
//
|
||||
// Run (host side, on the Linux box):
|
||||
// LUMEN_COMPOSITOR=gamescope LUMEN_GAMESCOPE_APP=vkcube LUMEN_ZEROCOPY=1 \
|
||||
// lumen-host m3-host --source virtual --seconds 120
|
||||
// PUNKTFUNK_COMPOSITOR=gamescope PUNKTFUNK_GAMESCOPE_APP=vkcube PUNKTFUNK_ZEROCOPY=1 \
|
||||
// punktfunk-host m3-host --source virtual --seconds 120
|
||||
// Then here:
|
||||
// LUMEN_REMOTE_HOST=192.168.1.70 swift test --filter RemoteFirstLightTests
|
||||
// PUNKTFUNK_REMOTE_HOST=192.168.1.70 swift test --filter RemoteFirstLightTests
|
||||
|
||||
import CoreMedia
|
||||
import VideoToolbox
|
||||
import XCTest
|
||||
@testable import LumenKit
|
||||
@testable import PunktfunkKit
|
||||
|
||||
final class RemoteFirstLightTests: XCTestCase {
|
||||
func testRemoteStreamDecodesToPixels() throws {
|
||||
guard let host = ProcessInfo.processInfo.environment["LUMEN_REMOTE_HOST"] else {
|
||||
throw XCTSkip("set LUMEN_REMOTE_HOST (and start m3-host --source virtual there)")
|
||||
guard let host = ProcessInfo.processInfo.environment["PUNKTFUNK_REMOTE_HOST"] else {
|
||||
throw XCTSkip("set PUNKTFUNK_REMOTE_HOST (and start m3-host --source virtual there)")
|
||||
}
|
||||
let width: UInt32 = 1280
|
||||
let height: UInt32 = 720
|
||||
|
||||
let conn = try LumenConnection(
|
||||
let conn = try PunktfunkConnection(
|
||||
host: host, width: width, height: height, refreshHz: 60)
|
||||
defer { conn.close() }
|
||||
XCTAssertEqual(conn.width, width)
|
||||
+2
-2
@@ -1,13 +1,13 @@
|
||||
// Real-bitstream proof of the decode-prep path: VTCompressionSession encodes HEVC, we
|
||||
// rebuild the host's wire shape (Annex-B AU with in-band VPS/SPS/PPS — exactly what
|
||||
// lumen-host emits on every IDR), run it through AnnexB, and hand the result to a real
|
||||
// punktfunk-host emits on every IDR), run it through AnnexB, and hand the result to a real
|
||||
// VTDecompressionSession. Pixels out = the whole client decode path is sound.
|
||||
|
||||
import AVFoundation
|
||||
import CoreMedia
|
||||
import VideoToolbox
|
||||
import XCTest
|
||||
@testable import LumenKit
|
||||
@testable import PunktfunkKit
|
||||
|
||||
final class VideoToolboxRoundTripTests: XCTestCase {
|
||||
private let width = 320
|
||||
@@ -1,17 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# Loopback integration: a real lumen/1 host (synthetic source — pure protocol, runs fine on
|
||||
# Loopback integration: a real punktfunk/1 host (synthetic source — pure protocol, runs fine on
|
||||
# macOS) on 127.0.0.1, then the Swift integration tests against it through the xcframework.
|
||||
# The m3 host serves exactly one session and exits; the trap is just for failure paths.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
PORT="${LUMEN_LOOPBACK_PORT:-19778}"
|
||||
PORT="${PUNKTFUNK_LOOPBACK_PORT:-19778}"
|
||||
|
||||
cargo build --release -p lumen-host
|
||||
target/release/lumen-host m3-host --port "$PORT" --source synthetic --frames 300 &
|
||||
cargo build --release -p punktfunk-host
|
||||
target/release/punktfunk-host m3-host --port "$PORT" --source synthetic --frames 300 &
|
||||
HOST_PID=$!
|
||||
trap 'kill "$HOST_PID" 2>/dev/null || true' EXIT
|
||||
sleep 1
|
||||
|
||||
cd clients/apple
|
||||
LUMEN_LOOPBACK_PORT="$PORT" swift test --filter LoopbackIntegrationTests
|
||||
PUNKTFUNK_LOOPBACK_PORT="$PORT" swift test --filter LoopbackIntegrationTests
|
||||
|
||||
Reference in New Issue
Block a user