apintergrationpost 4.0.5 → 4.0.6

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": "apintergrationpost",
3
- "version": "4.0.5",
3
+ "version": "4.0.6",
4
4
  "description": "Remote integration client for authorized lab and enterprise post-deployment workflows",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -10,23 +10,24 @@ const MAX_FRAME_BYTES = 768 * 1024;
10
10
  const FRAME_WATCHDOG_MS = 10000;
11
11
 
12
12
  function resolveFfmpegPath() {
13
- const probe = spawnSync('sh', ['-c', 'ffmpeg -hide_banner -devices 2>&1 | grep x11grab || true'], {
14
- encoding: 'utf8',
15
- });
16
- if (probe.stdout && probe.stdout.includes('x11grab')) {
17
- return 'ffmpeg';
18
- }
13
+ if (!hasCommand('ffmpeg')) return null;
19
14
 
20
- try {
21
- const bundled = require('ffmpeg-static');
22
- if (bundled && typeof bundled === 'string') return bundled;
23
- } catch {
24
- // ignore
25
- }
15
+ const devices = spawnSync('sh', ['-c', 'ffmpeg -hide_banner -devices 2>&1'], { encoding: 'utf8' });
16
+ if (!devices.stdout || !devices.stdout.includes('x11grab')) return null;
17
+
18
+ const encoders = spawnSync('sh', ['-c', 'ffmpeg -hide_banner -encoders 2>&1'], { encoding: 'utf8' });
19
+ if (!encoders.stdout || !encoders.stdout.includes('mjpeg')) return null;
26
20
 
27
21
  return 'ffmpeg';
28
22
  }
29
23
 
24
+ function detectImageMime(buffer) {
25
+ if (!buffer || buffer.length < 4) return 'application/octet-stream';
26
+ if (buffer[0] === 0xff && buffer[1] === 0xd8) return 'image/jpeg';
27
+ if (buffer[0] === 0x89 && buffer[1] === 0x50) return 'image/png';
28
+ return 'application/octet-stream';
29
+ }
30
+
30
31
  function buildScaleFilter(maxWidth, maxHeight) {
31
32
  return `scale='min(${maxWidth},iw)':'min(${maxHeight},ih)':force_original_aspect_ratio=decrease`;
32
33
  }
@@ -88,6 +89,7 @@ class ScreenStream extends EventEmitter {
88
89
  this._frameWatchdog = null;
89
90
  this._grimTimer = null;
90
91
  this._grimBusy = false;
92
+ this._grimFailures = 0;
91
93
  }
92
94
 
