buzz-me-in 2.2.0 → 2.2.2-beta.0

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/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ ![buzz-me-in: answer your apartment intercom from Apple Home](https://raw.githubusercontent.com/atdr/buzz-me-in/main/docs/assets/banner.png)
2
+
1
3
  # buzz-me-in
2
4
 
3
5
  [![npm](https://img.shields.io/npm/v/buzz-me-in.svg?style=flat-square)](https://www.npmjs.com/package/buzz-me-in)
package/homekit.js CHANGED
@@ -26,6 +26,7 @@ const { spawn } = require('child_process');
26
26
  const config = require('./src/core/config');
27
27
  const state = require('./src/core/state');
28
28
  const { sendDtmfSequence, sendMulawAudio } = require('./src/core/mulaw-audio');
29
+ const { buildInboundFfmpegArgs } = require('./src/core/inbound-ffmpeg-args');
29
30
  const {
30
31
  removeReturnAudioSdp,
31
32
  removeSdpDir,
@@ -281,127 +282,23 @@ function _startSession(sessionID, s, request, callback) {
281
282
  // Reopening the live view inside the grace window means the user is not
282
283
  // finished after all.
283
284
  cancelPendingHangUp('homekit-session-restarted');
284
- const videoParams = srtpParams(s.hkVideoKey, s.hkVideoSalt);
285
- const audioParams = srtpParams(s.hkAudioKey, s.hkAudioSalt);
286
- const video = request.video;
287
285
  const audio = request.audio;
288
- const videoBitrate = Math.max(64, video.max_bit_rate || 200);
289
- const videoBufferSize = Math.max(videoBitrate * 2, 128);
290
- const mtu = video.mtu || 1316;
291
286
 
292
- // -------------------------------------------------------------------------
293
- // Inbound ffmpeg
294
- //
295
- // Input 0 – raw mulaw/8kHz from Twilio via stdin
296
- // Let the raw audio demuxer derive PTS from sample count. Using wall-clock
297
- // timestamps breaks when ffmpeg drains the buffered startup audio burst.
298
- // -ch_layout declares mono rather than -ac counting one channel: a count
299
- // leaves the layout unset, so the decoder guesses it and logs "Guessed
300
- // Channel Layout: mono" on every call. Our stderr handler is a warn, so
301
- // that guess looked like a fault in the journal. Needs ffmpeg >= 5.1.
302
- //
303
- // Input 1 – synthetic black video (lavfi color source)
304
- // Generates H.264 Baseline/3.1 frames at 15 fps.
305
- // HomeKit requires a video track; there is no real camera feed.
306
- // keyint_min=15 / -g 15 forces an IDR frame every second — HomeKit
307
- // requests it when the live view is first opened; without frequent IDRs
308
- // the video stays blank until the next natural keyframe.
309
- //
310
- // Two separate SRTP outputs — no muxing, no pts coupling between streams.
311
- // -------------------------------------------------------------------------
312
- const ffIn = spawn('ffmpeg', [
313
- '-y',
314
- '-loglevel',
315
- 'warning',
316
-
317
- // ---- Input 0: raw mulaw from Twilio ----
318
- '-thread_queue_size',
319
- '512',
320
- '-f',
321
- 'mulaw',
322
- '-ar',
323
- '8000',
324
- '-ch_layout',
325
- 'mono',
326
- '-i',
327
- 'pipe:0',
328
-
329
- // ---- Input 1: blank video ----
330
- '-f',
331
- 'lavfi',
332
- '-i',
333
- 'color=black:s=1280x720:r=15',
334
-
335
- // ---- Video output → HomeKit SRTP ----
336
- '-map',
337
- '1:v',
338
- '-c:v',
339
- 'libx264',
340
- '-profile:v',
341
- 'baseline',
342
- '-level:v',
343
- '3.1',
344
- '-preset',
345
- 'ultrafast',
346
- '-tune',
347
- 'zerolatency',
348
- '-pix_fmt',
349
- 'yuv420p',
350
- '-b:v',
351
- `${videoBitrate}k`,
352
- '-maxrate',
353
- `${videoBitrate}k`,
354
- '-bufsize',
355
- `${videoBufferSize}k`,
356
- '-g',
357
- String(video.fps || 15),
358
- '-keyint_min',
359
- String(video.fps || 15),
360
- '-payload_type',
361
- String(video.pt),
362
- '-ssrc',
363
- String(s.videoSsrc),
364
- '-f',
365
- 'rtp',
366
- '-srtp_out_suite',
367
- 'AES_CM_128_HMAC_SHA1_80',
368
- '-srtp_out_params',
369
- videoParams,
370
- `srtp://${s.targetAddress}:${s.hkVideoPort}?rtcpport=${s.hkVideoPort}&localrtcpport=${s.hkVideoPort}&pkt_size=${mtu}`,
371
-
372
- // ---- Audio output → HomeKit SRTP (Opus/16kHz) ----
373
- //
374
- // Note on codec choice: libopus is in every standard ffmpeg build.
375
- // If you prefer AAC-ELD (required by some older HomeKit devices), compile
376
- // ffmpeg with --enable-libfdk-aac --enable-nonfree and change:
377
- // '-c:a', 'libfdk_aac', '-profile:a', 'aac_eld',
378
- // and update streamingOptions.audio.codecs below to AAC_ELD.
379
- '-map',
380
- '0:a',
381
- '-c:a',
382
- 'libopus',
383
- '-ar',
384
- '16000',
385
- '-ac',
386
- '1',
387
- '-b:a',
388
- '24k',
389
- '-application',
390
- 'voip',
391
- '-frame_duration',
392
- '20',
393
- '-payload_type',
394
- String(audio.pt),
395
- '-ssrc',
396
- String(s.audioSsrc),
397
- '-f',
398
- 'rtp',
399
- '-srtp_out_suite',
400
- 'AES_CM_128_HMAC_SHA1_80',
401
- '-srtp_out_params',
402
- audioParams,
403
- `srtp://${s.targetAddress}:${s.hkAudioPort}?rtcpport=${s.hkAudioPort}&localrtcpport=${s.hkAudioPort}`,
404
- ]);
287
+ // Inbound ffmpeg: see src/core/inbound-ffmpeg-args.js for the pipeline.
288
+ const ffIn = spawn(
289
+ 'ffmpeg',
290
+ buildInboundFfmpegArgs({
291
+ video: request.video,
292
+ audio,
293
+ videoParams: srtpParams(s.hkVideoKey, s.hkVideoSalt),
294
+ audioParams: srtpParams(s.hkAudioKey, s.hkAudioSalt),
295
+ targetAddress: s.targetAddress,
296
+ hkVideoPort: s.hkVideoPort,
297
+ hkAudioPort: s.hkAudioPort,
298
+ videoSsrc: s.videoSsrc,
299
+ audioSsrc: s.audioSsrc,
300
+ })
301
+ );
405
302
 
406
303
  ffIn.stderr.on('data', (d) => {
407
304
  logger.warn('Inbound ffmpeg stderr', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buzz-me-in",
3
- "version": "2.2.0",
3
+ "version": "2.2.2-beta.0",
4
4
  "description": "HomeKit-compatible apartment intercom via Twilio media streams",
5
5
  "keywords": [
6
6
  "homekit",
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "main": "server.js",
17
17
  "bin": {
18
- "buzz-me-in": "./server.js"
18
+ "buzz-me-in": "server.js"
19
19
  },
20
20
  "files": [
21
21
  "server.js",
@@ -57,14 +57,14 @@
57
57
  "hap-nodejs": "^0.14.2",
58
58
  "qrcode-terminal": "^0.12.0",
59
59
  "twilio": "^6.1.0",
60
- "websocket": "^1.0.35",
60
+ "ws": "^8.22.0",
61
61
  "zod": "^4.5.4"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@commitlint/cli": "^21.2.2",
65
65
  "@commitlint/config-conventional": "^21.0.2",
66
66
  "@eslint/js": "^10.0.1",
67
- "@types/websocket": "^1.0.10",
67
+ "@types/ws": "^8.18.1",
68
68
  "eslint": "^10.9.1",
69
69
  "eslint-config-prettier": "^10.1.8",
70
70
  "globals": "^17.11.0",
package/server.js CHANGED
@@ -29,7 +29,7 @@ if (require.main === module) {
29
29
 
30
30
  const http = require('http');
31
31
  const twilio = require('twilio');
32
- const WebSocketServer = require('websocket').server;
32
+ const { WebSocketServer } = require('ws');
33
33
 
34
34
  const config = require('./src/core/config');
35
35
  const state = require('./src/core/state');
@@ -45,7 +45,7 @@ const homekit = require('./homekit');
45
45
  const { createLogger } = require('./src/core/log');
46
46
  const { safeEqualString } = require('./src/core/safe-equal');
47
47
  const { MediaStream } = require('./src/core/media-stream');
48
- /** @import { request as WebSocketRequest } from 'websocket' */
48
+ /** @import { Duplex } from 'stream' */
49
49
  /** @import { HandshakeVerificationError } from './src/core/types' */
50
50
 
51
51
  const HTTP_SERVER_PORT = config.port;
@@ -82,17 +82,19 @@ const RINGTONE_WAV = createRingbackWav();
82
82
 
83
83
  const wsserver = http.createServer(handleRequest);
84
84
 
85
- // Enforce message bounds in the websocket library itself so oversized
86
- // frames are rejected before assembly, instead of relying only on the
87
- // per-message check in MediaStream (library defaults allow 1 MiB).
85
+ // Enforce message bounds in the ws library itself so oversized frames are
86
+ // rejected before assembly, instead of relying only on the per-message check
87
+ // in MediaStream (the library default allows 100 MiB).
88
88
  const WS_LIBRARY_MAX_BYTES = MAX_WS_UTF8_BYTES * 4;
89
89
  const MAX_CONCURRENT_WS_CONNECTIONS = config.wsMaxConnections;
90
90
 
91
+ // noServer: the upgrade handler below runs the handshake checks and only then
92
+ // hands the socket to ws. Sockets are tracked in activeWsConnections instead of
93
+ // by the library.
91
94
  const mediaws = new WebSocketServer({
92
- httpServer: wsserver,
93
- autoAcceptConnections: false,
94
- maxReceivedFrameSize: WS_LIBRARY_MAX_BYTES,
95
- maxReceivedMessageSize: WS_LIBRARY_MAX_BYTES,
95
+ noServer: true,
96
+ clientTracking: false,
97
+ maxPayload: WS_LIBRARY_MAX_BYTES,
96
98
  });
97
99
 
98
100
  state.setOnSessionStale((session) => {
@@ -294,28 +296,64 @@ GET_ROUTES.set('/readyz', function (_req, res) {
294
296
  // WebSocket media stream
295
297
  // ---------------------------------------------------------------------------
296
298
 
297
- mediaws.on('request', function (request) {
298
- /** @type {WebSocketRequest} */
299
- const wsRequest = request;
299
+ /**
300
+ * Refuse a WebSocket upgrade with a plain HTTP response, before any WebSocket
301
+ * handshake is written, then drop the socket.
302
+ *
303
+ * @param {Duplex} socket
304
+ * @param {number} status
305
+ * @param {string} reason
306
+ * @returns {void}
307
+ */
308
+ function rejectUpgrade(socket, status, reason) {
309
+ socket.end(
310
+ `HTTP/1.1 ${status} ${http.STATUS_CODES[status]}\r\n` +
311
+ 'Connection: close\r\n' +
312
+ 'Content-Type: text/plain\r\n' +
313
+ `Content-Length: ${Buffer.byteLength(reason)}\r\n` +
314
+ '\r\n' +
315
+ reason
316
+ );
317
+ }
318
+
319
+ /**
320
+ * Handshake checks for the `/media` WebSocket, run before ws takes the socket.
321
+ * Exported so tests can mount it on their own HTTP server.
322
+ *
323
+ * @param {import('http').IncomingMessage} request
324
+ * @param {Duplex} socket
325
+ * @param {Buffer} head
326
+ * @returns {void}
327
+ */
328
+ function handleMediaUpgrade(request, socket, head) {
329
+ // A client that resets mid-handshake would otherwise raise an unhandled
330
+ // 'error' and take the process down via the uncaughtException handler.
331
+ socket.on('error', () => socket.destroy());
300
332
  if (shuttingDown) {
301
- wsRequest.reject(503, 'Server shutting down');
333
+ rejectUpgrade(socket, 503, 'Server shutting down');
334
+ return;
335
+ }
336
+ // Node accepts an absolute-form target such as `http://[`, which URL cannot
337
+ // parse. Thrown here, it would exit the process via uncaughtException.
338
+ let path;
339
+ try {
340
+ path = new URL(request.url || '/', 'http://localhost').pathname;
341
+ } catch {
342
+ rejectUpgrade(socket, 400, 'Bad request');
302
343
  return;
303
344
  }
304
- const path = wsRequest.resourceURL && wsRequest.resourceURL.pathname;
305
345
  if (path !== STREAM_PATH) {
306
- wsRequest.reject(404, 'Not found');
346
+ rejectUpgrade(socket, 404, 'Not found');
307
347
  return;
308
348
  }
309
349
  // Twilio signs the Media Streams handshake, so an unauthenticated socket can
310
- // be refused here rather than after accept(). Without this the server accepts
311
- // the connection and allocates a MediaStream before any auth runs, because
312
- // the stream token only arrives later in the `start` frame. Defence in depth:
313
- // the one-time token below remains the authority, since this signature is an
314
- // HMAC over a constant URL and so never varies between calls.
350
+ // be refused here rather than after the upgrade. Without this the server
351
+ // accepts the connection and allocates a MediaStream before any auth runs,
352
+ // because the stream token only arrives later in the `start` frame. Defence
353
+ // in depth: the one-time token below remains the authority, since this
354
+ // signature is an HMAC over a constant URL and so never varies between calls.
315
355
  if (MEDIA_SIGNATURE_MODE !== 'off') {
316
- const handshake = verifyStreamHandshakeSignature(
317
- wsRequest.httpRequest.headers['x-twilio-signature']
318
- );
356
+ const handshake = verifyStreamHandshakeSignature(request.headers['x-twilio-signature']);
319
357
  if (!handshake.ok) {
320
358
  // strict:false disables discriminated-union narrowing; cast as elsewhere.
321
359
  const handshakeError = /** @type {HandshakeVerificationError} */ (handshake);
@@ -325,7 +363,7 @@ mediaws.on('request', function (request) {
325
363
  enforced: MEDIA_SIGNATURE_MODE === 'enforce',
326
364
  });
327
365
  if (MEDIA_SIGNATURE_MODE === 'enforce') {
328
- wsRequest.reject(403, 'Forbidden');
366
+ rejectUpgrade(socket, 403, 'Forbidden');
329
367
  return;
330
368
  }
331
369
  } else {
@@ -348,18 +386,31 @@ mediaws.on('request', function (request) {
348
386
  event: 'media-ws-rejected',
349
387
  reason: 'too-many-connections',
350
388
  });
351
- wsRequest.reject(503, 'Too many connections');
389
+ rejectUpgrade(socket, 503, 'Too many connections');
352
390
  return;
353
391
  }
354
392
 
355
- const connection = wsRequest.accept(null, wsRequest.origin);
356
- activeWsConnections.add(connection);
357
- connection.on('close', () => activeWsConnections.delete(connection));
358
- mediaWsLogger.info('Media websocket connection accepted', {
359
- event: 'media-ws-accepted',
393
+ mediaws.handleUpgrade(request, socket, head, function (connection) {
394
+ activeWsConnections.add(connection);
395
+ connection.on('close', () => activeWsConnections.delete(connection));
396
+ // ws emits 'error' for protocol violations and frames over maxPayload, then
397
+ // closes the socket itself; 'close' still fires, so MediaStream tears down.
398
+ // Unhandled, the error would exit the process.
399
+ connection.on('error', (err) => {
400
+ mediaWsLogger.warn('Media websocket protocol error', {
401
+ event: 'media-ws-error',
402
+ reason: /** @type {NodeJS.ErrnoException} */ (err).code || 'ws-error',
403
+ error: err,
404
+ });
405
+ });
406
+ mediaWsLogger.info('Media websocket connection accepted', {
407
+ event: 'media-ws-accepted',
408
+ });
409
+ new MediaStream(connection, mediaStreamDeps);
360
410
  });
361
- new MediaStream(connection, mediaStreamDeps);
362
- });
411
+ }
412
+
413
+ wsserver.on('upgrade', handleMediaUpgrade);
363
414
 
364
415
  /** @type {import('./src/core/media-stream').MediaStreamDeps} */
365
416
  const mediaStreamDeps = {
@@ -543,6 +594,7 @@ module.exports = {
543
594
  beginShutdown,
544
595
  buildTwiml,
545
596
  describeCaller,
597
+ handleMediaUpgrade,
546
598
  handleRequest,
547
599
  isAuthorizedForStatus,
548
600
  isValidTwilioRequest,
@@ -0,0 +1,163 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Argument list for the inbound ffmpeg process: Twilio mu-law on stdin →
5
+ * Opus/SRTP audio plus a synthetic H.264/SRTP video track to the controller.
6
+ * Kept apart from homekit.js so the arguments can be tested without spawning
7
+ * ffmpeg or standing up a HomeKit session.
8
+ */
9
+
10
+ /**
11
+ * @param {{
12
+ * video: { max_bit_rate?: number, mtu?: number, fps?: number, pt: number },
13
+ * audio: { pt: number },
14
+ * videoParams: string,
15
+ * audioParams: string,
16
+ * targetAddress: string,
17
+ * hkVideoPort: number,
18
+ * hkAudioPort: number,
19
+ * videoSsrc: number,
20
+ * audioSsrc: number,
21
+ * }} input
22
+ * @returns {string[]}
23
+ */
24
+ function buildInboundFfmpegArgs(input) {
25
+ const { video, audio, videoParams, audioParams } = input;
26
+ const videoBitrate = Math.max(64, video.max_bit_rate || 200);
27
+ const videoBufferSize = Math.max(videoBitrate * 2, 128);
28
+ const mtu = video.mtu || 1316;
29
+
30
+ // -------------------------------------------------------------------------
31
+ // Inbound ffmpeg
32
+ //
33
+ // Input 0 – raw mulaw/8kHz from Twilio via stdin
34
+ // Let the raw audio demuxer derive PTS from sample count. Using wall-clock
35
+ // timestamps breaks when ffmpeg drains the buffered startup audio burst.
36
+ // -ch_layout declares mono rather than -ac counting one channel: a count
37
+ // leaves the layout unset, so the decoder guesses it and logs "Guessed
38
+ // Channel Layout: mono" on every call. Our stderr handler is a warn, so
39
+ // that guess looked like a fault in the journal. Needs ffmpeg >= 5.1.
40
+ // -probesize 32 / -analyzeduration 0 skip stream probing: format, rate
41
+ // and layout are all declared, but by default ffmpeg still reads ~3 s of
42
+ // stdin before it outputs anything, then sends that backlog to the
43
+ // controller in one burst, and the caller's first words are lost (#102).
44
+ //
45
+ // Input 1 – synthetic black video (lavfi color source)
46
+ // Generates H.264 Baseline/3.1 frames at 15 fps.
47
+ // HomeKit requires a video track; there is no real camera feed.
48
+ // keyint_min=15 / -g 15 forces an IDR frame every second — HomeKit
49
+ // requests it when the live view is first opened; without frequent IDRs
50
+ // the video stays blank until the next natural keyframe.
51
+ //
52
+ // Two separate SRTP outputs — no muxing, no pts coupling between streams.
53
+ // -------------------------------------------------------------------------
54
+ return [
55
+ '-y',
56
+ '-loglevel',
57
+ 'warning',
58
+
59
+ // ---- Input 0: raw mulaw from Twilio ----
60
+ '-thread_queue_size',
61
+ '512',
62
+ '-probesize',
63
+ '32',
64
+ '-analyzeduration',
65
+ '0',
66
+ '-f',
67
+ 'mulaw',
68
+ '-ar',
69
+ '8000',
70
+ '-ch_layout',
71
+ 'mono',
72
+ '-i',
73
+ 'pipe:0',
74
+
75
+ // ---- Input 1: blank video ----
76
+ '-f',
77
+ 'lavfi',
78
+ '-i',
79
+ 'color=black:s=1280x720:r=15',
80
+
81
+ // ---- Video output → HomeKit SRTP ----
82
+ '-map',
83
+ '1:v',
84
+ '-c:v',
85
+ 'libx264',
86
+ '-profile:v',
87
+ 'baseline',
88
+ '-level:v',
89
+ '3.1',
90
+ '-preset',
91
+ 'ultrafast',
92
+ '-tune',
93
+ 'zerolatency',
94
+ '-pix_fmt',
95
+ 'yuv420p',
96
+ '-b:v',
97
+ `${videoBitrate}k`,
98
+ '-maxrate',
99
+ `${videoBitrate}k`,
100
+ '-bufsize',
101
+ `${videoBufferSize}k`,
102
+ '-g',
103
+ String(video.fps || 15),
104
+ '-keyint_min',
105
+ String(video.fps || 15),
106
+ '-payload_type',
107
+ String(video.pt),
108
+ '-ssrc',
109
+ String(input.videoSsrc),
110
+ '-f',
111
+ 'rtp',
112
+ '-srtp_out_suite',
113
+ 'AES_CM_128_HMAC_SHA1_80',
114
+ '-srtp_out_params',
115
+ videoParams,
116
+ `srtp://${input.targetAddress}:${input.hkVideoPort}?rtcpport=${input.hkVideoPort}&localrtcpport=${input.hkVideoPort}&pkt_size=${mtu}`,
117
+
118
+ // ---- Audio output → HomeKit SRTP (Opus/16kHz) ----
119
+ //
120
+ // Note on codec choice: libopus is in every standard ffmpeg build.
121
+ // If you prefer AAC-ELD (required by some older HomeKit devices), compile
122
+ // ffmpeg with --enable-libfdk-aac --enable-nonfree and change:
123
+ // '-c:a', 'libfdk_aac', '-profile:a', 'aac_eld',
124
+ // and update streamingOptions.audio.codecs below to AAC_ELD.
125
+ //
126
+ // RTP clock: HomeKit clocks Opus RTP timestamps at the negotiated sample
127
+ // rate (16 kHz, 320 ticks per 20 ms packet), which is also what the iPhone
128
+ // sends on the return leg. ffmpeg's RTP muxer always uses RFC 7587's
129
+ // 48 kHz for Opus (960 ticks), so the controller saw audio arriving three
130
+ // times slower than its timestamps said, and dropped words (#102).
131
+ // libopus stamps packets in 1/16000; relabelling that time base as
132
+ // 1/48000 stops the muxer rescaling, so each packet advances by 320.
133
+ '-map',
134
+ '0:a',
135
+ '-c:a',
136
+ 'libopus',
137
+ '-ar',
138
+ '16000',
139
+ '-ac',
140
+ '1',
141
+ '-b:a',
142
+ '24k',
143
+ '-application',
144
+ 'voip',
145
+ '-frame_duration',
146
+ '20',
147
+ '-bsf:a',
148
+ 'setts=time_base=1/48000',
149
+ '-payload_type',
150
+ String(audio.pt),
151
+ '-ssrc',
152
+ String(input.audioSsrc),
153
+ '-f',
154
+ 'rtp',
155
+ '-srtp_out_suite',
156
+ 'AES_CM_128_HMAC_SHA1_80',
157
+ '-srtp_out_params',
158
+ audioParams,
159
+ `srtp://${input.targetAddress}:${input.hkAudioPort}?rtcpport=${input.hkAudioPort}&localrtcpport=${input.hkAudioPort}`,
160
+ ];
161
+ }
162
+
163
+ module.exports = { buildInboundFfmpegArgs };
@@ -13,7 +13,7 @@ const { PassThrough } = require('stream');
13
13
  const { parseTwilioWsEvent, parseTwilioMediaPayload } = require('./ws-events-schema');
14
14
  const { createRingbackMulawCycle, sendMulawAudio } = require('./mulaw-audio');
15
15
 
16
- /** @import { connection, Message } from 'websocket' */
16
+ /** @import { WebSocket } from 'ws' */
17
17
  /** @import { WsEventParseOkSupported, StreamTokenVerificationResult, TokenVerificationError, WsEventParseError, MediaPayloadParseError } from './types' */
18
18
 
19
19
  /**
@@ -31,7 +31,7 @@ const { createRingbackMulawCycle, sendMulawAudio } = require('./mulaw-audio');
31
31
 
32
32
  class MediaStream {
33
33
  /**
34
- * @param {connection} connection
34
+ * @param {WebSocket} connection
35
35
  * @param {MediaStreamDeps} deps
36
36
  */
37
37
  constructor(connection, deps) {
@@ -67,11 +67,12 @@ class MediaStream {
67
67
  }
68
68
 
69
69
  /**
70
- * @param {Message} message
70
+ * @param {Buffer} data
71
+ * @param {boolean} isBinary
71
72
  */
72
- processMessage(message) {
73
- if (message.type !== 'utf8') return;
74
- if (message.utf8Data.length > this.deps.maxUtf8Bytes) {
73
+ processMessage(data, isBinary) {
74
+ if (isBinary) return;
75
+ if (data.length > this.deps.maxUtf8Bytes) {
75
76
  this.logger.warn('Media websocket message too large', {
76
77
  event: 'media-ws-message-too-large',
77
78
  reason: 'max-message-bytes-exceeded',
@@ -82,7 +83,7 @@ class MediaStream {
82
83
 
83
84
  let rawData;
84
85
  try {
85
- rawData = JSON.parse(message.utf8Data);
86
+ rawData = JSON.parse(data.toString('utf8'));
86
87
  } catch {
87
88
  this.logger.warn('Media websocket invalid JSON', {
88
89
  event: 'media-ws-invalid-json',
@@ -174,14 +174,14 @@ function createDualToneMulaw(lowFrequency, highFrequency, durationMs, amplitude)
174
174
  }
175
175
 
176
176
  /**
177
- * @param {{ streamSid: string, wsConnection: { sendUTF: (message: string) => void } }} activeCall
177
+ * @param {{ streamSid: string, wsConnection: { send: (message: string) => void } }} activeCall
178
178
  * @param {Buffer} payload
179
179
  */
180
180
  function sendMulawAudio(activeCall, payload) {
181
181
  if (!activeCall || !activeCall.streamSid || !activeCall.wsConnection) {
182
182
  throw new Error('Cannot send audio without an active media stream');
183
183
  }
184
- activeCall.wsConnection.sendUTF(
184
+ activeCall.wsConnection.send(
185
185
  JSON.stringify({
186
186
  event: 'media',
187
187
  streamSid: activeCall.streamSid,
@@ -191,7 +191,7 @@ function sendMulawAudio(activeCall, payload) {
191
191
  }
192
192
 
193
193
  /**
194
- * @param {{ streamSid: string, wsConnection: { sendUTF: (message: string) => void } }} activeCall
194
+ * @param {{ streamSid: string, wsConnection: { send: (message: string) => void } }} activeCall
195
195
  * @param {string} digits
196
196
  * @param {{ chunkMs?: number, toneMs?: number, trailingSilenceMs?: number }} [options]
197
197
  */
package/src/core/state.js CHANGED
@@ -4,7 +4,7 @@ const config = require('./config');
4
4
  const { createLogger } = require('./log');
5
5
  const logger = createLogger({ component: 'state' });
6
6
 
7
- /** @import { connection as WebSocketConnection } from 'websocket' */
7
+ /** @import { WebSocket as WebSocketConnection } from 'ws' */
8
8
  /** @import { CallSession, CallStatus, StartCallResult, ClearCallResult, ClearedCallSession } from './types.js' */
9
9
 
10
10
  class CallSessionManager {
package/src/core/types.js CHANGED
@@ -118,7 +118,7 @@
118
118
  * @typedef {{
119
119
  * callSid: string,
120
120
  * streamSid: string,
121
- * wsConnection: import('websocket').connection,
121
+ * wsConnection: import('ws').WebSocket,
122
122
  * createdAtMs: number,
123
123
  * lastEventAtMs: number,
124
124
  * lastEvent: string