Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ jobs:
cd TargetBridge-Sender
xcodebuild test -project TargetBridge.xcodeproj -scheme TBDisplaySender -destination 'platform=macOS' -derivedDataPath .build/DerivedData

- name: Check Integrated Video Queue Guards
run: ruby TargetBridge-Sender/scripts/test_video_queue_budget.rb

- name: Check Sender Permission Probe
run: ruby TargetBridge-Sender/scripts/test_permission_status.rb sender build/TargetBridge.app/Contents/MacOS/TargetBridge

build-receiver:
name: Build Receiver (${{ matrix.os }} - ${{ matrix.arch }})
strategy:
Expand Down Expand Up @@ -71,3 +77,6 @@ jobs:
run: |
cd TargetBridge-Receiver
./scripts/build_tbreceiver_c_app.sh

- name: Check Receiver Permission Probe
run: ruby TargetBridge-Sender/scripts/test_permission_status.rb receiver 'build/TargetBridge Receiver.app/Contents/MacOS/TargetBridgeReceiver'
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,18 @@ and improves the project. A special thank-you for this release goes to:
- [@preggocl](https://github.com/preggocl) for the physical Receiver display profile and the software decode fallback.
- [@Betafer](https://github.com/Betafer) for input-queue safety, cursor work, idle audio/display handling, watchdog protection, monitor shielding, and build improvements.

## TargetBridge 3.5.1
## TargetBridge 3.5.2

TargetBridge 3.5.1 is a maintenance release focused on safer input control,
accurate Receiver display profiles, idle resource handling, and improved compatibility with older Macs:
TargetBridge 3.5.2 is a focused maintenance update to the Input Dockstation.
When the Receiver is the input master, Shift and Command now remain active for
multi-file selection even when the keyboard is connected to the Receiver and
the mouse or trackpad is used locally on the Sender. This fixes Finder range
selection and non-contiguous selection without changing ordinary local input.

It also includes the video queue, capture-health and permission-status fixes
prepared for this maintenance line. For the wider 3.5.1 maintenance work,
including Receiver display profiles, idle resource handling, and compatibility
with older Macs:

- preserve keyboard and mouse release events when the Receiver input queue is under pressure
- preserve native macOS cursor shapes by default, including the screenshot crosshair; the custom overlay is reserved for the explicit large-cursor option
Expand All @@ -46,7 +54,8 @@ accurate Receiver display profiles, idle resource handling, and improved compati
- keep local notification banners behind the active monitor surface across display/fullscreen transitions
- package SDL runtimes safely and support explicit build/version overrides

See the [full 3.5.1 release notes](docs/releases/3.5.1.md) and
See the [full 3.5.2 release notes](docs/releases/3.5.2.md), the
[3.5.1 release notes](docs/releases/3.5.1.md), and the
[testing guide](docs/Testing.md), including the new monitor-shield lifecycle test.

It also keeps the established multi-Mac workspace features:
Expand Down
2 changes: 1 addition & 1 deletion TargetBridge-Receiver/TBReceiverC/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

CC ?= clang
PKG ?= pkgconf
APP_VERSION ?= 3.5.1
APP_VERSION ?= 3.5.2
APP_BUILD ?= dev

MACOSX_DEPLOYMENT_TARGET ?= 11.0
Expand Down
2 changes: 1 addition & 1 deletion TargetBridge-Receiver/TBReceiverC/src/display.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
#include <string.h>

#ifndef TB_RECEIVER_VERSION
#define TB_RECEIVER_VERSION "3.2.0"
#define TB_RECEIVER_VERSION "3.5.2"
#endif

#ifndef TB_RECEIVER_BUILD
Expand Down
8 changes: 8 additions & 0 deletions TargetBridge-Receiver/TBReceiverC/src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -1914,6 +1914,14 @@ static void build_display_host(char *buf, size_t bufsz, const char *ip_fallback,
int main(int argc, char **argv) {
int fullscreen = 1;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--permission-status") == 0) {
printf(
"TB_PERMISSION_STATUS:screen_recording=0:accessibility=%d:input_monitoring=%d\n",
tb_receiver_accessibility_trusted(),
tb_receiver_input_monitoring_trusted()
);
return 0;
}
if (strcmp(argv[i], "--windowed") == 0) fullscreen = 0;
}

Expand Down
2 changes: 1 addition & 1 deletion TargetBridge-Receiver/scripts/build_tbreceiver_c_app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ BUILD_DIR="${REPO_ROOT}/build"
APP_DIR="${BUILD_DIR}/TargetBridge Receiver.app"
BIN_NAME="TargetBridgeReceiver"
APP_NAME="TargetBridge Receiver"
APP_VERSION="${APP_VERSION:-3.5.1}"
APP_VERSION="${APP_VERSION:-3.5.2}"
STAMP="${APP_BUILD:-$(date +%Y%m%d%H%M%S)}"
ARCH="$(uname -m)"
ICONSET_DIR="$(mktemp -d)"
Expand Down
83 changes: 83 additions & 0 deletions TargetBridge-Sender/TBDisplaySender/TBCaptureHealth.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import Foundation
import CoreGraphics
import CoreMedia
import ScreenCaptureKit

/// Capture health is not FPS: event-driven capture may remain idle indefinitely.
/// Owned by the pipeline under its snapshot lock. Times use monotonic uptime.
struct TBCaptureHealth {
enum State: String {
case starting, active, idle, blank, suspended, stopped, unknown

init(_ status: SCFrameStatus) {
switch status {
case .complete, .started: self = .active
case .idle: self = .idle
case .blank: self = .blank
case .suspended: self = .suspended
case .stopped: self = .stopped
@unknown default: self = .unknown
}
}

init(_ status: CGDisplayStreamFrameStatus) {
switch status {
case .frameComplete: self = .active
case .frameIdle: self = .idle
case .frameBlank: self = .blank
case .stopped: self = .stopped
@unknown default: self = .unknown
}
}

init(sampleBuffer: CMSampleBuffer) {
guard let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: false)
as? [[SCStreamFrameInfo: Any]],
let raw = attachments.first?[SCStreamFrameInfo.status] as? Int,
let status = SCFrameStatus(rawValue: raw) else {
self = .unknown
return
}
self.init(status)
}

var canContainFrame: Bool { self == .active || self == .unknown }
var isQuiescent: Bool { self == .idle || self == .blank || self == .suspended }
}

private(set) var state: State = .starting
private(set) var frameCount: UInt64 = 0
private(set) var idleCount: UInt64 = 0
private(set) var lastFrameAt: TimeInterval?
private var progressAt: TimeInterval

init(now: TimeInterval) { progressAt = now }

mutating func observe(_ next: State, now: TimeInterval) {
// A resumed stream gets a fresh grace period, but repeated callbacks
// without usable frames must not hide a genuine capture failure.
if state.isQuiescent && !next.isQuiescent && next != .stopped {
progressAt = now
}
state = next
if next == .idle { idleCount &+= 1 }
}

mutating func recordFrame(now: TimeInterval) {
state = .active
lastFrameAt = now
progressAt = now
frameCount &+= 1
}

func shouldRestart(now: TimeInterval, timeout: TimeInterval = 8) -> Bool {
if state == .stopped { return true }
// Blank/suspended are deliberate states, not failures; don't wake a
// sleeping display in a restart loop. System-wake recovery is separate.
if state == .blank || state == .suspended { return false }
// An idle event is a state transition, NOT a periodic heartbeat promise.
// Require a real first frame so initial idle cannot mask a failed start.
if state == .idle && lastFrameAt != nil { return false }
return now - progressAt >= timeout
}
}
22 changes: 20 additions & 2 deletions TargetBridge-Sender/TBDisplaySender/TBDisplaySenderApp.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
import AppKit
import ApplicationServices
import Darwin
import SwiftUI

@main
struct TBDisplaySenderApp: App {
@StateObject private var service = TBDisplaySenderService.shared
private let statusItemController = TBDisplaySenderStatusItemController(service: TBDisplaySenderService.shared)
@StateObject private var service: TBDisplaySenderService
private let statusItemController: TBDisplaySenderStatusItemController

@MainActor
init() {
// This probe is intentionally handled before constructing the shared
// service. The Setup Assistant can inspect the exact installed binary
// without opening a window, starting capture or attaching a display.
if CommandLine.arguments.contains("--permission-status") {
let screenRecording = CGPreflightScreenCaptureAccess() ? 1 : 0
let accessibility = AXIsProcessTrusted() ? 1 : 0
let inputMonitoring = CGPreflightListenEventAccess() ? 1 : 0
print("TB_PERMISSION_STATUS:screen_recording=\(screenRecording):accessibility=\(accessibility):input_monitoring=\(inputMonitoring)")
fflush(stdout)
Darwin.exit(EXIT_SUCCESS)
}

let sharedService = TBDisplaySenderService.shared
_service = StateObject(wrappedValue: sharedService)
statusItemController = TBDisplaySenderStatusItemController(service: sharedService)

// LaunchAgent automation must not depend on SwiftUI restoring or showing
// the main window on a headless Mac.
TBSenderAutomation.handleLaunchArguments(CommandLine.arguments)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
enum TBDisplaySenderBuildInfo {
static let marketingVersion = "3.5.1"
static let marketingVersion = "3.5.2"
static let buildNumber = "20260810141154"
static let versionDisplay = "\(marketingVersion) + build \(buildNumber)"
}
Loading