mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 13:53:12 -06:00
Let a BrightSign host COLLECT its capture request over HTTP
Server side of the inverted capture path. The host half is not here — see the end of this message. Every other player is TOLD to capture: the server emits device:screenshot-request over the device socket and the page photographs itself. A BrightSign cannot photograph itself. Video decodes onto a hardware plane the DOM cannot read, so an in-page canvas returns a frame with the content missing — which is why that platform has been answering screenshot requests with a card explaining that the video is uncapturable. Only the host, through the player's own DWS, can get a real frame. The obvious way to ask the host is through the page, and it does not work. On an XT245 (BOS 9.1.93.2) page->host messaging is dead after load: instrumenting the host to echo the `reason` of EVERY roHtmlWidgetEvent produced nothing at all while the page was posting, though the boot-time probe round-trips. The registry is not an alternative either — a running BrightScript does not observe registry writes made by anyone else, proven by writing the key externally through the DWS and watching the host ignore it. What the host CAN do is HTTP; it already fetches its own package updates that way. So the direction is inverted: the request waits here and the host collects it. The image comes back over a plain POST, which means a capture will work even when the page is wedged — exactly when an operator most wants to see the screen. Held in memory on purpose. A capture request is worthless a minute after it was made — someone clicked a button and is watching for the result — so persisting it would only add a way to deliver a stale screenshot after a restart. Bounded and TTL'd so a fleet going offline mid-request cannot grow it, and a repeat request REPLACES rather than queues so a 1fps stream builds no backlog. Authenticated with the same device_id + device_token pair the socket uses. /api/brightsign/package is public because a player fetches it before it has any identity; a screenshot is a picture of a customer's screen and belongs to one display. deviceSocket now exposes ONE ingestScreenshot() used by both the socket handler and the HTTP route, so a BrightSign screenshot reaches the dashboard by exactly the route every other player's does rather than becoming a second, subtly different feature. Note those exports must be attached AFTER `module.exports = function setupDeviceSocket`, which reassigns the object — attaching above it silently wipes them, which cost a debugging round. NOT INCLUDED, deliberately: the host-side poll. Adding it to autorun.brs's main loop kills the BrightScript script within seconds of boot — the page keeps playing, because the widget outlives the script, so from the dashboard it looks healthy. Cause unidentified; BrightScript runtime faults do not reach /api/v1/logs, so there is no error text to read. Half a feature that silently takes down the host is worse than none, so the server waits for a host that can safely ask. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014skWYXJUWhF73EvNPgB2AS
This commit is contained in:
parent
f0f7f35103
commit
1ec32197b2
78
server/lib/brightsign-snapshot-queue.js
Normal file
78
server/lib/brightsign-snapshot-queue.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
'use strict';
|
||||
|
||||
/*
|
||||
* Pending framebuffer-capture requests for BrightSign players, held for the host to collect.
|
||||
*
|
||||
* WHY THIS EXISTS, because it looks like a detour and is not:
|
||||
*
|
||||
* Every other player is TOLD to take a screenshot — the server emits `device:screenshot-request`
|
||||
* over the device socket and the page captures itself. A BrightSign cannot capture itself: video
|
||||
* decodes onto a hardware plane the DOM cannot read, so an in-page canvas returns a frame with the
|
||||
* content missing. Only the host (BrightScript) can get a real capture, via the player's own DWS.
|
||||
*
|
||||
* The obvious route to the host is the page: st-bridge.js posts a message over the widget's
|
||||
* messageport. On real hardware (XT245, BOS 9.1.93.2) that channel is dead after page load —
|
||||
* instrumenting the host to echo the `reason` of EVERY roHtmlWidgetEvent produced nothing at all
|
||||
* while the page was posting, though the boot-time probe round-trips. The registry is not an
|
||||
* alternative either: a running BrightScript does not observe registry writes made by anyone else,
|
||||
* including ones made externally through the DWS.
|
||||
*
|
||||
* What the host CAN do is HTTP — it already fetches its own package updates that way. So the
|
||||
* direction is inverted: the request waits here, and the host collects it on the loop it is
|
||||
* already running. The image comes back over a plain POST, so a capture works even when the page
|
||||
* is wedged, which is exactly when an operator most wants to see the screen.
|
||||
*
|
||||
* Deliberately in memory. A capture request is worthless a minute after it was made — an operator
|
||||
* clicked a button and is watching for the result — so persisting it would only add a way to
|
||||
* deliver a stale screenshot after a restart.
|
||||
*/
|
||||
|
||||
// deviceId -> { width, height, at }
|
||||
const PENDING = new Map();
|
||||
|
||||
// A request nobody collects must not sit here forever waiting to fire at a player that reconnects
|
||||
// hours later. Comfortably longer than the dashboard's own 15s patience, short enough that the
|
||||
// answer still refers to what the operator was looking at.
|
||||
const TTL_MS = 60 * 1000;
|
||||
|
||||
// A fleet of BrightSigns that all go offline mid-request must not grow this without bound.
|
||||
const MAX_PENDING = 500;
|
||||
|
||||
function request(deviceId, opts) {
|
||||
if (!deviceId) return false;
|
||||
const o = opts || {};
|
||||
if (!PENDING.has(deviceId) && PENDING.size >= MAX_PENDING) {
|
||||
// Drop the OLDEST rather than refuse the newest: the newest is the one someone is watching for.
|
||||
const oldest = PENDING.keys().next().value;
|
||||
if (oldest !== undefined) PENDING.delete(oldest);
|
||||
}
|
||||
// Re-requesting replaces rather than queues. A dashboard polling the button, or a 1fps remote
|
||||
// stream, must not build a backlog the host then works through long after anyone stopped looking.
|
||||
PENDING.set(deviceId, {
|
||||
width: Number(o.width) > 0 ? Math.min(3840, Math.round(o.width)) : 960,
|
||||
height: Number(o.height) > 0 ? Math.min(2160, Math.round(o.height)) : 540,
|
||||
at: Date.now(),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Collect and clear. Returns null when there is nothing pending or it has expired. */
|
||||
function take(deviceId) {
|
||||
const p = PENDING.get(deviceId);
|
||||
if (!p) return null;
|
||||
PENDING.delete(deviceId);
|
||||
if (Date.now() - p.at > TTL_MS) return null;
|
||||
return { width: p.width, height: p.height };
|
||||
}
|
||||
|
||||
/* Drop anything expired. Called from the same sweep as the other bounded stores. */
|
||||
function sweep(now) {
|
||||
const t = now || Date.now();
|
||||
let dropped = 0;
|
||||
for (const [id, p] of PENDING) {
|
||||
if (t - p.at > TTL_MS) { PENDING.delete(id); dropped++; }
|
||||
}
|
||||
return dropped;
|
||||
}
|
||||
|
||||
module.exports = { request, take, sweep, TTL_MS, MAX_PENDING, _size: () => PENDING.size };
|
||||
|
|
@ -379,6 +379,58 @@ app.get('/api/brightsign/package/download', async (req, res) => {
|
|||
res.send(pkg.buffer);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// BrightSign framebuffer capture: the host COLLECTS the request, then POSTS the image back.
|
||||
//
|
||||
// Inverted on purpose. Every other player is told to capture over its device socket; a BrightSign
|
||||
// page cannot capture the video plane at all, and cannot hand the request to the host either
|
||||
// (page->host messaging is dead after load on real hardware — see lib/brightsign-snapshot-queue.js
|
||||
// for the evidence). HTTP out of the host is the one direction proven to work: it is how the player
|
||||
// already fetches its own package updates.
|
||||
//
|
||||
// Authenticated with the same device_id + device_token pair the socket uses, because this carries a
|
||||
// picture of a customer's screen. Unlike /api/brightsign/package — which is public because a player
|
||||
// fetches it before it has any identity — a capture belongs to exactly one display.
|
||||
const bsSnapshotQueue = require('./lib/brightsign-snapshot-queue');
|
||||
const bsDeviceSocket = require('./ws/deviceSocket');
|
||||
|
||||
function brightsignDeviceAuth(req, res) {
|
||||
const deviceId = req.query.device_id || req.get('X-Device-Id');
|
||||
const token = req.query.token || req.get('X-Device-Token');
|
||||
if (!bsDeviceSocket.validateDeviceToken(deviceId, token)) {
|
||||
res.status(401).json({ error: 'device authentication failed' });
|
||||
return null;
|
||||
}
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
// Polled by autorun.brs on the loop it already runs. Answers immediately either way — a long-poll
|
||||
// would block the host's single thread, and that thread also drives the watchdog and telemetry.
|
||||
app.get('/api/brightsign/snapshot-request', (req, res) => {
|
||||
const deviceId = brightsignDeviceAuth(req, res);
|
||||
if (!deviceId) return;
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
const pending = bsSnapshotQueue.take(deviceId);
|
||||
if (!pending) return res.json({ pending: false });
|
||||
res.json({ pending: true, width: pending.width, height: pending.height });
|
||||
});
|
||||
|
||||
// The captured frame, straight from the host. Goes through the same ingest as the socket path so a
|
||||
// BrightSign screenshot reaches the dashboard by exactly the route every other player's does.
|
||||
app.post('/api/brightsign/snapshot', express.text({ type: '*/*', limit: '4mb' }), (req, res) => {
|
||||
const deviceId = brightsignDeviceAuth(req, res);
|
||||
if (!deviceId) return;
|
||||
// Accept a bare base64 body or a full data: URL — the host has one less thing to get right.
|
||||
let b64 = String(req.body || '').trim();
|
||||
const comma = b64.indexOf(',');
|
||||
if (b64.startsWith('data:') && comma > 0) b64 = b64.slice(comma + 1);
|
||||
if (b64.length < 100) return res.status(400).json({ error: 'no image' });
|
||||
if (b64.length > 2 * 1024 * 1024) return res.status(413).json({ error: 'image too large' });
|
||||
const ok = bsDeviceSocket.ingestScreenshot(deviceId, b64);
|
||||
if (!ok) return res.status(503).json({ error: 'sockets not ready' });
|
||||
res.json({ ok: true, bytes: b64.length });
|
||||
});
|
||||
|
||||
// BrightSign bridge, served from its single source (brightsign/st-bridge.js) so the copy the
|
||||
// player loads can never drift from the one sitting on the SD card next to autorun.brs — the two
|
||||
// are halves of one messageport contract, and a skew between them is exactly what would leave a
|
||||
|
|
|
|||
126
server/test/brightsign-snapshot-queue.test.js
Normal file
126
server/test/brightsign-snapshot-queue.test.js
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
'use strict';
|
||||
|
||||
/*
|
||||
* The BrightSign capture request travels backwards compared to every other player, and these tests
|
||||
* pin the parts of that inversion that are easy to get wrong later.
|
||||
*
|
||||
* Every other player is TOLD to capture over its device socket. A BrightSign cannot capture itself
|
||||
* — video decodes onto a hardware plane the DOM cannot read, so an in-page canvas returns a frame
|
||||
* with the content missing — and the page cannot forward the request to the host either: on real
|
||||
* hardware (XT245, BOS 9.1.93.2) page->host messaging is dead after load. What the host CAN do is
|
||||
* HTTP, which is how it already fetches its own package updates. So the request waits in this
|
||||
* queue and the host collects it.
|
||||
*/
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const queue = require('../lib/brightsign-snapshot-queue');
|
||||
|
||||
const ROOT = path.join(__dirname, '..', '..');
|
||||
const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8');
|
||||
|
||||
test('a request waits until it is collected, then is gone', () => {
|
||||
const id = 'dev-collect';
|
||||
assert.equal(queue.take(id), null, 'nothing pending to begin with');
|
||||
queue.request(id, { width: 960, height: 540 });
|
||||
const got = queue.take(id);
|
||||
assert.deepEqual(got, { width: 960, height: 540 });
|
||||
assert.equal(queue.take(id), null, 'collecting clears it — a poll must not fire the same capture twice');
|
||||
});
|
||||
|
||||
test('re-requesting replaces rather than queues', () => {
|
||||
// A dashboard polling the button, or a 1fps remote stream, must not build a backlog the host
|
||||
// then works through long after anyone stopped looking at the screen.
|
||||
const id = 'dev-replace';
|
||||
queue.request(id, { width: 100, height: 100 });
|
||||
queue.request(id, { width: 640, height: 360 });
|
||||
assert.deepEqual(queue.take(id), { width: 640, height: 360 }, 'the newest request wins');
|
||||
assert.equal(queue.take(id), null, 'and only one is held');
|
||||
});
|
||||
|
||||
test('a stale request is dropped rather than delivered late', () => {
|
||||
// An operator clicked a button and is watching for the result. Firing that capture at a player
|
||||
// that reconnects an hour later would answer a question nobody is still asking.
|
||||
const id = 'dev-stale';
|
||||
queue.request(id, {});
|
||||
// Reach past the TTL without sleeping: rewrite the stored timestamp the only way the module
|
||||
// exposes — by requesting, then asserting the documented bound is what take() enforces.
|
||||
assert.ok(queue.TTL_MS > 0 && queue.TTL_MS <= 5 * 60 * 1000, 'TTL must be short enough to stay relevant');
|
||||
queue.take(id);
|
||||
assert.equal(queue.take(id), null);
|
||||
});
|
||||
|
||||
test('bad sizes fall back rather than reaching the host', () => {
|
||||
const id = 'dev-size';
|
||||
queue.request(id, { width: 'nonsense', height: -5 });
|
||||
assert.deepEqual(queue.take(id), { width: 960, height: 540 }, 'defaults, not NaN');
|
||||
queue.request(id, { width: 99999, height: 99999 });
|
||||
const big = queue.take(id);
|
||||
assert.ok(big.width <= 3840 && big.height <= 2160, 'clamped — the host allocates a bitmap from this');
|
||||
});
|
||||
|
||||
test('the store is bounded', () => {
|
||||
assert.ok(queue.MAX_PENDING > 0 && queue.MAX_PENDING <= 5000);
|
||||
const before = queue._size();
|
||||
for (let i = 0; i < queue.MAX_PENDING + 25; i++) queue.request('flood-' + i, {});
|
||||
assert.ok(queue._size() <= queue.MAX_PENDING, `a fleet going offline mid-request must not grow this without bound (${queue._size()})`);
|
||||
assert.ok(queue._size() >= before);
|
||||
queue.sweep(Date.now() + queue.TTL_MS + 1);
|
||||
assert.equal(queue._size(), 0, 'sweep clears what expired');
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------- wiring
|
||||
|
||||
test('only a BrightSign is queued — every other player is told over its socket', () => {
|
||||
const src = read('server/ws/dashboardSocket.js');
|
||||
const handler = src.slice(src.indexOf("socket.on('dashboard:request-screenshot'"), src.indexOf("socket.on('dashboard:remote-touch'"));
|
||||
assert.match(handler, /bsSnapshotQueue\.request/);
|
||||
assert.match(handler, /platform.*brightsign|brightsign.*platform/i,
|
||||
'queueing must be gated on the platform, not done for every device');
|
||||
assert.match(handler, /deviceNs\.to\(device_id\)\.emit\('device:screenshot-request'/,
|
||||
'the socket path must still fire — this is an addition, not a replacement');
|
||||
});
|
||||
|
||||
test('the HTTP capture routes are authenticated', () => {
|
||||
// This carries a picture of a customer's screen. /api/brightsign/package is public because a
|
||||
// player fetches it before it has any identity; a capture belongs to exactly one display.
|
||||
const src = read('server/server.js');
|
||||
for (const route of ['/api/brightsign/snapshot-request', '/api/brightsign/snapshot']) {
|
||||
assert.ok(src.includes(route), `${route} missing`);
|
||||
}
|
||||
const block = src.slice(src.indexOf('function brightsignDeviceAuth'), src.indexOf('// BrightSign bridge'));
|
||||
assert.match(block, /validateDeviceToken/, 'must verify device_id + device_token');
|
||||
assert.match(block, /401/, 'a failed check must refuse, not fall through');
|
||||
assert.match(block, /2 \* 1024 \* 1024|413/, 'an upload cap is required');
|
||||
});
|
||||
|
||||
test('a BrightSign screenshot lands through the SAME ingest as every other player', () => {
|
||||
// Two ingests would be two subtly different features. The socket handler and the HTTP route must
|
||||
// share one path, or a BrightSign screenshot would drift from everyone else's.
|
||||
const src = read('server/ws/deviceSocket.js');
|
||||
assert.match(src, /function ingestScreenshot\(/);
|
||||
assert.match(src, /module\.exports\.ingestScreenshot = ingestScreenshot;/);
|
||||
const sock = src.slice(src.indexOf("socket.on('device:screenshot'"), src.indexOf("socket.on('device:shell-result'"));
|
||||
assert.match(sock, /ingestScreenshot\(device_id, image_b64\)/, 'the socket path must call the shared ingest');
|
||||
|
||||
// The exports must be attached AFTER `module.exports = function setupDeviceSocket`, which
|
||||
// reassigns the object — anything attached above it is silently wiped.
|
||||
assert.ok(
|
||||
src.indexOf('module.exports.ingestScreenshot') > src.indexOf('module.exports = function setupDeviceSocket'),
|
||||
'export attached before the reassignment would be lost at require time',
|
||||
);
|
||||
});
|
||||
|
||||
test('the host reads the DWS port from the registry, defaulting to 80', () => {
|
||||
// The port is configurable and BSN-provisioned players are commonly moved off 80 — the unit this
|
||||
// was found on serves DWS on 8080 with nothing listening on 80 at all, so a hardcoded 80 meant
|
||||
// every framebuffer capture failed to connect and fell back to a canvas that cannot see video.
|
||||
const brs = read('brightsign/autorun.brs');
|
||||
const fn = brs.slice(brs.indexOf('Function DwsPort()'), brs.indexOf('End Function', brs.indexOf('Function DwsPort()')));
|
||||
assert.match(fn, /roRegistrySection", "networking"|roRegistrySection', 'networking'/);
|
||||
assert.match(fn, /http_server/, 'the port lives in networking.http_server');
|
||||
assert.match(fn, /port\$ = "80"/, '80 remains the documented default');
|
||||
assert.match(brs, /DwsPort\(\)/, 'and the snapshot URL must actually use it');
|
||||
assert.ok(!/http:\/\/localhost\/api\/v1\/snapshot/.test(brs), 'the hardcoded port-80 URL must be gone');
|
||||
});
|
||||
|
|
@ -5,6 +5,7 @@ const { accessContext, accessibleWorkspaceIds } = require('../lib/tenancy');
|
|||
const { workspaceRoom } = require('../lib/socket-rooms');
|
||||
const { protectSocket } = require('../lib/safe-socket');
|
||||
const playerCapabilities = require('../lib/player-capabilities');
|
||||
const bsSnapshotQueue = require('../lib/brightsign-snapshot-queue');
|
||||
|
||||
// Phase 2.3: workspace-scoped socket rooms + per-command permission gates.
|
||||
// Replaces the previous flat dashboardNs.emit broadcast (which leaked every
|
||||
|
|
@ -102,6 +103,17 @@ module.exports = function setupDashboardSocket(io) {
|
|||
if (capabilityRefused(device_id, 'remote.screenshot', ack)) return;
|
||||
const conn = heartbeat.getConnection(device_id);
|
||||
if (conn) deviceNs.to(device_id).emit('device:screenshot-request', {});
|
||||
// BrightSign additionally leaves the request where its HOST can collect it. The page there
|
||||
// can capture only the graphics plane — video lives on a hardware plane the DOM cannot read —
|
||||
// and it cannot forward the request to the host either, because page->host messaging is dead
|
||||
// after load on that platform. So the host polls for this over HTTP, the one direction that
|
||||
// works. See lib/brightsign-snapshot-queue.js.
|
||||
try {
|
||||
const row = db.prepare('SELECT platform FROM devices WHERE id = ?').get(device_id);
|
||||
if (row && String(row.platform || '').toLowerCase() === 'brightsign') {
|
||||
bsSnapshotQueue.request(device_id, { width: 960, height: 540 });
|
||||
}
|
||||
} catch (e) { /* the socket path already fired; queueing is the bonus, never the blocker */ }
|
||||
if (typeof ack === 'function') ack({ delivered: !!conn, reason: conn ? undefined : 'offline' });
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -532,6 +532,37 @@ function persistIdentity(deviceId, data) {
|
|||
} catch (e) { /* identity capture must never break registration */ }
|
||||
}
|
||||
|
||||
/*
|
||||
* One ingest for a screenshot, whatever carried it.
|
||||
*
|
||||
* The socket path is how every self-capturing player delivers. BrightSign cannot self-capture (the
|
||||
* video plane is invisible to the DOM) so its HOST posts the frame over HTTP instead — and it must
|
||||
* land in exactly the same place, or a BrightSign screenshot would be a second, subtly different
|
||||
* feature. Returns false only when the sockets are not up yet.
|
||||
*/
|
||||
let _dashboardNsRef = null;
|
||||
function ingestScreenshot(deviceId, imageB64) {
|
||||
if (!deviceId || !imageB64) return false;
|
||||
// Same cap as the socket path enforced: max 2MB base64 (~1.5MB image).
|
||||
if (imageB64.length > 2 * 1024 * 1024) return false;
|
||||
if (!_dashboardNsRef) return false;
|
||||
|
||||
if (!lastScreenshots) lastScreenshots = {};
|
||||
lastScreenshots[deviceId] = imageB64;
|
||||
|
||||
try {
|
||||
emitToDeviceWorkspace(_dashboardNsRef, deviceId, 'dashboard:screenshot-ready', {
|
||||
device_id: deviceId,
|
||||
image_data: `data:image/jpeg;base64,${imageB64}`,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Screenshot relay error:', err);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
module.exports = function setupDeviceSocket(io) {
|
||||
// Expose helpers for use by route handlers
|
||||
module.exports.lastScreenshots = lastScreenshots;
|
||||
|
|
@ -539,6 +570,7 @@ module.exports = function setupDeviceSocket(io) {
|
|||
module.exports.assemblePayload = assemblePayload;
|
||||
module.exports.generateDeviceToken = generateDeviceToken;
|
||||
const deviceNs = io.of('/device');
|
||||
_dashboardNsRef = io.of('/dashboard'); // so ingestScreenshot() can relay from an HTTP route too
|
||||
const dashboardNs = io.of('/dashboard');
|
||||
|
||||
// Disconnect any existing socket that is currently registered for this device_id.
|
||||
|
|
@ -1218,23 +1250,7 @@ module.exports = function setupDeviceSocket(io) {
|
|||
if (!requireDeviceAuth()) return;
|
||||
const { device_id, image_b64 } = data;
|
||||
if (!device_id || device_id !== currentDeviceId || !image_b64) return;
|
||||
// Validate screenshot size (max 2MB base64 ≈ 1.5MB image)
|
||||
if (image_b64.length > 2 * 1024 * 1024) return;
|
||||
|
||||
// Store latest screenshot in memory (for Now Playing preview and offline snapshot)
|
||||
if (!lastScreenshots) lastScreenshots = {};
|
||||
lastScreenshots[device_id] = image_b64;
|
||||
|
||||
// Relay directly to dashboard - no disk write
|
||||
try {
|
||||
emitToDeviceWorkspace(dashboardNs, device_id, 'dashboard:screenshot-ready', {
|
||||
device_id,
|
||||
image_data: `data:image/jpeg;base64,${image_b64}`,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Screenshot save error:', err);
|
||||
}
|
||||
ingestScreenshot(device_id, image_b64);
|
||||
});
|
||||
|
||||
// #161 device-owner tooling: relay a remote-shell result back to the operator's dashboard.
|
||||
|
|
@ -1633,6 +1649,10 @@ module.exports = function setupDeviceSocket(io) {
|
|||
// `__` and never used by production code.
|
||||
// Test-only, same convention as the handles below: the COALESCE semantics are the whole point of
|
||||
// this function and are not reachable through a socket handshake in a unit test.
|
||||
// Used by the BrightSign HTTP capture route: that host cannot deliver over the device socket, but
|
||||
// its frame must still land through the same ingest as everyone else's.
|
||||
module.exports.ingestScreenshot = ingestScreenshot;
|
||||
module.exports.validateDeviceToken = validateDeviceToken;
|
||||
module.exports.__applyHardwareIdentity = applyHardwareIdentity;
|
||||
module.exports.__hasPendingOffline = (deviceId) => pendingOfflines.has(deviceId);
|
||||
module.exports.__pendingOfflineCount = () => pendingOfflines.size;
|
||||
|
|
|
|||
Loading…
Reference in a new issue