scrypted-kasa 1.3.0 → 1.3.1
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 +1 -1
- package/src/camera/intercom.ts +29 -6
- package/src/main.ts +41 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scrypted-kasa",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "TP-Link Kasa plugin for Scrypted — cameras (video, two-way audio, spotlight, siren, status LED), plugs/outlets, wall switches, dimmers, and smart bulbs (on/off, brightness, color, color temperature).",
|
|
5
5
|
"author": "Sergey Vilgelm <sergey@vilgelm.com>",
|
|
6
6
|
"license": "MIT",
|
package/src/camera/intercom.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
2
|
import { ClientRequest } from 'http';
|
|
3
3
|
import https from 'https';
|
|
4
|
+
import net from 'net';
|
|
4
5
|
import { PassThrough } from 'stream';
|
|
5
6
|
|
|
6
7
|
export const KASA_TALK_PORT = 18443;
|
|
@@ -77,6 +78,21 @@ export class KasaTalkSession {
|
|
|
77
78
|
|
|
78
79
|
this.request = req;
|
|
79
80
|
|
|
81
|
+
// Disable Nagle on the underlying TCP socket as soon as it's available. Each 20 ms
|
|
82
|
+
// audio part we write is small (~230 B) and fewer than the segment size, so Nagle
|
|
83
|
+
// would coalesce parts waiting for an ACK or for the 200 ms deadline — adding
|
|
84
|
+
// user-perceptible chunkiness on top of the inherent network round-trip. The talk
|
|
85
|
+
// session is a constant low-rate stream, exactly the workload Nagle hurts.
|
|
86
|
+
//
|
|
87
|
+
// Cover both timings: the default https.Agent pools connections and may assign a
|
|
88
|
+
// pooled socket synchronously inside https.request(), in which case `'socket'`
|
|
89
|
+
// already fired by the time we get here and our once-listener would never run.
|
|
90
|
+
// Check `req.socket` immediately, and otherwise register a once-listener for the
|
|
91
|
+
// fresh-connection path.
|
|
92
|
+
const setSocketNoDelay = (socket: net.Socket) => socket.setNoDelay(true);
|
|
93
|
+
if (req.socket) setSocketNoDelay(req.socket as net.Socket);
|
|
94
|
+
else req.once('socket', setSocketNoDelay);
|
|
95
|
+
|
|
80
96
|
req.on('error', e => {
|
|
81
97
|
// Suppress when the close came first (deliberate teardown via close()): ending
|
|
82
98
|
// the body mid-flight makes the in-flight HTTPS request emit "socket hang up"
|
|
@@ -127,12 +143,19 @@ export class KasaTalkSession {
|
|
|
127
143
|
|
|
128
144
|
private writePart(contentType: string, body: Buffer): boolean {
|
|
129
145
|
if (this.closed) return true;
|
|
130
|
-
const header =
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
146
|
+
const header = Buffer.from(
|
|
147
|
+
`--${TALK_BOUNDARY}\r\nContent-Length: ${body.length}\r\nContent-Type: ${contentType}\r\n\r\n`,
|
|
148
|
+
);
|
|
149
|
+
// Concat header + body + CRLF into one write. Three separate writes would each
|
|
150
|
+
// become its own chunked-encoding chunk on the HTTP body and (worse) its own TLS
|
|
151
|
+
// record + TCP segment. Cameras and Node's HTTP/TLS stack both prefer one part
|
|
152
|
+
// arriving as one segment — fewer per-packet ACKs and no risk of the camera
|
|
153
|
+
// partially parsing a part header before the body lands.
|
|
154
|
+
const trailer = Buffer.from('\r\n');
|
|
155
|
+
const out = body.length
|
|
156
|
+
? Buffer.concat([header, body, trailer], header.length + body.length + trailer.length)
|
|
157
|
+
: Buffer.concat([header, trailer], header.length + trailer.length);
|
|
158
|
+
return this.body.write(out);
|
|
136
159
|
}
|
|
137
160
|
|
|
138
161
|
close(): void {
|
package/src/main.ts
CHANGED
|
@@ -502,16 +502,56 @@ class KasaCamera extends ScryptedDeviceBase implements VideoCamera, Settings, In
|
|
|
502
502
|
// -loglevel error + -nostats: without these, ffmpeg emits a `size= time= bitrate=`
|
|
503
503
|
// progress line every ~0.5 s during the talkback session. On long talks that's a
|
|
504
504
|
// steady stream of stderr → string allocations + log I/O for no diagnostic value.
|
|
505
|
+
//
|
|
506
|
+
// Low-latency knobs (without these, talkback lagged ~10 s — the symptom was hearing
|
|
507
|
+
// your own voice through the camera speaker only after releasing Push-to-Talk):
|
|
508
|
+
// -fflags +nobuffer : skip libavformat's input prebuffer (typically 1-2 s on SDP/SRTP).
|
|
509
|
+
// -fflags +flush_packets, -flush_packets 1
|
|
510
|
+
// : flush the AVIO output buffer after every encoded packet
|
|
511
|
+
// instead of when the 32 KB buffer fills (~4 s of 8 kHz mulaw).
|
|
512
|
+
// The two forms cover both global flags and the output-muxer
|
|
513
|
+
// explicit option; cheap belt-and-suspenders.
|
|
514
|
+
// -flags low_delay : codec-level low-delay decode/encode flag.
|
|
515
|
+
// -probesize 32, -analyzeduration 0
|
|
516
|
+
// : skip the auto-detect probe phase, which would otherwise
|
|
517
|
+
// wait for ~5 s of audio to estimate stream parameters
|
|
518
|
+
// before producing the first output sample.
|
|
519
|
+
// -rtsp_transport tcp, -reorder_queue_size 0 (RTSP-only, conditional)
|
|
520
|
+
// : force TCP-interleaved RTP and zero the demuxer's
|
|
521
|
+
// out-of-order reordering window. Only emitted when the
|
|
522
|
+
// input URL is RTSP; for non-RTSP inputs ffmpeg would just
|
|
523
|
+
// warn "Option not found" but the conditional keeps the arg
|
|
524
|
+
// list honest about which demuxer each option targets.
|
|
525
|
+
// -max_delay 0 : zero libavformat's max-delay setting. General — applies
|
|
526
|
+
// to several demuxers, including RTSP/RTP — so safe to pass
|
|
527
|
+
// unconditionally.
|
|
528
|
+
// -af aresample=async=0:min_hard_comp=0
|
|
529
|
+
// : disable async resampling. ffmpeg auto-inserts aresample
|
|
530
|
+
// when input/output sample rates differ (Opus 24 kHz →
|
|
531
|
+
// mulaw 8 kHz); async=0 keeps it operating on tiny frames
|
|
532
|
+
// without the multi-second resync window async=1 introduces.
|
|
533
|
+
const inputArgs = ffmpegInput.inputArguments || [];
|
|
534
|
+
const isRtspInput = inputArgs.some(a => typeof a === 'string' && a.startsWith('rtsp://'));
|
|
505
535
|
// prettier-ignore
|
|
506
536
|
const args = [
|
|
507
537
|
'-hide_banner',
|
|
508
538
|
'-loglevel', 'error',
|
|
509
539
|
'-nostats',
|
|
510
|
-
|
|
540
|
+
'-fflags', '+nobuffer+flush_packets',
|
|
541
|
+
'-flags', 'low_delay',
|
|
542
|
+
'-probesize', '32',
|
|
543
|
+
'-analyzeduration', '0',
|
|
544
|
+
'-max_delay', '0',
|
|
545
|
+
...(isRtspInput ? ['-rtsp_transport', 'tcp', '-reorder_queue_size', '0'] : []),
|
|
546
|
+
'-thread_queue_size', '1',
|
|
547
|
+
'-use_wallclock_as_timestamps', '1',
|
|
548
|
+
...inputArgs,
|
|
511
549
|
'-vn', '-sn', '-dn',
|
|
550
|
+
'-af', 'aresample=async=0:min_hard_comp=0',
|
|
512
551
|
'-f', 'mulaw',
|
|
513
552
|
'-ar', '8000',
|
|
514
553
|
'-ac', '1',
|
|
554
|
+
'-flush_packets', '1',
|
|
515
555
|
'pipe:1',
|
|
516
556
|
];
|
|
517
557
|
const cp = child_process.spawn(ffmpegPath, args);
|