@mindexec/cli 0.2.40 → 0.2.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindexec/cli",
3
- "version": "0.2.40",
3
+ "version": "0.2.41",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
package/remote-hub.js CHANGED
@@ -9,6 +9,8 @@ const DEFAULT_AGENT_TASK_TIMEOUT_MS = 120000;
9
9
  const MAX_LINE_CHARS = 4 * 1024 * 1024;
10
10
  const MAX_THUMBNAIL_BASE64_CHARS = 3 * 1024 * 1024;
11
11
  const MAX_STREAM_BASE64_CHARS = 3 * 1024 * 1024;
12
+ const MAX_THUMBNAIL_BINARY_BYTES = Math.floor(MAX_THUMBNAIL_BASE64_CHARS * 3 / 4);
13
+ const MAX_STREAM_BINARY_BYTES = Math.floor(MAX_STREAM_BASE64_CHARS * 3 / 4);
12
14
  const MAX_AGENT_TASK_CHARS = 4000;
13
15
  const MAX_AGENT_TASK_RESULT_CHARS = 3000;
14
16
  const RECENT_TASK_LIMIT = 12;
@@ -1075,6 +1077,161 @@ export function createRemoteHub(options = {}) {
1075
1077
  return task;
1076
1078
  }
1077
1079
 
