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
|
@@ -10,23 +10,24 @@ const MAX_FRAME_BYTES = 768 * 1024;
|
|
|
10
10
|
const FRAME_WATCHDOG_MS = 10000;
|
|
11
11
|
|
|
12
12
|
function resolveFfmpegPath() {
|
|
13
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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'
|
|
123
|
-
? '
|
|
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},
|
|
127
|
-
+ (
|
|
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
|
-
|
|
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
|
-
['-
|
|
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) => {
|
|
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)
|
|
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.
|
|
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.
|
|
352
|
+
this._emitFrameObject(frame, 'image/jpeg');
|
|
318
353
|
}
|
|
319
354
|
}
|
|
320
355
|
|
|
321
356
|
_emitFrame(frame) {
|
|
322
|
-
|
|
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', (
|
|
368
|
+
stream.once('frame', (frameObj) => {
|
|
344
369
|
clearTimeout(timeout);
|
|
345
370
|
stream.stop();
|
|
346
|
-
|
|
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
|
|
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,
|
|
51
|
+
ctx.sendScreenFrame(screenSessionId, frameBuffer.toString('base64'), {
|
|
48
52
|
seq: frameSeq++,
|
|
49
53
|
final: false,
|
|
50
54
|
encoding: 'base64',
|
|
51
|
-
mime:
|
|
52
|
-
byteLength:
|
|
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,
|
|
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 =
|
|
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,
|
|
104
|
-
queueFrame(ctx,
|
|
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(
|
|
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.
|
|
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
|
};
|