feat(apple/cursor): send the mid-stream cursor-render flip — ABI v12
PunktfunkConnection.setCursorRender(clientDraws:) over the new punktfunk_connection_set_cursor_render export, driven by one edge-detected reconciler in StreamView (clientDraws = captured && desktopMouse) called from every transition — the ⌃⌥⇧M chord, engage/release, and session start. Capture model and released now get the host-composited pointer back in the video (full fidelity); the desktop model keeps the local NSCursor path. xcframework rebuilt v12. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -462,6 +462,20 @@ public final class PunktfunkConnection {
|
|||||||
throw PunktfunkClientError.status(rc)
|
throw PunktfunkClientError.status(rc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tell the host who renders the pointer (the §8 mid-stream mouse-model flip, ABI v12):
|
||||||
|
/// `clientDraws = true` — this client draws it locally (the desktop mouse model; the host
|
||||||
|
/// excludes the pointer from the video and forwards shape/state); `false` — the host
|
||||||
|
/// composites it into the video (the capture model, full fidelity). Idempotent,
|
||||||
|
/// latest-wins; harmless against hosts without the cursor cap. Fire-and-forget — errors
|
||||||
|
/// are swallowed (a closed session is the only failure and it moots the flip).
|
||||||
|
public func setCursorRender(clientDraws: Bool) {
|
||||||
|
cursorLock.lock()
|
||||||
|
defer { cursorLock.unlock() }
|
||||||
|
guard let h = liveHandle() else { return }
|
||||||
|
_ = punktfunk_connection_set_cursor_render(h, clientDraws)
|
||||||
|
}
|
||||||
|
|
||||||
/// The resolved codec as a `VideoCodec` (H.264 / HEVC / AV1) — drives the bitstream framing
|
/// The resolved codec as a `VideoCodec` (H.264 / HEVC / AV1) — drives the bitstream framing
|
||||||
/// (Annex-B NAL parsing vs the AV1 OBU repack).
|
/// (Annex-B NAL parsing vs the AV1 OBU repack).
|
||||||
public var videoCodec: VideoCodec { VideoCodec(wire: resolvedCodec) }
|
public var videoCodec: VideoCodec { VideoCodec(wire: resolvedCodec) }
|
||||||
|
|||||||
@@ -225,6 +225,10 @@ public final class StreamLayerView: NSView {
|
|||||||
private var cursorChannelActive = false
|
private var cursorChannelActive = false
|
||||||
private var hostCursors: [UInt32: NSCursor] = [:]
|
private var hostCursors: [UInt32: NSCursor] = [:]
|
||||||
private var cursorState: PunktfunkConnection.CursorStateEvent?
|
private var cursorState: PunktfunkConnection.CursorStateEvent?
|
||||||
|
/// Last `CursorRenderMode.clientDraws` told to the host (the §8 mid-stream render flip);
|
||||||
|
/// nil = nothing sent yet. Edge-detected by [`reconcileCursorRender`] from the live mouse
|
||||||
|
/// model, so the chord, engage/release, and session start all reconcile through one path.
|
||||||
|
private var sentClientDraws: Bool?
|
||||||
/// M3 hint tracking: edge-triggered so a manual ⌃⌥⇧M isn't fought — the override latch
|
/// M3 hint tracking: edge-triggered so a manual ⌃⌥⇧M isn't fought — the override latch
|
||||||
/// holds until the HOST's intent next changes.
|
/// holds until the HOST's intent next changes.
|
||||||
private var lastHint: Bool?
|
private var lastHint: Bool?
|
||||||
@@ -466,6 +470,7 @@ public final class StreamLayerView: NSView {
|
|||||||
window?.makeFirstResponder(self)
|
window?.makeFirstResponder(self)
|
||||||
window?.invalidateCursorRects(for: self) // desktop model: hide-over-view engages
|
window?.invalidateCursorRects(for: self) // desktop model: hide-over-view engages
|
||||||
notifyCaptureChange(true)
|
notifyCaptureChange(true)
|
||||||
|
reconcileCursorRender()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func releaseCapture() {
|
private func releaseCapture() {
|
||||||
@@ -476,6 +481,7 @@ public final class StreamLayerView: NSView {
|
|||||||
captured = false
|
captured = false
|
||||||
window?.invalidateCursorRects(for: self)
|
window?.invalidateCursorRects(for: self)
|
||||||
notifyCaptureChange(false)
|
notifyCaptureChange(false)
|
||||||
|
reconcileCursorRender() // released ⇒ the host composites the pointer again
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A fully transparent cursor for the desktop mouse model's hide-over-view rect —
|
/// A fully transparent cursor for the desktop mouse model's hide-over-view rect —
|
||||||
@@ -504,6 +510,19 @@ public final class StreamLayerView: NSView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tell the host who renders the pointer (the §8 mid-stream render flip): we draw it only
|
||||||
|
/// while the DESKTOP model is engaged (the local OS cursor wears the host shape); under
|
||||||
|
/// the capture model — and while released — the host composites it into the video (full
|
||||||
|
/// fidelity, the pre-channel look). One edge-detected reconciler, called from every
|
||||||
|
/// transition (chord, engage/release, session start).
|
||||||
|
private func reconcileCursorRender() {
|
||||||
|
guard cursorChannelActive, let connection else { return }
|
||||||
|
let clientDraws = captured && desktopMouse
|
||||||
|
guard sentClientDraws != clientDraws else { return }
|
||||||
|
sentClientDraws = clientDraws
|
||||||
|
connection.setCursorRender(clientDraws: clientDraws)
|
||||||
|
}
|
||||||
|
|
||||||
/// Flip the mouse model with the atomic release/re-engage swap; `reappearAt` (host video
|
/// Flip the mouse model with the atomic release/re-engage swap; `reappearAt` (host video
|
||||||
/// px — the M3 hand-back position) warps the local pointer so leaving relative lands the
|
/// px — the M3 hand-back position) warps the local pointer so leaving relative lands the
|
||||||
/// cursor exactly where the host last had it.
|
/// cursor exactly where the host last had it.
|
||||||
@@ -517,6 +536,7 @@ public final class StreamLayerView: NSView {
|
|||||||
if on, let p = reappearAt, let sp = cgScreenPoint(forHostX: p.x, p.y) {
|
if on, let p = reappearAt, let sp = cgScreenPoint(forHostX: p.x, p.y) {
|
||||||
CGWarpMouseCursorPosition(sp)
|
CGWarpMouseCursorPosition(sp)
|
||||||
}
|
}
|
||||||
|
reconcileCursorRender()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The single cursor pull thread (both planes share the connection's cursor lock):
|
/// The single cursor pull thread (both planes share the connection's cursor lock):
|
||||||
@@ -827,6 +847,7 @@ public final class StreamLayerView: NSView {
|
|||||||
cursorChannelActive = true
|
cursorChannelActive = true
|
||||||
streamInputLog.info("cursor channel negotiated — host cursor renders locally")
|
streamInputLog.info("cursor channel negotiated — host cursor renders locally")
|
||||||
startCursorPump(connection)
|
startCursorPump(connection)
|
||||||
|
reconcileCursorRender() // initial render mode (a capture-model start composites)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Presenter choice + lifecycle live in SessionPresenter (shared with iOS/tvOS): stage-2
|
// Presenter choice + lifecycle live in SessionPresenter (shared with iOS/tvOS): stage-2
|
||||||
@@ -923,6 +944,7 @@ public final class StreamLayerView: NSView {
|
|||||||
cursorChannelActive = false
|
cursorChannelActive = false
|
||||||
cursorState = nil
|
cursorState = nil
|
||||||
hostCursors.removeAll()
|
hostCursors.removeAll()
|
||||||
|
sentClientDraws = nil
|
||||||
window?.invalidateCursorRects(for: self)
|
window?.invalidateCursorRects(for: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user