fix(socket): raise Engine.IO ping/pong + prefer WebSocket transport

Connection-stability layer for issue #3. LG webOS WebKit (and other
TV-grade clients) miss Engine.IO pongs under decode load with the
Socket.IO defaults of 25s ping / 20s timeout, causing spurious
transport drops and a connect/reconnect/evict/disconnect loop on
the device. Default polling-first transport adds another fragility
layer via the polling->WebSocket upgrade dance.

- pingInterval / pingTimeout default to 30000 / 30000 (worst-case
  dead-socket detection 60s, up from ~45s). Both env-configurable
  via PING_INTERVAL / PING_TIMEOUT.
- Player Socket.IO client: transports: ['websocket', 'polling'].
  Tries WebSocket first; falls back to polling on the same connect
  attempt if WebSocket fails. Polling fallback preserved for
  firewall-restricted networks.

App-level heartbeat checker is unchanged and remains the safety net
for clients that miss the transport-level ping/pong window.

Tradeoffs documented in inline comments. README env table extended
with PING_INTERVAL and PING_TIMEOUT rows.

Refs #3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ScreenTinker 2026-05-14 13:02:34 -05:00
parent c4ac81c7a6
commit 1aee4f2d5b
4 changed files with 19 additions and 1 deletions

View file

@ -115,6 +115,8 @@ Schema migrations run automatically on first boot — no manual migration comman
| `JWT_SECRET` | JWT signing key (auto-generated if not set) | _(auto)_ | | `JWT_SECRET` | JWT signing key (auto-generated if not set) | _(auto)_ |
| `SSL_CERT` | Path to SSL certificate | `server/certs/cert.pem` | | `SSL_CERT` | Path to SSL certificate | `server/certs/cert.pem` |
| `SSL_KEY` | Path to SSL private key | `server/certs/key.pem` | | `SSL_KEY` | Path to SSL private key | `server/certs/key.pem` |
| `PING_INTERVAL` | Socket.IO Engine.IO ping interval (ms). Raise for slow TV WebKits that miss pongs under decode load. | `30000` |
| `PING_TIMEOUT` | Socket.IO Engine.IO pong wait (ms). Lower = faster dead-socket detection; higher = more forgiving of laggy clients. | `30000` |
### Optional Integrations ### Optional Integrations

View file

@ -10,6 +10,12 @@ module.exports = {
frontendDir: path.join(__dirname, '..', 'frontend'), frontendDir: path.join(__dirname, '..', 'frontend'),
heartbeatInterval: 10000, // Check every 10s heartbeatInterval: 10000, // Check every 10s
heartbeatTimeout: 45000, // Offline after 45s (3 missed 15s beats) heartbeatTimeout: 45000, // Offline after 45s (3 missed 15s beats)
// Engine.IO transport-level ping/pong. Raised from Socket.IO defaults
// (25000/20000) because TV WebKits (LG webOS, older Tizen) miss pongs
// under decode load - tighter values cause spurious transport drops.
// Worst-case dead-socket detection: pingInterval + pingTimeout = 60s.
pingInterval: parseInt(process.env.PING_INTERVAL) || 30000,
pingTimeout: parseInt(process.env.PING_TIMEOUT) || 30000,
maxFileSize: 500 * 1024 * 1024, // 500MB maxFileSize: 500 * 1024 * 1024, // 500MB
thumbnailWidth: 320, thumbnailWidth: 320,
screenshotQuality: 70, screenshotQuality: 70,

View file

@ -467,6 +467,14 @@
reconnectionDelay: 2000, reconnectionDelay: 2000,
reconnectionDelayMax: 10000, reconnectionDelayMax: 10000,
timeout: 20000, timeout: 20000,
// Prefer WebSocket but allow polling fallback. Socket.IO default is
// polling-first with an upgrade dance that's fragile on TV WebKits
// (LG webOS especially). Reversing the order opens a WebSocket directly;
// if that fails (rare - blocked by firewall), it falls back to polling
// on the same connect attempt. Tradeoff: WS-blocked networks add a few
// seconds to first connect while WS times out. Worth it for the common
// case where WS is fine but the upgrade dance was hanging the device.
transports: ['websocket', 'polling'],
}); });
socket.on('connect', () => { socket.on('connect', () => {

View file

@ -43,7 +43,9 @@ const io = new Server(server, {
origin: (origin, cb) => corsOriginCheck(origin, cb), origin: (origin, cb) => corsOriginCheck(origin, cb),
credentials: true, credentials: true,
}, },
maxHttpBufferSize: 10 * 1024 * 1024 // 10MB for screenshot uploads maxHttpBufferSize: 10 * 1024 * 1024, // 10MB for screenshot uploads
pingInterval: config.pingInterval,
pingTimeout: config.pingTimeout,
}); });
// Middleware // Middleware