93
95
  getStatus() {
@@ -119,12 +121,13 @@ class ScreenStream extends EventEmitter {
119
121
  this._frameWatchdog = setTimeout(() => {
120
122
  if (!this._gotFrame) {
121
123
  const ctx = this.captureCtx || {};
122
- const hint = ctx.sessionType === 'wayland' && !ctx.grimAvailable
123
- ? ' Ubuntu Wayland detected install grim: apt install grim'
124
+ const hint = ctx.sessionType === 'wayland'
125
+ ? ' Ensure you are logged into the desktop GUI (Wayland session).'
124
126
  : ' Log into the desktop GUI and run: xhost +local: (as desktop user)';
127
+ const detail = this._stderr ? this._stderr.trim().slice(0, 240) : '';
125
128
  this.emit('error', new Error(
126
- `No screen frames received (${this.backend}, DISPLAY=${ctx.display || '?'}, user=${ctx.runAsUser || ctx.user || '?'}).${hint}`
127
- + (this._stderr ? ` ffmpeg: ${this._stderr.trim().slice(0, 180)}` : '')
129
+ `No screen frames received (${this.backend}, user=${ctx.runAsUser || ctx.user || '?'}).${hint}`
130
+ + (detail ? ` Capture error: ${detail}` : '')
128
131
  ));
129
132
  this.stop();
130
133
  }
@@ -147,11 +150,16 @@ class ScreenStream extends EventEmitter {
147
150
  }
148
151
 
149
152
  this.backend = 'x11grab';
150
- this._startFfmpeg(this.captureCtx.x11Env);
153
+ const ffmpegPath = resolveFfmpegPath();
154
+ if (!ffmpegPath) {
155
+ throw new Error(
156
+ 'System ffmpeg with x11grab and mjpeg support required. Run: apt install ffmpeg'
157
+ );
158
+ }
159
+ this._startFfmpeg(this.captureCtx.x11Env, ffmpegPath);
151
160
  }
152
161
 
153
- _startFfmpeg(env) {
154
- const ffmpegPath = resolveFfmpegPath();
162
+ _startFfmpeg(env, ffmpegPath) {
155
163
  const ffmpegArgs = buildFfmpegArgs({
156
164
  fps: this.fps,
157
165
  maxWidth: this.maxWidth,
@@ -193,6 +201,20 @@ class ScreenStream extends EventEmitter {
193
201
  this._grimTimer = setInterval(() => this._captureGrimFrame(), intervalMs);
194
202
  }
195
203
 
204
+ _emitFrameObject(buffer, mime) {
205
+ if (buffer.length < 500) return;
206
+
207
+ if (buffer.length > MAX_FRAME_BYTES && mime === 'image/jpeg') {
208
+ if (this.bumpQuality()) {
209
+ this.emit('quality-adjusted', { jpegQuality: this.jpegQuality });
210
+ return;
211
+ }
212
+ }
213
+
214
+ this._markFrame();
215
+ this.emit('frame', { buffer, mime: mime || detectImageMime(buffer) });
216
+ }
217
+
196
218
  _captureGrimFrame() {
197
219
  if (!this.running || this._grimBusy) return;
198
220
  if (!hasCommand('grim')) {
@@ -205,21 +227,34 @@ class ScreenStream extends EventEmitter {
205
227
  const proc = spawnAsUser(
206
228
  this.captureCtx.runAsUser,
207
229
  'grim',
208
- ['-t', 'jpeg', '-'],
230
+ ['-'],
209
231
  this.captureCtx.waylandEnv,
210
232
  );
211
233
 
212
234
  const chunks = [];
213
235
  proc.stdout.on('data', (c) => chunks.push(c));
214
- proc.stderr.on('data', (c) => { this._stderr += c.toString(); });
236
+ proc.stderr.on('data', (c) => {
237
+ const line = c.toString().trim();
238
+ if (line) this._stderr = `${this._stderr}${line}\n`.slice(-2000);
239
+ });
215
240
 
216
241
  proc.on('close', (code) => {
217
242
  this._grimBusy = false;
218
- if (code !== 0) return;
243
+ if (code !== 0) {
244
+ this._grimFailures += 1;
245
+ if (this._grimFailures >= 3) {
246
+ const detail = this._stderr.trim() || `grim exited ${code}`;
247
+ this.emit('error', new Error(
248
+ `grim capture failed for user ${this.captureCtx.runAsUser || this.captureCtx.user}: ${detail}`
249
+ ));
250
+ this.stop();
251
+ }
252
+ return;
253
+ }
254
+ this._grimFailures = 0;
219
255
  const frame = Buffer.concat(chunks);
220
256
  if (frame.length > 100) {
221
- this._markFrame();
222
- this.emit('frame', frame);
257
+ this._emitFrameObject(frame, 'image/png');
223
258
  }
224
259
  });
225
260
 
@@ -314,22 +349,12 @@ class ScreenStream extends EventEmitter {
314
349
 
315
350
  const frame = this.buffer.subarray(0, end + 2);
316
351
  this.buffer = this.buffer.subarray(end + 2);
317
- this._emitFrame(frame);
352
+ this._emitFrameObject(frame, 'image/jpeg');
318
353
  }
319
354
  }
320
355
 
321
356
  _emitFrame(frame) {
322
- if (frame.length < 500) return;
323
-
324
- if (frame.length > MAX_FRAME_BYTES) {
325
- if (this.bumpQuality()) {
326
- this.emit('quality-adjusted', { jpegQuality: this.jpegQuality });
327
- return;
328
- }
329
- }
330
-
331
- this._markFrame();
332
- this.emit('frame', frame);
357
+ this._emitFrameObject(frame, 'image/jpeg');
333
358
  }
334
359
 
335
360
  static captureOnce(options = {}) {
@@ -340,10 +365,11 @@ class ScreenStream extends EventEmitter {
340
365
  reject(new Error('Screen capture timed out'));
341
366
  }, 15000);
342
367
 
343
- stream.once('frame', (frame) => {
368
+ stream.once('frame', (frameObj) => {
344
369
  clearTimeout(timeout);
345
370
  stream.stop();
346
- resolve(frame);
371
+ const buffer = frameObj.buffer || frameObj;
372
+ resolve(buffer);
347
373
  });
348
374
 
349
375
  stream.once('error', (err) => {
@@ -12,6 +12,7 @@ let frameSeq = 0;
12
12
  let activeOptions = null;
13
13
  let inFlight = false;
14
14
  let pendingFrame = null;
15
+ let pendingMime = 'image/jpeg';
15
16
  let skippedFrames = 0;
16
17
  let adaptiveSkipMod = 1;
17
18
  let lastErrorSentAt = 0;
@@ -28,6 +29,7 @@ function clearCapture() {
28
29
  activeOptions = null;
29
30
  inFlight = false;
30
31
  pendingFrame = null;
32
+ pendingMime = 'image/jpeg';
31
33
  skippedFrames = 0;
32
34
  adaptiveSkipMod = 1;
33
35
  lastErrorSentAt = 0;
@@ -40,16 +42,18 @@ function flushPending(ctx) {
40
42
  return;
41
43
  }
42
44
 
43
- const jpegBuffer = pendingFrame;
45
+ const frameBuffer = pendingFrame;
46
+ const frameMime = pendingMime;
44
47
  pendingFrame = null;
48
+ pendingMime = 'image/jpeg';
45
49
  inFlight = true;
46
50
 
47
- ctx.sendScreenFrame(screenSessionId, jpegBuffer.toString('base64'), {
51
+ ctx.sendScreenFrame(screenSessionId, frameBuffer.toString('base64'), {
48
52
  seq: frameSeq++,
49
53
  final: false,
50
54
  encoding: 'base64',
51
- mime: 'image/jpeg',
52
- byteLength: jpegBuffer.length,
55
+ mime: frameMime,
56
+ byteLength: frameBuffer.length,
53
57
  skipped: skippedFrames,
54
58
  });
55
59
 
@@ -65,14 +69,18 @@ function flushPending(ctx) {
65
69
  }
66
70
  }
67
71
 
68
- function queueFrame(ctx, jpegBuffer) {
72
+ function queueFrame(ctx, framePayload) {
73
+ const buffer = framePayload.buffer || framePayload;
74
+ const mime = framePayload.mime || 'image/jpeg';
75
+
69
76
  if (pendingFrame) {
70
77
  skippedFrames += 1;
71
78
  if (skippedFrames % 8 === 0) {
72
79
  updateAdaptiveThrottle();
73
80
  }
74
81
  }
75
- pendingFrame = jpegBuffer;
82
+ pendingFrame = buffer;
83
+ pendingMime = mime;
76
84
 
77
85
  if (!flushScheduled) {
78
86
  flushScheduled = true;
@@ -100,8 +108,8 @@ function updateAdaptiveThrottle() {
100
108
  }
101
109
  }
102
110
 
103
- function onStreamFrame(ctx, jpegBuffer) {
104
- queueFrame(ctx, jpegBuffer);
111
+ function onStreamFrame(ctx, framePayload) {
112
+ queueFrame(ctx, framePayload);
105
113
  }
106
114
 
107
115
  function onStreamError(ctx, err) {
@@ -5,11 +5,11 @@ const { EventEmitter } = require('events');
5
5
 
6
6
  const BOUNDARY = 'frame';
7
7
 
8
- function writeMultipartFrame(res, frameBuffer) {
8
+ function writeMultipartFrame(res, frameBuffer, mime) {
9
9
  if (res.destroyed || res.writableEnded) return false;
10
10
  try {
11
11
  res.write(`--${BOUNDARY}\r\n`);
12
- res.write('Content-Type: image/jpeg\r\n');
12
+ res.write(`Content-Type: ${mime || 'image/jpeg'}\r\n`);
13
13
  res.write(`Content-Length: ${frameBuffer.length}\r\n\r\n`);
14
14
  res.write(frameBuffer);
15
15
  res.write('\r\n');
@@ -23,6 +23,7 @@ function createScreenViewer(port = 5555, options = {}) {
23
23
  const emitter = new EventEmitter();
24
24
  let latestFrame = null;
25
25
  let latestFrameBinary = null;
26
+ let latestMime = 'image/jpeg';
26
27
  let lastError = null;
27
28
  let frameCount = 0;
28
29
  const streamClients = new Set();
@@ -57,9 +58,9 @@ function createScreenViewer(port = 5555, options = {}) {
57
58
  scheduleIdleStop();
58
59
  }
59
60
 
60
- function pushFrameToClients(frameBuffer) {
61
+ function pushFrameToClients(frameBuffer, mime) {
61
62
  for (const client of streamClients) {
62
- if (!writeMultipartFrame(client, frameBuffer)) {
63
+ if (!writeMultipartFrame(client, frameBuffer, mime)) {
63
64
  streamClients.delete(client);
64
65
  }
65
66
  }
@@ -68,7 +69,7 @@ function createScreenViewer(port = 5555, options = {}) {
68
69
  const server = http.createServer((req, res) => {
69
70
  const url = req.url.split('?')[0];
70
71
 
71
- if (url === '/' || url === '/index.html' || url === '/frame.jpg' || url.startsWith('/frame')) {
72
+ if (url === '/' || url === '/index.html' || url === '/frame.jpg' || url === '/frame.img' || url.startsWith('/frame')) {
72
73
  touchViewer();
73
74
  }
74
75
 
@@ -98,7 +99,7 @@ function createScreenViewer(port = 5555, options = {}) {
98
99
 
99
100
  async function tick() {
100
101
  try {
101
- const r = await fetch('/frame.jpg?' + Date.now());
102
+ const r = await fetch('/frame.img?' + Date.now());
102
103
  if (r.status === 204) {
103
104
  err.textContent = 'Waiting for frames from client...';
104
105
  return;
@@ -139,7 +140,7 @@ function createScreenViewer(port = 5555, options = {}) {
139
140
  clearIdleTimer();
140
141
 
141
142
  if (latestFrameBinary) {
142
- writeMultipartFrame(res, latestFrameBinary);
143
+ writeMultipartFrame(res, latestFrameBinary, latestMime);
143
144
  }
144
145
 
145
146
  req.on('close', () => removeStreamClient(res));
@@ -147,7 +148,7 @@ function createScreenViewer(port = 5555, options = {}) {
147
148
  return;
148
149
  }
149
150
 
150
- if (url === '/frame.jpg') {
151
+ if (url === '/frame.jpg' || url === '/frame.img') {
151
152
  if (lastError && !latestFrameBinary) {
152
153
  res.writeHead(503, {
153
154
  'Content-Type': 'text/plain; charset=utf-8',
@@ -162,7 +163,7 @@ function createScreenViewer(port = 5555, options = {}) {
162
163
  return;
163
164
  }
164
165
  res.writeHead(200, {
165
- 'Content-Type': 'image/jpeg',
166
+ 'Content-Type': latestMime || 'image/jpeg',
166
167
  'Cache-Control': 'no-store',
167
168
  'X-Frame-Count': String(frameCount),
168
169
  });
@@ -224,9 +225,10 @@ function createScreenViewer(port = 5555, options = {}) {
224
225
  if (base64) {
225
226
  latestFrame = base64;
226
227
  latestFrameBinary = Buffer.from(base64, 'base64');
228
+ latestMime = meta.mime || 'image/jpeg';
227
229
  frameCount += 1;
228
230
  lastError = null;
229
- pushFrameToClients(latestFrameBinary);
231
+ pushFrameToClients(latestFrameBinary, latestMime);
230
232
  }
231
233
  },
232
234
  };