1080
+ function normalizeFrameByteLength(framePayload, frameData = '') {
1081
+ if (Buffer.isBuffer(framePayload)) {
1082
+ return framePayload.length;
1083
+ }
1084
+
1085
+ return Math.floor(String(frameData || '').length * 3 / 4);
1086
+ }
1087
+
1088
+ function buildFrameDataUrl(framePayload, frameData, mimeType) {
1089
+ if (Buffer.isBuffer(framePayload)) {
1090
+ return `data:${mimeType};base64,${framePayload.toString('base64')}`;
1091
+ }
1092
+
1093
+ return String(frameData || '').startsWith('data:')
1094
+ ? String(frameData || '')
1095
+ : `data:${mimeType};base64,${frameData}`;
1096
+ }
1097
+
1098
+ function applyThumbnailFrame(device, message, framePayload, transport = 'json-base64') {
1099
+ const frameData = Buffer.isBuffer(framePayload)
1100
+ ? ''
1101
+ : safeString(framePayload, MAX_THUMBNAIL_BASE64_CHARS + 1);
1102
+ const frameSeq = Number(message.frameSeq);
1103
+ const byteLength = normalizeFrameByteLength(framePayload, frameData);
1104
+ if ((!Buffer.isBuffer(framePayload) && !frameData)
1105
+ || byteLength > MAX_THUMBNAIL_BINARY_BYTES
1106
+ || !Number.isFinite(frameSeq)) {
1107
+ device.counters.thumbnailFramesDropped += 1;
1108
+ emitRemoteEvent('RemoteFrameDropped', device, {
1109
+ reason: 'invalid-thumbnail-frame',
1110
+ frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
1111
+ transport
1112
+ });
1113
+ return false;
1114
+ }
1115
+
1116
+ const mimeType = safeString(message.mimeType || message.format || 'image/jpeg', 80) || 'image/jpeg';
1117
+ const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
1118
+ device.latestThumbnail = {
1119
+ streamId: safeString(message.streamId, 128) || 'thumbnail',
1120
+ frameSeq,
1121
+ commandId: safeString(message.commandId, 128),
1122
+ width: Number.isFinite(Number(message.width)) ? Number(message.width) : 0,
1123
+ height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
1124
+ mimeType,
1125
+ format: mimeType,
1126
+ capturedAt,
1127
+ receivedAt: device.lastSeenAt,
1128
+ byteLength,
1129
+ transport,
1130
+ dataUrl: buildFrameDataUrl(framePayload, frameData, mimeType)
1131
+ };
1132
+ device.counters.thumbnailFramesReceived += 1;
1133
+ emitRemoteEvent('RemoteFrameReceived', device, {
1134
+ streamId: device.latestThumbnail.streamId,
1135
+ frameSeq,
1136
+ width: device.latestThumbnail.width,
1137
+ height: device.latestThumbnail.height,
1138
+ transport
1139
+ });
1140
+ return true;
1141
+ }
1142
+
1143
+ function applyLiveFrame(device, message, framePayload, transport = 'json-base64') {
1144
+ const frameData = Buffer.isBuffer(framePayload)
1145
+ ? ''
1146
+ : safeString(framePayload, MAX_STREAM_BASE64_CHARS + 1);
1147
+ const frameSeq = Number(message.frameSeq);
1148
+ const streamId = safeString(message.streamId, 128) || 'live';
1149
+ if (!device.activeLiveStream?.active || device.activeLiveStream.streamId !== streamId) {
1150
+ device.counters.liveFramesDropped += 1;
1151
+ emitRemoteEvent('RemoteFrameDropped', device, {
1152
+ reason: 'stale-live-stream-frame',
1153
+ streamId,
1154
+ frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
1155
+ transport
1156
+ });
1157
+ return false;
1158
+ }
1159
+
1160
+ const byteLength = normalizeFrameByteLength(framePayload, frameData);
1161
+ if ((!Buffer.isBuffer(framePayload) && !frameData)
1162
+ || byteLength > MAX_STREAM_BINARY_BYTES
1163
+ || !Number.isFinite(frameSeq)) {
1164
+ device.counters.liveFramesDropped += 1;
1165
+ emitRemoteEvent('RemoteFrameDropped', device, {
1166
+ reason: 'invalid-live-frame',
1167
+ streamId,
1168
+ frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
1169
+ transport
1170
+ });
1171
+ return false;
1172
+ }
1173
+
1174
+ const mimeType = safeString(message.mimeType || message.format || 'image/jpeg', 80) || 'image/jpeg';
1175
+ const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
1176
+ device.latestLiveFrame = {
1177
+ streamId,
1178
+ frameSeq,
1179
+ commandId: safeString(message.commandId, 128),
1180
+ width: Number.isFinite(Number(message.width)) ? Number(message.width) : 0,
1181
+ height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
1182
+ mimeType,
1183
+ format: mimeType,
1184
+ mode: safeString(message.mode || device.activeLiveStream.mode || 'remote-fast', 80),
1185
+ fps: Number.isFinite(Number(message.fps)) ? Number(message.fps) : device.activeLiveStream.fps,
1186
+ capturedAt,
1187
+ receivedAt: device.lastSeenAt,
1188
+ byteLength,
1189
+ transport,
1190
+ dataUrl: buildFrameDataUrl(framePayload, frameData, mimeType)
1191
+ };
1192
+ device.activeLiveStream.lastFrameAt = device.lastSeenAt;
1193
+ device.activeLiveStream.lastFrameSeq = frameSeq;
1194
+ device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
1195
+ device.counters.liveFramesReceived += 1;
1196
+ emitRemoteEvent('RemoteFrameReceived', device, {
1197
+ streamId,
1198
+ frameSeq,
1199
+ width: device.latestLiveFrame.width,
1200
+ height: device.latestLiveFrame.height,
1201
+ mode: device.latestLiveFrame.mode,
1202
+ transport
1203
+ });
1204
+ return true;
1205
+ }
1206
+
1207
+ function handleAgentBinaryFrame(socket, state, header, framePayload) {
1208
+ if (!state.authenticated || !state.device) {
1209
+ writeJsonLine(socket, { type: 'error', error: 'hello-required' });
1210
+ socket.destroy();
1211
+ return;
1212
+ }
1213
+
1214
+ const device = state.device;
1215
+ device.counters.messagesReceived += 1;
1216
+ device.lastSeenAt = new Date().toISOString();
1217
+
1218
+ const frameKind = safeString(header.frameKind || header.kind || header.frameType || '', 40).toLowerCase();
1219
+ if (frameKind === 'thumbnail' || header.type === 'thumbnail.binary') {
1220
+ applyThumbnailFrame(device, header, framePayload, 'binary');
1221
+ return;
1222
+ }
1223
+
1224
+ if (frameKind === 'stream' || frameKind === 'live' || header.type === 'stream.binary') {
1225
+ applyLiveFrame(device, header, framePayload, 'binary');
1226
+ return;
1227
+ }
1228
+
1229
+ emitRemoteEvent('RemoteAgentMessageIgnored', device, {
1230
+ messageType: safeString(header.type, 80),
1231
+ frameKind
1232
+ });
1233
+ }
1234
+
1078
1235
  function handleAgentMessage(socket, state, message) {
1079
1236
  if (!message || typeof message !== 'object') {
1080
1237
  return;
@@ -1139,97 +1296,11 @@ export function createRemoteHub(options = {}) {
1139
1296
  });
1140
1297
  break;
1141
1298
  case 'thumbnail.frame': {
1142
- const frameData = safeString(message.data, MAX_THUMBNAIL_BASE64_CHARS + 1);
1143
- const frameSeq = Number(message.frameSeq);
1144
- if (!frameData || frameData.length > MAX_THUMBNAIL_BASE64_CHARS || !Number.isFinite(frameSeq)) {
1145
- device.counters.thumbnailFramesDropped += 1;
1146
- emitRemoteEvent('RemoteFrameDropped', device, {
1147
- reason: 'invalid-thumbnail-frame',
1148
- frameSeq: Number.isFinite(frameSeq) ? frameSeq : null
1149
- });
1150
- break;
1151
- }
1152
-
1153
- const mimeType = safeString(message.mimeType || message.format || 'image/jpeg', 80) || 'image/jpeg';
1154
- const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
1155
- device.latestThumbnail = {
1156
- streamId: safeString(message.streamId, 128) || 'thumbnail',
1157
- frameSeq,
1158
- commandId: safeString(message.commandId, 128),
1159
- width: Number.isFinite(Number(message.width)) ? Number(message.width) : 0,
1160
- height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
1161
- mimeType,
1162
- format: mimeType,
1163
- capturedAt,
1164
- receivedAt: device.lastSeenAt,
1165
- byteLength: Math.floor(frameData.length * 3 / 4),
1166
- dataUrl: frameData.startsWith('data:')
1167
- ? frameData
1168
- : `data:${mimeType};base64,${frameData}`
1169
- };
1170
- device.counters.thumbnailFramesReceived += 1;
1171
- emitRemoteEvent('RemoteFrameReceived', device, {
1172
- streamId: device.latestThumbnail.streamId,
1173
- frameSeq,
1174
- width: device.latestThumbnail.width,
1175
- height: device.latestThumbnail.height
1176
- });
1299
+ applyThumbnailFrame(device, message, message.data, 'json-base64');
1177
1300
  break;
1178
1301
  }
1179
1302
  case 'stream.frame': {
1180
- const frameData = safeString(message.data, MAX_STREAM_BASE64_CHARS + 1);
1181
- const frameSeq = Number(message.frameSeq);
1182
- const streamId = safeString(message.streamId, 128) || 'live';
1183
- if (!device.activeLiveStream?.active || device.activeLiveStream.streamId !== streamId) {
1184
- device.counters.liveFramesDropped += 1;
1185
- emitRemoteEvent('RemoteFrameDropped', device, {
1186
- reason: 'stale-live-stream-frame',
1187
- streamId,
1188
- frameSeq: Number.isFinite(frameSeq) ? frameSeq : null
1189
- });
1190
- break;
1191
- }
1192
-
1193
- if (!frameData || frameData.length > MAX_STREAM_BASE64_CHARS || !Number.isFinite(frameSeq)) {
1194
- device.counters.liveFramesDropped += 1;
1195
- emitRemoteEvent('RemoteFrameDropped', device, {
1196
- reason: 'invalid-live-frame',
1197
- streamId,
1198
- frameSeq: Number.isFinite(frameSeq) ? frameSeq : null
1199
- });
1200
- break;
1201
- }
1202
-
1203
- const mimeType = safeString(message.mimeType || message.format || 'image/jpeg', 80) || 'image/jpeg';
1204
- const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
1205
- device.latestLiveFrame = {
1206
- streamId,
1207
- frameSeq,
1208
- commandId: safeString(message.commandId, 128),
1209
- width: Number.isFinite(Number(message.width)) ? Number(message.width) : 0,
1210
- height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
1211
- mimeType,
1212
- format: mimeType,
1213
- mode: safeString(message.mode || device.activeLiveStream.mode || 'remote-fast', 80),
1214
- fps: Number.isFinite(Number(message.fps)) ? Number(message.fps) : device.activeLiveStream.fps,
1215
- capturedAt,
1216
- receivedAt: device.lastSeenAt,
1217
- byteLength: Math.floor(frameData.length * 3 / 4),
1218
- dataUrl: frameData.startsWith('data:')
1219
- ? frameData
1220
- : `data:${mimeType};base64,${frameData}`
1221
- };
1222
- device.activeLiveStream.lastFrameAt = device.lastSeenAt;
1223
- device.activeLiveStream.lastFrameSeq = frameSeq;
1224
- device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
1225
- device.counters.liveFramesReceived += 1;
1226
- emitRemoteEvent('RemoteFrameReceived', device, {
1227
- streamId,
1228
- frameSeq,
1229
- width: device.latestLiveFrame.width,
1230
- height: device.latestLiveFrame.height,
1231
- mode: device.latestLiveFrame.mode
1232
- });
1303
+ applyLiveFrame(device, message, message.data, 'json-base64');
1233
1304
  break;
1234
1305
  }
1235
1306
  default:
@@ -1242,14 +1313,14 @@ export function createRemoteHub(options = {}) {
1242
1313
 
1243
1314
  function handleSocket(socket) {
1244
1315
  allSockets.add(socket);
1245
- socket.setEncoding('utf8');
1246
1316
  socket.setNoDelay(true);
1247
1317
  socket.setKeepAlive(true, heartbeatMs);
1248
1318
 
1249
1319
  const state = {
1250
1320
  authenticated: false,
1251
1321
  device: null,
1252
- buffer: ''
1322
+ buffer: Buffer.alloc(0),
1323
+ pendingBinaryFrame: null
1253
1324
  };
1254
1325
 
1255
1326
  const helloTimer = setTimeout(() => {
@@ -1260,27 +1331,63 @@ export function createRemoteHub(options = {}) {
1260
1331
  }, 10000);
1261
1332
 
1262
1333
  socket.on('data', chunk => {
1263
- state.buffer += chunk;
1264
- if (state.buffer.length > MAX_LINE_CHARS) {
1265
- writeJsonLine(socket, { type: 'error', error: 'message-too-large' });
1266
- socket.destroy();
1267
- return;
1268
- }
1334
+ const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1335
+ state.buffer = state.buffer.length > 0
1336
+ ? Buffer.concat([state.buffer, incoming])
1337
+ : incoming;
1338
+
1339
+ while (!socket.destroyed) {
1340
+ if (state.pendingBinaryFrame) {
1341
+ const { header, byteLength } = state.pendingBinaryFrame;
1342
+ if (state.buffer.length < byteLength) {
1343
+ return;
1344
+ }
1345
+
1346
+ const framePayload = state.buffer.subarray(0, byteLength);
1347
+ state.buffer = state.buffer.subarray(byteLength);
1348
+ state.pendingBinaryFrame = null;
1349
+ handleAgentBinaryFrame(socket, state, header, framePayload);
1350
+ continue;
1351
+ }
1352
+
1353
+ const newlineIndex = state.buffer.indexOf(0x0a);
1354
+ if (newlineIndex < 0) {
1355
+ if (state.buffer.length > MAX_LINE_CHARS) {
1356
+ writeJsonLine(socket, { type: 'error', error: 'message-too-large' });
1357
+ socket.destroy();
1358
+ }
1359
+ return;
1360
+ }
1269
1361
 
1270
- let newlineIndex = state.buffer.indexOf('\n');
1271
- while (newlineIndex >= 0) {
1272
- const line = state.buffer.slice(0, newlineIndex);
1273
- state.buffer = state.buffer.slice(newlineIndex + 1);
1362
+ const lineBuffer = state.buffer.subarray(0, newlineIndex);
1363
+ state.buffer = state.buffer.subarray(newlineIndex + 1);
1274
1364
 
1275
1365
  try {
1276
- const message = parseJsonLine(line);
1366
+ const message = parseJsonLine(lineBuffer.toString('utf8'));
1367
+ if (message?.type === 'frame.binary') {
1368
+ const frameKind = safeString(message.frameKind || message.kind || message.frameType, 40).toLowerCase();
1369
+ const maxBytes = frameKind === 'thumbnail'
1370
+ ? MAX_THUMBNAIL_BINARY_BYTES
1371
+ : MAX_STREAM_BINARY_BYTES;
1372
+ const byteLength = Number(message.byteLength ?? message.payloadBytes ?? message.dataLength);
1373
+ if (!Number.isFinite(byteLength) || byteLength < 1 || byteLength > maxBytes) {
1374
+ writeJsonLine(socket, { type: 'error', error: 'invalid-binary-frame' });
1375
+ logWarn('remote', 'invalid binary frame header from agent.');
1376
+ continue;
1377
+ }
1378
+
1379
+ state.pendingBinaryFrame = {
1380
+ header: message,
1381
+ byteLength: Math.floor(byteLength)
1382
+ };
1383
+ continue;
1384
+ }
1385
+
1277
1386
  handleAgentMessage(socket, state, message);
1278
1387
  } catch (err) {
1279
1388
  writeJsonLine(socket, { type: 'error', error: 'invalid-json' });
1280
1389
  logWarn('remote', `invalid agent message: ${err?.message || err}`);
1281
1390
  }
1282
-
1283
- newlineIndex = state.buffer.indexOf('\n');
1284
1391
  }
1285
1392
  });
1286
1393
 
@@ -5,10 +5,13 @@ import net from 'node:net';
5
5
  import { spawn } from 'node:child_process';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
+ import { readFileSync } from 'node:fs';
8
9
 
9
10
  const BRIDGE_TOKEN = 'remote-http-smoke-token';
10
11
  const PAIR_TOKEN = 'remote-http-pair-token';
11
12
  const SYNTHETIC_COUNT = Number(process.env.REMOTE_HTTP_SMOKE_COUNT || 500);
13
+ const LOCAL_BRIDGE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
14
+ const LOCAL_BRIDGE_PACKAGE = JSON.parse(readFileSync(path.join(LOCAL_BRIDGE_DIR, 'package.json'), 'utf8'));
12
15
 
13
16
  function wait(ms) {
14
17
  return new Promise(resolve => setTimeout(resolve, ms));
@@ -155,7 +158,7 @@ async function runSyntheticEnabledSmoke() {
155
158
  assert.equal(status.started, true);
156
159
  assert.equal(status.port, remoteHubPort);
157
160
  assert.equal(status.managerPackage, '@mindexec/cli');
158
- assert.equal(status.managerVersion, '0.2.25');
161
+ assert.equal(status.managerVersion, LOCAL_BRIDGE_PACKAGE.version);
159
162
  assert.equal(status.agentPackage, '@mindexec/remote');
160
163
  assert.equal(status.canvasPagination, 'none');
161
164
  assert.equal(status.canvasDeviceListMode, 'all-devices');
@@ -8,6 +8,16 @@ function writeJsonLine(socket, payload) {
8
8
  socket.write(`${JSON.stringify(payload)}\n`);
9
9
  }
10
10
 
11
+ function writeBinaryFrame(socket, header, payload) {
12
+ const framePayload = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
13
+ socket.write(`${JSON.stringify({
14
+ ...header,
15
+ type: 'frame.binary',
16
+ byteLength: framePayload.length
17
+ })}\n`);
18
+ socket.write(framePayload);
19
+ }
20
+
11
21
  function wait(ms) {
12
22
  return new Promise(resolve => setTimeout(resolve, ms));
13
23
  }
@@ -34,6 +44,7 @@ const hub = createRemoteHub({
34
44
  REMOTE_HUB_TASK_TIMEOUT_MS: '120'
35
45
  }
36
46
  });
47
+ const smokePngFrame = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAADElEQVR42mP8z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC', 'base64');
37
48
 
38
49
  try {
39
50
  await hub.start();
@@ -115,6 +126,33 @@ try {
115
126
  assert.equal(thumbnailDevice.latestThumbnail.streamId, 'smoke-thumb');
116
127
  assert.equal(thumbnailDevice.counters.thumbnailFramesReceived, 1);
117
128
 
129
+ const binaryThumbnailCommand = hub.requestThumbnail('smoke-device', {
130
+ streamId: 'smoke-thumb-binary',
131
+ maxWidth: 320,
132
+ maxHeight: 180,
133
+ quality: 50
134
+ });
135
+ assert.equal(binaryThumbnailCommand.ok, true);
136
+ writeBinaryFrame(socket, {
137
+ frameKind: 'thumbnail',
138
+ commandId: binaryThumbnailCommand.commandId,
139
+ streamId: 'smoke-thumb-binary',
140
+ frameSeq: 4,
141
+ width: 2,
142
+ height: 1,
143
+ mimeType: 'image/png',
144
+ capturedAt: new Date().toISOString()
145
+ }, smokePngFrame);
146
+
147
+ const binaryThumbnailDevice = await waitFor(() => {
148
+ const current = hub.listDevices();
149
+ return current[0]?.latestThumbnail?.frameSeq === 4 ? current[0] : null;
150
+ });
151
+ assert.equal(binaryThumbnailDevice.latestThumbnail.streamId, 'smoke-thumb-binary');
152
+ assert.equal(binaryThumbnailDevice.latestThumbnail.transport, 'binary');
153
+ assert.equal(binaryThumbnailDevice.latestThumbnail.byteLength, smokePngFrame.length);
154
+ assert.equal(binaryThumbnailDevice.counters.thumbnailFramesReceived, 2);
155
+
118
156
  const liveCommand = hub.startLiveStream('smoke-device', {
119
157
  streamId: 'smoke-live',
120
158
  fps: 5,
@@ -145,6 +183,28 @@ try {
145
183
  assert.equal(liveDevice.latestLiveFrame.mode, 'remote-fast');
146
184
  assert.equal(liveDevice.counters.liveFramesReceived, 1);
147
185
 
186
+ writeBinaryFrame(socket, {
187
+ frameKind: 'stream',
188
+ commandId: liveCommand.commandId,
189
+ streamId: 'smoke-live',
190
+ frameSeq: 5,
191
+ width: 2,
192
+ height: 1,
193
+ mimeType: 'image/png',
194
+ mode: 'remote-fast',
195
+ fps: 5,
196
+ capturedAt: new Date().toISOString()
197
+ }, smokePngFrame);
198
+
199
+ const binaryLiveDevice = await waitFor(() => {
200
+ const current = hub.listDevices();
201
+ return current[0]?.latestLiveFrame?.frameSeq === 5 ? current[0] : null;
202
+ });
203
+ assert.equal(binaryLiveDevice.latestLiveFrame.streamId, 'smoke-live');
204
+ assert.equal(binaryLiveDevice.latestLiveFrame.transport, 'binary');
205
+ assert.equal(binaryLiveDevice.latestLiveFrame.byteLength, smokePngFrame.length);
206
+ assert.equal(binaryLiveDevice.counters.liveFramesReceived, 2);
207
+
148
208
  const staleFrameBefore = liveDevice.counters.liveFramesDropped;
149
209
  writeJsonLine(socket, {
150
210
  type: 'stream.frame',