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
1 change: 1 addition & 0 deletions src/ScreenCapturer.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)startCapture;
- (void)stopCapture;
- (void)restartCapture;
- (void)logDiagnosticState;

// Health status properties (read-only)
@property (nonatomic, readonly) NSTimeInterval lastFrameTime;
Expand Down
101 changes: 95 additions & 6 deletions src/ScreenCapturer.m
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ - (void)startCapture {
// set max frame rate to 60 FPS
config.minimumFrameInterval = CMTimeMake(1, 60);
config.pixelFormat = kCVPixelFormatType_32BGRA;

NSLog(@"[ScreenCapturer] Stream configuration: minFrameInterval=%lld/%d (%.1f fps), pixelFormat=%u",
config.minimumFrameInterval.value, config.minimumFrameInterval.timescale,
(double)config.minimumFrameInterval.timescale / (double)config.minimumFrameInterval.value,
(unsigned int)config.pixelFormat);

SCContentFilter *filter = nil;
{
Expand All @@ -91,11 +96,17 @@ - (void)startCapture {
return;
}

NSLog(@"[ScreenCapturer] Found target window: %.0fx%.0f",
selectedWindow.frame.size.width, selectedWindow.frame.size.height);
NSLog(@"[ScreenCapturer] Found target window: %.0fx%.0f at (%.0f, %.0f), onScreen=%d, layer=%ld",
selectedWindow.frame.size.width, selectedWindow.frame.size.height,
selectedWindow.frame.origin.x, selectedWindow.frame.origin.y,
selectedWindow.isOnScreen ? 1 : 0,
(long)selectedWindow.windowLayer);

config.width = selectedWindow.frame.size.width;
config.height = selectedWindow.frame.size.height;

NSLog(@"[ScreenCapturer] Configured capture resolution: %.0fx%.0f",
config.width, config.height);
if ([SCContentFilter instancesRespondToSelector:@selector(initWithDesktopIndependentWindow:)]) {
filter = [[SCContentFilter alloc] initWithDesktopIndependentWindow:selectedWindow];
NSLog(@"[ScreenCapturer] Using desktop-independent window filter");
Expand Down Expand Up @@ -237,6 +248,31 @@ - (void)restartCapture {
});
}

- (void)logDiagnosticState {
NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
NSTimeInterval timeSinceLastFrame = now - self.lastFrameTime;

NSLog(@"[ScreenCapturer] ==================== DIAGNOSTIC STATE ====================");
NSLog(@"[ScreenCapturer] Health Status:");
NSLog(@"[ScreenCapturer] isHealthy: %d", self.isHealthy);
NSLog(@"[ScreenCapturer] isStopping: %d", self.isStopping);
NSLog(@"[ScreenCapturer] isRestarting: %d", self.isRestarting);
NSLog(@"[ScreenCapturer] Frame Statistics:");
NSLog(@"[ScreenCapturer] Total frames: %llu", self.frameCount);
NSLog(@"[ScreenCapturer] Time since last frame: %.2f seconds", timeSinceLastFrame);
NSLog(@"[ScreenCapturer] Last frame timestamp: %.3f", self.lastFrameTime);
NSLog(@"[ScreenCapturer] Restart Statistics:");
NSLog(@"[ScreenCapturer] Total restarts: %u", self.restartCount);
NSLog(@"[ScreenCapturer] Consecutive failures: %u", self.consecutiveRestartFailures);
NSLog(@"[ScreenCapturer] Stream State:");
NSLog(@"[ScreenCapturer] Stream object: %@", self.stream ? @"EXISTS" : @"NULL");
NSLog(@"[ScreenCapturer] Watchdog timer: %@", self.watchdogTimer ? @"ACTIVE" : @"INACTIVE");
NSLog(@"[ScreenCapturer] Metrics timer: %@", self.metricsTimer ? @"ACTIVE" : @"INACTIVE");
NSLog(@"[ScreenCapturer] Window Info:");
NSLog(@"[ScreenCapturer] Target window ID: %u", self.windowID);
NSLog(@"[ScreenCapturer] ==========================================================");
}

#pragma mark - Watchdog Timer

- (void)startWatchdogTimer {
Expand Down Expand Up @@ -325,19 +361,71 @@ - (void)logMetrics {
#pragma mark - SCStreamDelegate methods

- (void) stream:(SCStream *) stream didStopWithError:(NSError *) error {
NSLog(@"[ScreenCapturer] DELEGATE: stream didStopWithError called (error code: %ld)",
(long)(error ? error.code : 0));
NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
NSTimeInterval timeSinceLastFrame = now - self.lastFrameTime;

NSLog(@"[ScreenCapturer] ==================== STREAM STOPPED ====================");
NSLog(@"[ScreenCapturer] DELEGATE: stream didStopWithError called");
NSLog(@"[ScreenCapturer] Stream state at stop: healthy=%d, frames=%llu, restarts=%u",
self.isHealthy, self.frameCount, self.restartCount);
NSLog(@"[ScreenCapturer] Time since last frame: %.2f seconds", timeSinceLastFrame);
NSLog(@"[ScreenCapturer] Intentional stop: isStopping=%d, isRestarting=%d",
self.isStopping, self.isRestarting);

// Log full diagnostic state
[self logDiagnosticState];

// Check if window is still available
[SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *content, NSError *contentError) {
if (contentError) {
NSLog(@"[ScreenCapturer] ERROR: Failed to check window availability: %@", contentError);
} else {
BOOL windowFound = NO;
SCWindow *targetWindow = nil;
for (SCWindow *w in content.windows) {
if (w.windowID == self.windowID) {
windowFound = YES;
targetWindow = w;
break;
}
}

if (windowFound) {
NSLog(@"[ScreenCapturer] Window %u STILL EXISTS: %.0fx%.0f, onScreen=%d, layer=%ld",
self.windowID,
targetWindow.frame.size.width, targetWindow.frame.size.height,
targetWindow.isOnScreen ? 1 : 0,
(long)targetWindow.windowLayer);
} else {
NSLog(@"[ScreenCapturer] Window %u NOT FOUND in shareable content (likely closed/hidden)",
self.windowID);
}
NSLog(@"[ScreenCapturer] Total available windows: %lu", (unsigned long)content.windows.count);
}
}];

if (error && error.code != 0) {
NSLog(@"[ScreenCapturer] ERROR: Stream stopped with error: %@", error);
NSLog(@"[ScreenCapturer] ERROR DETAILS:");
NSLog(@"[ScreenCapturer] Domain: %@", error.domain);
NSLog(@"[ScreenCapturer] Code: %ld", (long)error.code);
NSLog(@"[ScreenCapturer] Description: %@", error.localizedDescription);
NSLog(@"[ScreenCapturer] Reason: %@", error.localizedFailureReason ?: @"(none)");
NSLog(@"[ScreenCapturer] Recovery: %@", error.localizedRecoverySuggestion ?: @"(none)");

if (error.userInfo) {
NSLog(@"[ScreenCapturer] UserInfo: %@", error.userInfo);
}

self.isHealthy = NO;
self.errorHandler(error);
} else {
NSLog(@"[ScreenCapturer] Stream stopped without error (may be intentional or silent failure)");
NSLog(@"[ScreenCapturer] Stream stopped without error (error is nil or code 0)");

// If we weren't intentionally stopping, this is a problem
if (!self.isStopping && !self.isRestarting) {
NSLog(@"[ScreenCapturer] WARNING: Stream stopped unexpectedly without error!");
NSLog(@"[ScreenCapturer] This indicates a silent ScreenCaptureKit failure");
NSLog(@"[ScreenCapturer] Possible causes: window closed, window minimized, screen lock, display sleep, permissions revoked");
self.isHealthy = NO;
self.consecutiveRestartFailures++;

Expand All @@ -347,6 +435,7 @@ - (void) stream:(SCStream *) stream didStopWithError:(NSError *) error {
});
}
}
NSLog(@"[ScreenCapturer] ========================================================");
}


Expand Down
43 changes: 43 additions & 0 deletions src/mac.m
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,26 @@ ScreenCaptureKit does not have something like CGDisplayStreamUpdateGetRects(),

void clientGone(rfbClientPtr cl)
{
// Count remaining clients
int remainingClients = 0;
rfbClientIteratorPtr iterator = rfbGetClientIterator(rfbScreen);
rfbClientPtr otherClient;
while ((otherClient = rfbClientIteratorNext(iterator))) {
if (otherClient != cl) {
remainingClients++;
}
}
rfbReleaseClientIterator(iterator);

fprintf(stderr, "[macVNC] ==================== CLIENT DISCONNECTED ====================\n");
fprintf(stderr, "[macVNC] Client %s disconnected (remaining clients: %d)\n", cl->host, remainingClients);

// Log ScreenCapturer state when client disconnects
if (screenCapturer) {
[screenCapturer logDiagnosticState];
}

fprintf(stderr, "[macVNC] ===============================================================\n");
rfbLog("Client %s disconnected. Server continues running.\n", cl->host);
}

Expand All @@ -991,6 +1011,29 @@ enum rfbNewClientAction newClient(rfbClientPtr cl)
cl->clientGoneHook = clientGone;
cl->viewOnly = viewOnly;

// Count total clients (including this new one)
int totalClients = 1;
rfbClientIteratorPtr iterator = rfbGetClientIterator(rfbScreen);
rfbClientPtr otherClient;
while ((otherClient = rfbClientIteratorNext(iterator))) {
if (otherClient != cl) {
totalClients++;
}
}
rfbReleaseClientIterator(iterator);

fprintf(stderr, "[macVNC] ==================== NEW CLIENT CONNECTION ====================\n");
fprintf(stderr, "[macVNC] Client %s connected (total clients: %d, viewOnly: %d)\n",
cl->host, totalClients, viewOnly);
fprintf(stderr, "[macVNC] Client protocol version: %d.%d\n",
cl->protocolMajorVersion, cl->protocolMinorVersion);

// Log ScreenCapturer state when client connects
if (screenCapturer) {
[screenCapturer logDiagnosticState];
}

fprintf(stderr, "[macVNC] ==================================================================\n");
rfbLog("Client %s connected\n", cl->host);
return(RFB_CLIENT_ACCEPT);
}
Expand Down
107 changes: 107 additions & 0 deletions test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>macVNC WebSocket Viewer</title>
<style>
html, body { height: 100%; margin: 0; background: #111; color: #ddd; font: 14px/1.4 -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif; }
#toolbar { position: fixed; top: 0; left: 0; right: 0; height: 44px; background: #222; display: flex; align-items: center; gap: 8px; padding: 0 10px; z-index: 2; }
#status { margin-left: auto; opacity: 0.8; }
#screen { position: absolute; top: 44px; left: 0; right: 0; bottom: 0; display: grid; place-items: center; }
#screen .rfb-container { box-shadow: 0 10px 30px rgba(0,0,0,0.5); background: #000; }
button, input { background: #333; border: 1px solid #444; color: #eee; padding: 6px 10px; border-radius: 6px; }
label { opacity: 0.85; }
</style>
</head>
<body>
<div id="toolbar">
<label>ws://</label>
<input id="host" placeholder="host" size="16" />
<label>:</label>
<input id="port" placeholder="port" size="6" />
<button id="connectBtn">Connect</button>
<button id="disconnectBtn" disabled>Disconnect</button>
<label style="margin-left:12px"><input id="scale" type="checkbox" checked /> Scale</label>
<label><input id="clip" type="checkbox" /> Clip</label>
<span id="status">idle</span>
</div>
<div id="screen"></div>

<script type="module">
import RFB from 'https://cdn.jsdelivr.net/npm/@novnc/novnc/core/rfb.js';

const qs = new URLSearchParams(location.search);
const hostEl = document.getElementById('host');
const portEl = document.getElementById('port');
const connectBtn = document.getElementById('connectBtn');
const disconnectBtn = document.getElementById('disconnectBtn');
const scaleEl = document.getElementById('scale');
const clipEl = document.getElementById('clip');
const statusEl = document.getElementById('status');
const container = document.getElementById('screen');

// Defaults: same host as page (fallback to localhost for file://), WebSocket port 5900 unless ?wsport= overrides
hostEl.value = qs.get('wshost') || location.hostname || 'localhost';
portEl.value = qs.get('wsport') || '6081';

let rfb = null;

function setStatus(text) { statusEl.textContent = text; }

function connect() {
if (rfb) return;
const host = (hostEl.value.trim() || 'localhost');
const port = (portEl.value.trim() || '5900');
const wsProto = location.protocol === 'https:' ? 'wss' : 'ws';
const url = `${wsProto}://${host}:${port}`;
setStatus('connecting…');

rfb = new RFB(container, url, { wsProtocols: ['binary'] });
rfb.viewOnly = false;
rfb.scaleViewport = scaleEl.checked;
rfb.resizeSession = false;
rfb.clipViewport = clipEl.checked;
rfb.background = '#000000';
rfb.focusOnClick = true;

rfb.addEventListener('connect', () => {
setStatus('connected');
connectBtn.disabled = true;
disconnectBtn.disabled = false;
});
rfb.addEventListener('disconnect', (e) => {
setStatus('disconnected');
connectBtn.disabled = false;
disconnectBtn.disabled = true;
rfb = null;
});
rfb.addEventListener('credentialsrequired', () => {
// rfb.sendCredentials({ username, password });
});
rfb.addEventListener('securityfailure', (e) => {
console.error('securityfailure', e);
setStatus('security failure');
});
}

function disconnect() {
if (!rfb) return;
try { rfb.disconnect(); } catch {}
rfb = null;
connectBtn.disabled = false;
disconnectBtn.disabled = true;
setStatus('idle');
}

connectBtn.addEventListener('click', connect);
disconnectBtn.addEventListener('click', disconnect);
scaleEl.addEventListener('change', () => { if (rfb) rfb.scaleViewport = scaleEl.checked; });
clipEl.addEventListener('change', () => { if (rfb) rfb.clipViewport = clipEl.checked; });

if (qs.get('autoconnect') !== '0') connect();
</script>
</body>
</html>