• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

homebridge / HAP-NodeJS / 10228497839

03 Aug 2024 01:48PM UTC coverage: 64.121%. First build
10228497839

push

github

bwp91
Updated dependencies + lint

1352 of 2499 branches covered (54.1%)

Branch coverage included in aggregate %.

6 of 10 new or added lines in 3 files covered. (60.0%)

6170 of 9232 relevant lines covered (66.83%)

312.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

45.01
/src/lib/camera/RTPStreamManagement.ts
1
import assert from "assert";
18✔
2
import crypto from "crypto";
18✔
3
import createDebug from "debug";
18✔
4
import net from "net";
18✔
5
import { Access, Characteristic, CharacteristicEventTypes, CharacteristicSetCallback } from "../Characteristic";
18✔
6
import { CameraController, CameraStreamingDelegate, ResourceRequestReason, StateChangeDelegate } from "../controller";
18✔
7
import type { CameraRTPStreamManagement } from "../definitions";
8
import { CharacteristicValue } from "../../types";
9
import { HAPStatus } from "../HAPServer";
10
import { Service } from "../Service";
18✔
11
import { HAPConnection, HAPConnectionEvent } from "../util/eventedhttp";
12
import { HapStatusError } from "../util/hapStatusError";
18✔
13
import { once } from "../util/once";
18✔
14
import * as tlv from "../util/tlv";
18✔
15
import * as uuid from "../util/uuid";
18✔
16
import RTPProxy from "./RTPProxy";
18✔
17

18
const debug = createDebug("HAP-NodeJS:Camera:RTPStreamManagement");
18✔
19
// ---------------------------------- TLV DEFINITIONS START ----------------------------------
20

21
const enum StreamingStatusTypes {
18✔
22
  STATUS = 0x01,
18✔
23
}
24

25
const enum StreamingStatus {
18✔
26
  AVAILABLE = 0x00,
18✔
27
  IN_USE = 0x01, // Session is marked IN_USE after the first setup request
18✔
28
  UNAVAILABLE = 0x02, // other reasons
18✔
29
}
30

31
// ----------
32

33
const enum SupportedVideoStreamConfigurationTypes {
18✔
34
  VIDEO_CODEC_CONFIGURATION = 0x01,
18✔
35
}
36

37
const enum VideoCodecConfigurationTypes {
18✔
38
  CODEC_TYPE = 0x01,
18✔
39
  CODEC_PARAMETERS = 0x02,
18✔
40
  ATTRIBUTES = 0x03,
18✔
41
}
42

43
const enum VideoCodecParametersTypes {
18✔
44
  PROFILE_ID = 0x01,
18✔
45
  LEVEL = 0x02,
18✔
46
  PACKETIZATION_MODE = 0x03,
18✔
47
  CVO_ENABLED = 0x04,
18✔
48
  CVO_ID = 0x05, // ID for CVO RTP extension, value in range from 1 to 14
18✔
49
}
50

51
const enum VideoAttributesTypes {
18✔
52
  IMAGE_WIDTH = 0x01,
18✔
53
  IMAGE_HEIGHT = 0x02,
18✔
54
  FRAME_RATE = 0x03
18✔
55
}
56

57
/**
58
 * @group Camera
59
 */
60
export const enum VideoCodecType {
18✔
61
  H264 = 0x00,
18✔
62
  // while the namespace is already reserved for H265 it isn't currently supported.
63
  // H265 = 0x01,
64
}
65

66
/**
67
 * @group Camera
68
 */
69
export const enum H264Profile {
18✔
70
  BASELINE = 0x00,
18✔
71
  MAIN = 0x01,
18✔
72
  HIGH = 0x02,
18✔
73
}
74

75
/**
76
 * @group Camera
77
 */
78
export const enum H264Level {
18✔
79
  LEVEL3_1 = 0x00,
18✔
80
  LEVEL3_2 = 0x01,
18✔
81
  LEVEL4_0 = 0x02,
18✔
82
}
83

84
/**
85
 * @group Camera
86
 */
87
export const enum VideoCodecPacketizationMode {
18✔
88
  NON_INTERLEAVED = 0x00
18✔
89
}
90

91
const enum VideoCodecCVO { // Coordination of Video Orientation
18✔
92
  UNSUPPORTED = 0x00,
18✔
93
  SUPPORTED = 0x01
18✔
94
}
95

96
// ----------
97

98
const enum SupportedAudioStreamConfigurationTypes {
18✔
99
  AUDIO_CODEC_CONFIGURATION = 0x01,
18✔
100
  COMFORT_NOISE_SUPPORT = 0x02,
18✔
101
}
102

103
const enum AudioCodecConfigurationTypes {
18✔
104
  CODEC_TYPE = 0x01,
18✔
105
  CODEC_PARAMETERS = 0x02,
18✔
106
}
107

108
const enum AudioCodecTypes { // only really by HAP supported codecs are AAC-ELD and OPUS
18✔
109
  PCMU = 0x00,
18✔
110
  PCMA = 0x01,
18✔
111
  AAC_ELD = 0x02,
18✔
112
  OPUS = 0x03,
18✔
113
  MSBC = 0x04, // mSBC is a bluetooth codec (lol)
18✔
114
  AMR = 0x05,
18✔
115
  AMR_WB = 0x06,
18✔
116
}
117

118
const enum AudioCodecParametersTypes {
18✔
119
  CHANNEL = 0x01,
18✔
120
  BIT_RATE = 0x02,
18✔
121
  SAMPLE_RATE = 0x03,
18✔
122
  PACKET_TIME = 0x04 // only present in selected audio codec parameters tlv
18✔
123
}
124

125
/**
126
 * @group Camera
127
 */
128
export const enum AudioBitrate {
18✔
129
  VARIABLE = 0x00,
18✔
130
  CONSTANT = 0x01
18✔
131
}
132

133
/**
134
 * @group Camera
135
 */
136
export const enum AudioSamplerate {
18✔
137
  KHZ_8 = 0x00,
18✔
138
  KHZ_16 = 0x01,
18✔
139
  KHZ_24 = 0x02
18✔
140
  // 3, 4, 5 are theoretically defined, but no idea to what kHz value they correspond to
141
  // probably KHZ_32, KHZ_44_1, KHZ_48 (as supported by Secure Video recordings)
142
}
143

144
// ----------
145

146
const enum SupportedRTPConfigurationTypes {
18✔
147
  SRTP_CRYPTO_SUITE = 0x02,
18✔
148
}
149

150
/**
151
 * @group Camera
152
 */
153
export const enum SRTPCryptoSuites { // public API
18✔
154
  AES_CM_128_HMAC_SHA1_80 = 0x00,
18✔
155
  AES_CM_256_HMAC_SHA1_80 = 0x01,
18✔
156
  NONE = 0x02
18✔
157
}
158

159

160
// ----------
161

162

163
const enum SetupEndpointsTypes {
18✔
164
  SESSION_ID = 0x01,
18✔
165
  CONTROLLER_ADDRESS = 0x03,
18✔
166
  VIDEO_SRTP_PARAMETERS = 0x04,
18✔
167
  AUDIO_SRTP_PARAMETERS = 0x05,
18✔
168
}
169

170
const enum AddressTypes {
18✔
171
  ADDRESS_VERSION = 0x01,
18✔
172
  ADDRESS = 0x02,
18✔
173
  VIDEO_RTP_PORT = 0x03,
18✔
174
  AUDIO_RTP_PORT = 0x04,
18✔
175
}
176

177
const enum IPAddressVersion {
18✔
178
  IPV4 = 0x00,
18✔
179
  IPV6 = 0x01
18✔
180
}
181

182

183
const enum SRTPParametersTypes {
18✔
184
  SRTP_CRYPTO_SUITE = 0x01,
18✔
185
  MASTER_KEY = 0x02, // 16 bytes for AES_CM_128_HMAC_SHA1_80; 32 bytes for AES_256_CM_HMAC_SHA1_80
18✔
186
  MASTER_SALT = 0x03 // 14 bytes
18✔
187
}
188

189
const enum SetupEndpointsResponseTypes {
18✔
190
  SESSION_ID = 0x01,
18✔
191
  STATUS = 0x02,
18✔
192
  ACCESSORY_ADDRESS = 0x03,
18✔
193
  VIDEO_SRTP_PARAMETERS = 0x04,
18✔
194
  AUDIO_SRTP_PARAMETERS = 0x05,
18✔
195
  VIDEO_SSRC = 0x06,
18✔
196
  AUDIO_SSRC = 0x07,
18✔
197
}
198

199
const enum SetupEndpointsStatus {
18✔
200
  SUCCESS = 0x00,
18✔
201
  BUSY = 0x01,
18✔
202
  ERROR = 0x02
18✔
203
}
204

205

206
// ----------
207

208

209
const enum SelectedRTPStreamConfigurationTypes {
18✔
210
  SESSION_CONTROL = 0x01,
18✔
211
  SELECTED_VIDEO_PARAMETERS = 0x02,
18✔
212
  SELECTED_AUDIO_PARAMETERS = 0x03
18✔
213
}
214

215
const enum SessionControlTypes {
18✔
216
  SESSION_IDENTIFIER = 0x01, // uuid, 16 bytes
18✔
217
  COMMAND = 0x02,
18✔
218
}
219

220
enum SessionControlCommand {
18✔
221
  END_SESSION = 0x00,
18✔
222
  START_SESSION = 0x01,
18✔
223
  SUSPEND_SESSION = 0x02,
18✔
224
  RESUME_SESSION = 0x03,
18✔
225
  RECONFIGURE_SESSION = 0x04,
18✔
226
}
227

228
const enum SelectedVideoParametersTypes {
18✔
229
  CODEC_TYPE = 0x01,
18✔
230
  CODEC_PARAMETERS = 0x02,
18✔
231
  ATTRIBUTES = 0x03,
18✔
232
  RTP_PARAMETERS = 0x04,
18✔
233
}
234

235
const enum VideoRTPParametersTypes {
18✔
236
  PAYLOAD_TYPE = 0x01,
18✔
237
  SYNCHRONIZATION_SOURCE = 0x02,
18✔
238
  MAX_BIT_RATE = 0x03,
18✔
239
  MIN_RTCP_INTERVAL = 0x04, // minimum RTCP interval in seconds
18✔
240
  MAX_MTU = 0x05, // only there if value is not default value; default values: ipv4 1378; ipv6 1228 bytes
18✔
241
}
242

243
const enum SelectedAudioParametersTypes {
18✔
244
  CODEC_TYPE = 0x01,
18✔
245
  CODEC_PARAMETERS = 0x02,
18✔
246
  RTP_PARAMETERS = 0x03,
18✔
247
  COMFORT_NOISE = 0x04,
18✔
248
}
249

250
const enum AudioRTPParametersTypes {
18✔
251
  PAYLOAD_TYPE = 0x01,
18✔
252
  SYNCHRONIZATION_SOURCE = 0x02,
18✔
253
  MAX_BIT_RATE = 0x03,
18✔
254
  MIN_RTCP_INTERVAL = 0x04, // minimum RTCP interval in seconds
18✔
255
  COMFORT_NOISE_PAYLOAD_TYPE = 0x06
18✔
256
}
257

258
// ---------------------------------- TLV DEFINITIONS END ------------------------------------
259

260
/**
261
 * @group Camera
262
 */
263
export type CameraStreamingOptions = CameraStreamingOptionsBase & (CameraStreamingOptionsLegacySRTP | CameraStreamingOptionsSupportedCryptoSuites)
264
/**
265
 * @group Camera
266
 */
267
export interface CameraStreamingOptionsBase {
268
  proxy?: boolean; // default false
269
  disable_audio_proxy?: boolean; // default false; If proxy = true, you can opt out audio proxy via this
270

271
  video: VideoStreamingOptions;
272
  /**
273
   * "audio" is optional and only needs to be declared if audio streaming is supported.
274
   * If defined the Microphone service will be added and Microphone volume control will be made available.
275
   * If not defined hap-nodejs will expose a default codec in order for the video stream to work
276
   */
277
  audio?: AudioStreamingOptions;
278
}
279

280
/**
281
 * @group Camera
282
 */
283
export interface CameraStreamingOptionsLegacySRTP {
284
  srtp: boolean; // a value of true indicates support of AES_CM_128_HMAC_SHA1_80
285
}
286

287
/**
288
 * @group Camera
289
 */
290
export interface CameraStreamingOptionsSupportedCryptoSuites {
291
  supportedCryptoSuites: SRTPCryptoSuites[], // Suite NONE should only be used for testing and will probably be never selected by iOS!
292
}
293

294
// eslint-disable-next-line @typescript-eslint/no-explicit-any
295
function isLegacySRTPOptions(options: any): options is CameraStreamingOptionsLegacySRTP {
296
  return "srtp" in options;
432✔
297
}
298

299
/**
300
 * @group Camera
301
 */
302
export type VideoStreamingOptions = {
303
  codec: H264CodecParameters,
304
  resolutions: Resolution[],
305
  cvoId?: number,
306
}
307

308
/**
309
 * @group Camera
310
 */
311
export interface H264CodecParameters {
312
  levels: H264Level[],
313
  profiles: H264Profile[],
314
}
315

316
/**
317
 * @group Camera
318
 */
319
export type Resolution = [number, number, number]; // width, height, framerate
320

321
/**
322
 * @group Camera
323
 */
324
export type AudioStreamingOptions = {
325
  codecs: AudioStreamingCodec[],
326
  twoWayAudio?: boolean, // default false, indicates support of 2way audio (will add the Speaker service and Speaker volume control)
327
  comfort_noise?: boolean, // default false
328
}
329

330
/**
331
 * @group Camera
332
 */
333
export type AudioStreamingCodec = {
334
  type: AudioStreamingCodecType | string, // string type for backwards compatibility
335
  audioChannels?: number, // default 1
336
  bitrate?: AudioBitrate, // default VARIABLE, AAC-ELD or OPUS MUST support VARIABLE bitrate
337
  samplerate: AudioStreamingSamplerate[] | AudioStreamingSamplerate, // OPUS or AAC-ELD must support samplerate at 16k and 25k
338
}
339

340
/**
341
 * @group Camera
342
 */
343
export const enum AudioStreamingCodecType { // codecs as defined by the HAP spec; only AAC-ELD and OPUS seem to work
18✔
344
  PCMU = "PCMU",
18✔
345
  PCMA = "PCMA",
18✔
346
  AAC_ELD = "AAC-eld",
18✔
347
  OPUS = "OPUS",
18✔
348
  MSBC = "mSBC",
18✔
349
  AMR = "AMR",
18✔
350
  AMR_WB = "AMR-WB",
18✔
351
}
352

353
/**
354
 * @group Camera
355
 */
356
export const enum AudioStreamingSamplerate {
18✔
357
  KHZ_8 = 8,
18✔
358
  KHZ_16 = 16,
18✔
359
  KHZ_24 = 24,
18✔
360
}
361

362

363
/**
364
 * @group Camera
365
 */
366
export type StreamSessionIdentifier = string; // uuid provided by HAP to identify a streaming session
367

368
/**
369
 * @group Camera
370
 */
371
export type SnapshotRequest = {
372
  height: number;
373
  width: number;
374
  /**
375
   * An optional {@link ResourceRequestReason}. The client decides if it wants to send this value. It is typically
376
   * only sent in the context of HomeKit Secure Video Cameras.
377
   * This value might be used by a `CameraStreamingDelegate` for informational purposes.
378
   * When `handleSnapshotRequest` is called, it is already checked if the respective reason is allowed in the current camera configuration.
379
   */
380
  reason?: ResourceRequestReason
381
}
382

383
/**
384
 * @group Camera
385
 */
386
export type PrepareStreamRequest = {
387
  sessionID: StreamSessionIdentifier,
388
  sourceAddress: string,
389
  targetAddress: string,
390
  addressVersion: "ipv4" | "ipv6",
391
  audio: Source,
392
  video: Source,
393
}
394

395
/**
396
 * @group Camera
397
 */
398
export type Source = {
399
  port: number,
400

401
  srtpCryptoSuite: SRTPCryptoSuites, // if cryptoSuite is NONE, key and salt are both zero-length
402
  srtp_key: Buffer,
403
  srtp_salt: Buffer,
404

405
  proxy_rtp?: number,
406
  proxy_rtcp?: number,
407
};
408

409
/**
410
 * @group Camera
411
 */
412
export type PrepareStreamResponse = {
413
  /**
414
   * Any value set to this optional property will overwrite the automatically determined local address,
415
   * which is sent as RTP endpoint to the iOS device.
416
   */
417
  addressOverride?: string;
418
  // video should be instanceOf ProxiedSourceResponse if proxy is required
419
  video: SourceResponse | ProxiedSourceResponse;
420
  // needs to be only supplied if audio is required; audio should be instanceOf ProxiedSourceResponse if proxy is required and audio proxy is not disabled
421
  audio?: SourceResponse | ProxiedSourceResponse;
422
}
423

424
/**
425
 * @group Camera
426
 */
427
export interface SourceResponse {
428
  port: number, // RTP/RTCP port of streaming server
429
  ssrc: number, // synchronization source of the stream
430

431
  srtp_key?: Buffer, // SRTP Key. Required if SRTP is used for the current stream
432
  srtp_salt?: Buffer, // SRTP Salt. Required if SRTP is used for the current stream
433
}
434

435
/**
436
 * @group Camera
437
 */
438
export interface ProxiedSourceResponse {
439
  proxy_pt: number, // Payload Type of input stream
440
  proxy_server_address: string, // IP address of RTP server
441
  proxy_server_rtp: number, // RTP port
442
  proxy_server_rtcp: number, // RTCP port
443
}
444

445
/**
446
 * @group Camera
447
 */
448
export const enum StreamRequestTypes {
18✔
449
  RECONFIGURE = "reconfigure",
18✔
450
  START = "start",
18✔
451
  STOP = "stop",
18✔
452
}
453

454
/**
455
 * @group Camera
456
 */
457
export type StreamingRequest = StartStreamRequest | ReconfigureStreamRequest | StopStreamRequest;
458

459
/**
460
 * @group Camera
461
 */
462
export type StartStreamRequest = {
463
  sessionID: StreamSessionIdentifier,
464
  type: StreamRequestTypes.START,
465
  video: VideoInfo,
466
  audio: AudioInfo,
467
}
468

469
/**
470
 * @group Camera
471
 */
472
export type ReconfigureStreamRequest = {
473
  sessionID: StreamSessionIdentifier,
474
  type: StreamRequestTypes.RECONFIGURE,
475
  video: ReconfiguredVideoInfo,
476
}
477

478
/**
479
 * @group Camera
480
 */
481
export type StopStreamRequest = {
482
  sessionID: StreamSessionIdentifier,
483
  type: StreamRequestTypes.STOP,
484
}
485

486
/**
487
 * @group Camera
488
 */
489
export type AudioInfo = {
490
  codec: AudioStreamingCodecType, // block size for AAC-ELD must be 480 samples
491

492
  channel: number,
493
  bit_rate: number,
494
  sample_rate: AudioStreamingSamplerate, // 8, 16, 24
495
  packet_time: number, // rtp packet time: length of time in ms represented by the media in a packet (20ms, 30ms, 40ms, 60ms)
496

497
  pt: number, // payloadType, typically 110
498
  ssrc: number, // synchronisation source
499
  max_bit_rate: number,
500
  rtcp_interval: number, // minimum rtcp interval in seconds (floating point number), pretty much always 0.5
501
  comfort_pt: number, // comfortNoise payloadType, 13
502

503
  comfortNoiseEnabled: boolean,
504
};
505

506
/**
507
 * @group Camera
508
 */
509
export type VideoInfo = {  // minimum keyframe interval is about 5 seconds
510
  codec: VideoCodecType;
511
  profile: H264Profile,
512
  level: H264Level,
513
  packetizationMode: VideoCodecPacketizationMode,
514
  cvoId?: number, // Coordination of Video Orientation, only supplied if enabled AND supported; ranges from 1 to 14
515

516
  width: number,
517
  height: number,
518
  fps: number,
519

520
  pt: number, // payloadType, 99 for h264
521
  ssrc: number, // synchronisation source
522
  max_bit_rate: number,
523
  rtcp_interval: number, // minimum rtcp interval in seconds (floating point number), pretty much always 0.5 (standard says a rang from 0.5 to 1.5)
524
  mtu: number, // maximum transmissions unit, default values: ipv4: 1378 bytes; ipv6: 1228 bytes
525
};
526

527
/**
528
 * @group Camera
529
 */
530
export type ReconfiguredVideoInfo = {
531
  width: number,
532
  height: number,
533
  fps: number,
534

535
  max_bit_rate: number,
536
  rtcp_interval: number, // minimum rtcp interval in seconds (floating point number)
537
}
538

539
/**
540
 * @group Camera
541
 */
542
export interface RTPStreamManagementState {
543
  id: number;
544
  active: boolean;
545
}
546

547
/**
548
 * @group Camera
549
 */
550
export class RTPStreamManagement {
18✔
551
  private readonly id: number;
552
  private readonly delegate: CameraStreamingDelegate;
553
  readonly service: CameraRTPStreamManagement;
554

555
  private stateChangeDelegate?: StateChangeDelegate;
556

557
  requireProxy: boolean;
558
  disableAudioProxy: boolean;
559
  supportedCryptoSuites: SRTPCryptoSuites[];
560
  videoOnly = false;
432✔
561

562
  readonly supportedRTPConfiguration: string;
563
  readonly supportedVideoStreamConfiguration: string;
564
  readonly supportedAudioStreamConfiguration: string;
565

566
  private activeConnection?: HAPConnection;
567
  private readonly activeConnectionClosedListener: (callback?: CharacteristicSetCallback) => void;
568
  sessionIdentifier?: StreamSessionIdentifier = undefined;
432✔
569
  /**
570
   * @private private API
571
   */
572
  streamStatus: StreamingStatus = StreamingStatus.AVAILABLE; // use _updateStreamStatus to update this property
432✔
573
  private ipVersion?: "ipv4" | "ipv6"; // ip version for the current session
574

575
  selectedConfiguration = ""; // base64 representation of the currently selected configuration
432✔
576
  setupEndpointsResponse = ""; // response of the SetupEndpoints Characteristic
432✔
577

578
  /**
579
   * @private deprecated API
580
   */
581
  audioProxy?: RTPProxy;
582
  /**
583
   * @private deprecated API
584
   */
585
  videoProxy?: RTPProxy;
586

587
  /**
588
   * A RTPStreamManagement is considered disabled if `HomeKitCameraActive` is set to false.
589
   * We use a closure based approach to retrieve the value of this characteristic.
590
   * The characteristic is managed by the RecordingManagement.
591
   */
592
  private readonly disabledThroughOperatingMode?: () => boolean;
593

594
  constructor(
595
    id: number,
596
    options: CameraStreamingOptions,
597
    delegate: CameraStreamingDelegate,
598
    service?: CameraRTPStreamManagement,
599
    disabledThroughOperatingMode?: () => boolean,
600
  ) {
601
    this.id = id;
432✔
602
    this.delegate = delegate;
432✔
603

604
    this.requireProxy = options.proxy || false;
432✔
605
    this.disableAudioProxy = options.disable_audio_proxy || false;
432✔
606
    if (isLegacySRTPOptions(options)) {
432!
607
      this.supportedCryptoSuites = [options.srtp? SRTPCryptoSuites.AES_CM_128_HMAC_SHA1_80: SRTPCryptoSuites.NONE];
×
608
    } else {
609
      this.supportedCryptoSuites = options.supportedCryptoSuites;
432✔
610
    }
611

612
    if (this.supportedCryptoSuites.length === 0) {
432!
613
      this.supportedCryptoSuites.push(SRTPCryptoSuites.NONE);
×
614
    }
615

616
    if (!options.video) {
432!
617
      throw new Error("Video parameters cannot be undefined in options");
×
618
    }
619

620
    this.supportedRTPConfiguration = RTPStreamManagement._supportedRTPConfiguration(this.supportedCryptoSuites);
432✔
621
    this.supportedVideoStreamConfiguration = RTPStreamManagement._supportedVideoStreamConfiguration(options.video);
432✔
622
    this.supportedAudioStreamConfiguration = this._supportedAudioStreamConfiguration(options.audio);
432✔
623

624
    this.activeConnectionClosedListener = this._handleStopStream.bind(this);
432✔
625

626
    this.service = service || this.constructService(id);
432✔
627
    this.setupServiceHandlers();
432✔
628

629
    this.resetSetupEndpointsResponse();
432✔
630
    this.resetSelectedStreamConfiguration();
432✔
631

632
    this.disabledThroughOperatingMode = disabledThroughOperatingMode;
432✔
633
  }
634

635
  public forceStop(): void {
636
    this.handleSessionClosed();
×
637
  }
638

639
  getService(): CameraRTPStreamManagement {
640
    return this.service;
804✔
641
  }
642

643
  handleFactoryReset(): void {
644
    this.resetSelectedStreamConfiguration();
48✔
645
    this.resetSetupEndpointsResponse();
48✔
646

647
    this.service.updateCharacteristic(Characteristic.Active, true);
48✔
648
    // on a factory reset the assumption is that all connections were already terminated and thus "handleStopStream" was already called
649
  }
650

651
  public destroy(): void {
652
    if (this.activeConnection) {
12!
653
      this._handleStopStream();
×
654
    }
655
  }
656

657
  private constructService(id: number): CameraRTPStreamManagement {
658
    const managementService = new Service.CameraRTPStreamManagement("", id.toString());
432✔
659

660
    // this service is required only when recording is enabled. We don't really have access to this info here,
661
    // so we just add the characteristic. Doesn't really hurt.
662
    managementService.setCharacteristic(Characteristic.Active, true);
432✔
663

664
    return managementService;
432✔
665
  }
666

667
  private setupServiceHandlers() {
668
    if (!this.service.testCharacteristic(Characteristic.Active)) {
432!
669
      // the active characteristic might not be present on some older configurations.
670
      this.service.setCharacteristic(Characteristic.Active, true);
×
671
    }
672
    this.service.getCharacteristic(Characteristic.Active)
432✔
673
      .on(CharacteristicEventTypes.CHANGE, () => this.stateChangeDelegate?.())
108✔
674
      .setProps({ adminOnlyAccess: [Access.WRITE] });
675

676
    // ensure that configurations are up-to-date and reflected in the characteristic values
677
    this.service.setCharacteristic(Characteristic.SupportedRTPConfiguration, this.supportedRTPConfiguration);
432✔
678
    this.service.setCharacteristic(Characteristic.SupportedVideoStreamConfiguration, this.supportedVideoStreamConfiguration);
432✔
679
    this.service.setCharacteristic(Characteristic.SupportedAudioStreamConfiguration, this.supportedAudioStreamConfiguration);
432✔
680

681
    this._updateStreamStatus(StreamingStatus.AVAILABLE); // reset streaming status to available
432✔
682
    this.service.setCharacteristic(Characteristic.SetupEndpoints, this.setupEndpointsResponse); // reset SetupEndpoints to default
432✔
683

684
    this.service.getCharacteristic(Characteristic.SelectedRTPStreamConfiguration)!
432✔
685
      .on(CharacteristicEventTypes.GET, callback => {
686
        if (this.streamingIsDisabled()) {
×
687
          callback(null, tlv.encode(
×
688
            SelectedRTPStreamConfigurationTypes.SESSION_CONTROL, tlv.encode(
689
              SessionControlTypes.COMMAND, SessionControlCommand.SUSPEND_SESSION,
690
            ),
691
          ).toString("base64"));
692
          return;
×
693
        }
694

695
        callback(null, this.selectedConfiguration);
×
696
      })
697
      .on(CharacteristicEventTypes.SET, this._handleSelectedStreamConfigurationWrite.bind(this));
698

699
    this.service.getCharacteristic(Characteristic.SetupEndpoints)!
432✔
700
      .on(CharacteristicEventTypes.GET, callback => {
701
        if (this.streamingIsDisabled()) {
×
702
          callback(null, tlv.encode(
×
703
            SetupEndpointsResponseTypes.STATUS, SetupEndpointsStatus.ERROR,
704
          ).toString("base64"));
705
          return;
×
706
        }
707

708
        callback(null, this.setupEndpointsResponse);
×
709
      })
710
      .on(CharacteristicEventTypes.SET, (value, callback, context, connection) => {
711
        if (!connection) {
×
712
          debug("Set event handler for SetupEndpoints cannot be called from plugin. Connection undefined!");
×
713
          callback(HAPStatus.INVALID_VALUE_IN_REQUEST);
×
714
          return;
×
715
        }
716
        this.handleSetupEndpoints(value, callback, connection);
×
717
      });
718
  }
719

720
  private handleSessionClosed(): void { // called when the streaming was ended or aborted and needs to be cleaned up
721
    this.resetSelectedStreamConfiguration();
×
722
    this.resetSetupEndpointsResponse();
×
723

724
    if (this.activeConnection) {
×
725
      this.activeConnection.removeListener(HAPConnectionEvent.CLOSED, this.activeConnectionClosedListener);
×
726
      this.activeConnection.setMaxListeners(this.activeConnection.getMaxListeners() - 1);
×
727
      this.activeConnection = undefined;
×
728
    }
729

730
    this._updateStreamStatus(StreamingStatus.AVAILABLE);
×
731
    this.sessionIdentifier = undefined;
×
732
    this.ipVersion = undefined;
×
733

734
    if (this.videoProxy) {
×
735
      this.videoProxy.destroy();
×
736
      this.videoProxy = undefined;
×
737
    }
738
    if (this.audioProxy) {
×
739
      this.audioProxy.destroy();
×
740
      this.audioProxy = undefined;
×
741
    }
742
  }
743

744
  private streamingIsDisabled(callback?: CharacteristicSetCallback): boolean {
745
    if (!this.service.getCharacteristic(Characteristic.Active).value) {
×
NEW
746
      if (typeof callback === "function") {
×
NEW
747
        callback(new HapStatusError(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE));
×
748
      }
749
      return true;
×
750
    }
751

752
    if (this.disabledThroughOperatingMode?.()) {
×
NEW
753
      if (typeof callback === "function") {
×
NEW
754
        callback(new HapStatusError(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE));
×
755
      }
756
      return true;
×
757
    }
758

759
    return false;
×
760
  }
761

762
  private _handleSelectedStreamConfigurationWrite(value: CharacteristicValue, callback: CharacteristicSetCallback): void {
763
    if (this.streamingIsDisabled(callback)) {
×
764
      return;
×
765
    }
766

767
    const data = Buffer.from(value as string, "base64");
×
768
    const objects = tlv.decode(data);
×
769

770
    const sessionControl = tlv.decode(objects[SelectedRTPStreamConfigurationTypes.SESSION_CONTROL]);
×
771
    const sessionIdentifier = uuid.unparse(sessionControl[SessionControlTypes.SESSION_IDENTIFIER]);
×
772
    const requestType: SessionControlCommand = sessionControl[SessionControlTypes.COMMAND][0];
×
773

774
    if (sessionIdentifier !== this.sessionIdentifier) {
×
775
      debug(`Received unknown session Identifier with request to ${SessionControlCommand[requestType]}`);
×
776
      callback(HAPStatus.INVALID_VALUE_IN_REQUEST);
×
777
      return;
×
778
    }
779

780
    this.selectedConfiguration = value as string;
×
781

782
    // intercept the callback chain to check if an error occurred.
783
    const streamCallback: CharacteristicSetCallback = (error, writeResponse) => {
×
784
      callback(error, writeResponse); // does not support writeResponse, but how knows what comes in the future.
×
785
      if (error) {
×
786
        this.handleSessionClosed();
×
787
      }
788
    };
789

790
    switch (requestType) {
×
791
    case SessionControlCommand.START_SESSION: {
792
      const selectedVideoParameters = tlv.decode(objects[SelectedRTPStreamConfigurationTypes.SELECTED_VIDEO_PARAMETERS]);
×
793
      const selectedAudioParameters = tlv.decode(objects[SelectedRTPStreamConfigurationTypes.SELECTED_AUDIO_PARAMETERS]);
×
794

795
      this._handleStartStream(selectedVideoParameters, selectedAudioParameters, streamCallback);
×
796
      break;
×
797
    }
798
    case SessionControlCommand.RECONFIGURE_SESSION: {
799
      const reconfiguredVideoParameters = tlv.decode(objects[SelectedRTPStreamConfigurationTypes.SELECTED_VIDEO_PARAMETERS]);
×
800

801
      this.handleReconfigureStream(reconfiguredVideoParameters, streamCallback);
×
802
      break;
×
803
    }
804
    case SessionControlCommand.END_SESSION:
805
      this._handleStopStream(streamCallback);
×
806
      break;
×
807
    case SessionControlCommand.RESUME_SESSION:
808
    case SessionControlCommand.SUSPEND_SESSION:
809
    default:
810
      debug(`Unhandled request type ${SessionControlCommand[requestType]}`);
×
811
      callback(HAPStatus.INVALID_VALUE_IN_REQUEST);
×
812
      return;
×
813
    }
814
  }
815

816
  private _handleStartStream(
817
    videoConfiguration: Record<number, Buffer>,
818
    audioConfiguration: Record<number, Buffer>,
819
    callback: CharacteristicSetCallback,
820
  ): void {
821
    // selected video configuration
822
    // noinspection JSUnusedLocalSymbols
823
    const videoCodec = videoConfiguration[SelectedVideoParametersTypes.CODEC_TYPE]; // always 0x00 for h264
×
824
    const videoParametersTLV = videoConfiguration[SelectedVideoParametersTypes.CODEC_PARAMETERS];
×
825
    const videoAttributesTLV = videoConfiguration[SelectedVideoParametersTypes.ATTRIBUTES];
×
826
    const videoRTPParametersTLV = videoConfiguration[SelectedVideoParametersTypes.RTP_PARAMETERS];
×
827

828
    // video parameters
829
    const videoParameters = tlv.decode(videoParametersTLV);
×
830
    const h264Profile: H264Profile = videoParameters[VideoCodecParametersTypes.PROFILE_ID][0];
×
831
    const h264Level: H264Level = videoParameters[VideoCodecParametersTypes.LEVEL][0];
×
832
    const packetizationMode: VideoCodecPacketizationMode = videoParameters[VideoCodecParametersTypes.PACKETIZATION_MODE][0];
×
833
    const cvoEnabled = videoParameters[VideoCodecParametersTypes.CVO_ENABLED];
×
834
    let cvoId: number | undefined = undefined;
×
835
    if (cvoEnabled && cvoEnabled[0] === VideoCodecCVO.SUPPORTED) {
×
836
      cvoId = videoParameters[VideoCodecParametersTypes.CVO_ID].readUInt8(0);
×
837
    }
838

839
    // video attributes
840
    const videoAttributes = tlv.decode(videoAttributesTLV);
×
841
    const width = videoAttributes[VideoAttributesTypes.IMAGE_WIDTH].readUInt16LE(0);
×
842
    const height = videoAttributes[VideoAttributesTypes.IMAGE_HEIGHT].readUInt16LE(0);
×
843
    const frameRate = videoAttributes[VideoAttributesTypes.FRAME_RATE].readUInt8(0);
×
844

845
    // video rtp parameters
846
    const videoRTPParameters = tlv.decode(videoRTPParametersTLV);
×
847
    const videoPayloadType = videoRTPParameters[VideoRTPParametersTypes.PAYLOAD_TYPE].readUInt8(0); // 99
×
848
    const videoSSRC = videoRTPParameters[VideoRTPParametersTypes.SYNCHRONIZATION_SOURCE].readUInt32LE(0);
×
849
    const videoMaximumBitrate = videoRTPParameters[VideoRTPParametersTypes.MAX_BIT_RATE].readUInt16LE(0);
×
850
    const videoRTCPInterval = videoRTPParameters[VideoRTPParametersTypes.MIN_RTCP_INTERVAL].readFloatLE(0);
×
851
    let maxMTU = this.ipVersion === "ipv6"? 1228: 1378; // default values ipv4: 1378 bytes; ipv6: 1228 bytes
×
852
    if (videoRTPParameters[VideoRTPParametersTypes.MAX_MTU]) {
×
853
      maxMTU = videoRTPParameters[VideoRTPParametersTypes.MAX_MTU].readUInt16LE(0);
×
854
    }
855

856

857
    // selected audio configuration
858
    const audioCodec: AudioCodecTypes = audioConfiguration[SelectedAudioParametersTypes.CODEC_TYPE][0];
×
859
    const audioParametersTLV = audioConfiguration[SelectedAudioParametersTypes.CODEC_PARAMETERS];
×
860
    const audioRTPParametersTLV = audioConfiguration[SelectedAudioParametersTypes.RTP_PARAMETERS];
×
861
    const comfortNoise = !!audioConfiguration[SelectedAudioParametersTypes.COMFORT_NOISE].readUInt8(0);
×
862

863
    // audio parameters
864
    const audioParameters = tlv.decode(audioParametersTLV);
×
865
    const channels = audioParameters[AudioCodecParametersTypes.CHANNEL][0];
×
866
    const audioBitrate: AudioBitrate = audioParameters[AudioCodecParametersTypes.BIT_RATE][0];
×
867
    const samplerate: AudioSamplerate = audioParameters[AudioCodecParametersTypes.SAMPLE_RATE][0];
×
868
    const rtpPacketTime = audioParameters[AudioCodecParametersTypes.PACKET_TIME].readUInt8(0);
×
869

870
    // audio rtp parameters
871
    const audioRTPParameters = tlv.decode(audioRTPParametersTLV);
×
872
    const audioPayloadType = audioRTPParameters[AudioRTPParametersTypes.PAYLOAD_TYPE].readUInt8(0); // 110
×
873
    const audioSSRC = audioRTPParameters[AudioRTPParametersTypes.SYNCHRONIZATION_SOURCE].readUInt32LE(0);
×
874
    const audioMaximumBitrate = audioRTPParameters[AudioRTPParametersTypes.MAX_BIT_RATE].readUInt16LE(0);
×
875
    const audioRTCPInterval = audioRTPParameters[AudioRTPParametersTypes.MIN_RTCP_INTERVAL].readFloatLE(0);
×
876
    const comfortNoisePayloadType = audioRTPParameters[AudioRTPParametersTypes.COMFORT_NOISE_PAYLOAD_TYPE].readUInt8(0); // 13
×
877

878
    if (this.requireProxy) {
×
879
      this.videoProxy!.setOutgoingPayloadType(videoPayloadType);
×
880
      if (!this.disableAudioProxy) {
×
881
        this.audioProxy!.setOutgoingPayloadType(audioPayloadType);
×
882
      }
883
    }
884

885

886
    const videoInfo: VideoInfo = {
×
887
      codec: videoCodec.readUInt8(0),
888
      profile: h264Profile,
889
      level: h264Level,
890
      packetizationMode: packetizationMode,
891
      cvoId: cvoId,
892

893
      width: width,
894
      height: height,
895
      fps: frameRate,
896

897
      pt: videoPayloadType,
898
      ssrc: videoSSRC,
899
      max_bit_rate: videoMaximumBitrate,
900
      rtcp_interval: videoRTCPInterval,
901
      mtu: maxMTU,
902
    };
903

904
    let audioCodecName: AudioStreamingCodecType;
905
    let samplerateNum: AudioStreamingSamplerate;
906

907
    switch (audioCodec) {
×
908
    case AudioCodecTypes.PCMU:
909
      audioCodecName = AudioStreamingCodecType.PCMU;
×
910
      break;
×
911
    case AudioCodecTypes.PCMA:
912
      audioCodecName = AudioStreamingCodecType.PCMA;
×
913
      break;
×
914
    case AudioCodecTypes.AAC_ELD:
915
      audioCodecName = AudioStreamingCodecType.AAC_ELD;
×
916
      break;
×
917
    case AudioCodecTypes.OPUS:
918
      audioCodecName = AudioStreamingCodecType.OPUS;
×
919
      break;
×
920
    case AudioCodecTypes.MSBC:
921
      audioCodecName = AudioStreamingCodecType.MSBC;
×
922
      break;
×
923
    case AudioCodecTypes.AMR:
924
      audioCodecName = AudioStreamingCodecType.AMR;
×
925
      break;
×
926
    case AudioCodecTypes.AMR_WB:
927
      audioCodecName = AudioStreamingCodecType.AMR_WB;
×
928
      break;
×
929
    default:
930
      throw new Error(`Encountered unknown selected audio codec ${audioCodec}`);
×
931
    }
932

933
    switch (samplerate) {
×
934
    case AudioSamplerate.KHZ_8:
935
      samplerateNum = 8;
×
936
      break;
×
937
    case AudioSamplerate.KHZ_16:
938
      samplerateNum = 16;
×
939
      break;
×
940
    case AudioSamplerate.KHZ_24:
941
      samplerateNum = 24;
×
942
      break;
×
943
    default:
944
      throw new Error(`Encountered unknown selected audio samplerate ${samplerate}`);
×
945
    }
946

947
    const audioInfo: AudioInfo = {
×
948
      codec: audioCodecName,
949

950
      channel: channels,
951
      bit_rate: audioBitrate,
952
      sample_rate: samplerateNum,
953
      packet_time: rtpPacketTime,
954

955
      pt: audioPayloadType,
956
      ssrc: audioSSRC,
957
      max_bit_rate: audioMaximumBitrate,
958
      rtcp_interval: audioRTCPInterval,
959
      comfort_pt: comfortNoisePayloadType,
960

961
      comfortNoiseEnabled: comfortNoise,
962
    };
963

964
    const request: StartStreamRequest = {
×
965
      sessionID: this.sessionIdentifier!,
966
      type: StreamRequestTypes.START,
967
      video: videoInfo,
968
      audio: audioInfo,
969
    };
970

971
    this.delegate.handleStreamRequest(request, error => callback(error));
×
972
  }
973

974
  private handleReconfigureStream(videoConfiguration: Record<number, Buffer>, callback: CharacteristicSetCallback): void {
975
    // selected video configuration
976
    const videoAttributesTLV = videoConfiguration[SelectedVideoParametersTypes.ATTRIBUTES];
×
977
    const videoRTPParametersTLV = videoConfiguration[SelectedVideoParametersTypes.RTP_PARAMETERS];
×
978

979
    // video attributes
980
    const videoAttributes = tlv.decode(videoAttributesTLV);
×
981
    const width = videoAttributes[VideoAttributesTypes.IMAGE_WIDTH].readUInt16LE(0);
×
982
    const height = videoAttributes[VideoAttributesTypes.IMAGE_HEIGHT].readUInt16LE(0);
×
983
    const frameRate = videoAttributes[VideoAttributesTypes.FRAME_RATE].readUInt8(0);
×
984

985
    // video rtp parameters
986
    const videoRTPParameters = tlv.decode(videoRTPParametersTLV);
×
987
    const videoMaximumBitrate = videoRTPParameters[VideoRTPParametersTypes.MAX_BIT_RATE].readUInt16LE(0);
×
988
    // seems to be always zero, use default of 0.5
989
    const videoRTCPInterval = videoRTPParameters[VideoRTPParametersTypes.MIN_RTCP_INTERVAL].readFloatLE(0) || 0.5;
×
990

991
    const reconfiguredVideoInfo: ReconfiguredVideoInfo = {
×
992
      width: width,
993
      height: height,
994
      fps: frameRate,
995

996
      max_bit_rate: videoMaximumBitrate,
997
      rtcp_interval: videoRTCPInterval,
998
    };
999

1000
    const request: ReconfigureStreamRequest = {
×
1001
      sessionID: this.sessionIdentifier!,
1002
      type: StreamRequestTypes.RECONFIGURE,
1003
      video: reconfiguredVideoInfo,
1004
    };
1005

1006
    this.delegate.handleStreamRequest(request, error => callback(error));
×
1007
  }
1008

1009
  private _handleStopStream(callback?: CharacteristicSetCallback): void {
1010
    const request: StopStreamRequest = {
×
1011
      sessionID: this.sessionIdentifier!, // save sessionIdentifier before handleSessionClosed is called
1012
      type: StreamRequestTypes.STOP,
1013
    };
1014

1015
    this.handleSessionClosed();
×
1016

1017
    this.delegate.handleStreamRequest(request, error => callback? callback(error): undefined);
×
1018
  }
1019

1020
  private handleSetupEndpoints(value: CharacteristicValue, callback: CharacteristicSetCallback, connection: HAPConnection): void {
1021
    if (this.streamingIsDisabled(callback)) {
×
1022
      return;
×
1023
    }
1024

1025
    const data = Buffer.from(value as string, "base64");
×
1026
    const objects = tlv.decode(data);
×
1027

1028
    const sessionIdentifier = uuid.unparse(objects[SetupEndpointsTypes.SESSION_ID]);
×
1029

1030
    if (this.streamStatus !== StreamingStatus.AVAILABLE) {
×
1031
      this.setupEndpointsResponse = tlv.encode(
×
1032
        SetupEndpointsResponseTypes.SESSION_ID, uuid.write(sessionIdentifier),
1033
        SetupEndpointsResponseTypes.STATUS, SetupEndpointsStatus.BUSY,
1034
      ).toString("base64");
1035
      callback();
×
1036
      return;
×
1037
    }
1038

1039
    assert(this.activeConnection == null,
×
1040
      "Found non-nil `activeConnection` when trying to setup streaming endpoints, even though streamStatus is reported to be AVAILABLE!");
1041

1042
    this.activeConnection = connection;
×
1043
    this.activeConnection.setMaxListeners(this.activeConnection.getMaxListeners() + 1);
×
1044
    this.activeConnection.on(HAPConnectionEvent.CLOSED, this.activeConnectionClosedListener);
×
1045

1046
    this.sessionIdentifier = sessionIdentifier;
×
1047
    this._updateStreamStatus(StreamingStatus.IN_USE);
×
1048

1049
    // Address
1050
    const targetAddressPayload = objects[SetupEndpointsTypes.CONTROLLER_ADDRESS];
×
1051
    const processedAddressInfo = tlv.decode(targetAddressPayload);
×
1052
    const addressVersion = processedAddressInfo[AddressTypes.ADDRESS_VERSION][0];
×
1053
    const controllerAddress = processedAddressInfo[AddressTypes.ADDRESS].toString("utf8");
×
1054
    const targetVideoPort = processedAddressInfo[AddressTypes.VIDEO_RTP_PORT].readUInt16LE(0);
×
1055
    const targetAudioPort = processedAddressInfo[AddressTypes.AUDIO_RTP_PORT].readUInt16LE(0);
×
1056

1057
    // Video SRTP Params
1058
    const videoSRTPPayload = objects[SetupEndpointsTypes.VIDEO_SRTP_PARAMETERS];
×
1059
    const processedVideoInfo = tlv.decode(videoSRTPPayload);
×
1060
    const videoCryptoSuite = processedVideoInfo[SRTPParametersTypes.SRTP_CRYPTO_SUITE][0];
×
1061
    const videoMasterKey = processedVideoInfo[SRTPParametersTypes.MASTER_KEY];
×
1062
    const videoMasterSalt = processedVideoInfo[SRTPParametersTypes.MASTER_SALT];
×
1063

1064
    // Audio SRTP Params
1065
    const audioSRTPPayload = objects[SetupEndpointsTypes.AUDIO_SRTP_PARAMETERS];
×
1066
    const processedAudioInfo = tlv.decode(audioSRTPPayload);
×
1067
    const audioCryptoSuite = processedAudioInfo[SRTPParametersTypes.SRTP_CRYPTO_SUITE][0];
×
1068
    const audioMasterKey = processedAudioInfo[SRTPParametersTypes.MASTER_KEY];
×
1069
    const audioMasterSalt = processedAudioInfo[SRTPParametersTypes.MASTER_SALT];
×
1070

1071
    debug(
×
1072
      "Session: ", sessionIdentifier,
1073
      "\nControllerAddress: ", controllerAddress,
1074
      "\nVideoPort: ", targetVideoPort,
1075
      "\nAudioPort: ", targetAudioPort,
1076
      "\nVideo Crypto: ", videoCryptoSuite,
1077
      "\nVideo Master Key: ", videoMasterKey,
1078
      "\nVideo Master Salt: ", videoMasterSalt,
1079
      "\nAudio Crypto: ", audioCryptoSuite,
1080
      "\nAudio Master Key: ", audioMasterKey,
1081
      "\nAudio Master Salt: ", audioMasterSalt,
1082
    );
1083

1084

1085
    const prepareRequest: PrepareStreamRequest = {
×
1086
      sessionID: sessionIdentifier,
1087
      sourceAddress: connection.localAddress,
1088
      targetAddress: controllerAddress,
1089
      addressVersion: addressVersion === IPAddressVersion.IPV6? "ipv6": "ipv4",
×
1090

1091
      video: { // if suite is NONE, keys and salts are zero-length
1092
        port: targetVideoPort,
1093

1094
        srtpCryptoSuite: videoCryptoSuite,
1095
        srtp_key: videoMasterKey,
1096
        srtp_salt: videoMasterSalt,
1097
      },
1098
      audio: {
1099
        port: targetAudioPort,
1100

1101
        srtpCryptoSuite: audioCryptoSuite,
1102
        srtp_key: audioMasterKey,
1103
        srtp_salt: audioMasterSalt,
1104
      },
1105
    };
1106

1107
    const promises: Promise<void>[] = [];
×
1108

1109
    if (this.requireProxy) {
×
1110
      prepareRequest.targetAddress = connection.getLocalAddress(addressVersion === IPAddressVersion.IPV6? "ipv6": "ipv4"); // ip versions must be the same
×
1111

1112
      this.videoProxy = new RTPProxy({
×
1113
        outgoingAddress: controllerAddress,
1114
        outgoingPort: targetVideoPort,
1115
        outgoingSSRC: crypto.randomBytes(4).readUInt32LE(0), // videoSSRC
1116
        disabled: false,
1117
      });
1118

1119
      promises.push(this.videoProxy.setup().then(() => {
×
1120
        prepareRequest.video.proxy_rtp = this.videoProxy!.incomingRTPPort();
×
1121
        prepareRequest.video.proxy_rtcp = this.videoProxy!.incomingRTCPPort();
×
1122
      }));
1123

1124
      if (!this.disableAudioProxy) {
×
1125
        this.audioProxy = new RTPProxy({
×
1126
          outgoingAddress: controllerAddress,
1127
          outgoingPort: targetAudioPort,
1128
          outgoingSSRC: crypto.randomBytes(4).readUInt32LE(0), // audioSSRC
1129
          disabled: this.videoOnly,
1130
        });
1131

1132
        promises.push(this.audioProxy.setup().then(() => {
×
1133
          prepareRequest.audio.proxy_rtp = this.audioProxy!.incomingRTPPort();
×
1134
          prepareRequest.audio.proxy_rtcp = this.audioProxy!.incomingRTCPPort();
×
1135
        }));
1136
      }
1137
    }
1138

1139
    Promise.all(promises).then(() => {
×
1140
      this.delegate.prepareStream(prepareRequest, once((error?: Error, response?: PrepareStreamResponse) => {
×
1141
        if (error || !response) {
×
1142
          debug(`PrepareStream request encountered an error: ${error? error.message: undefined}`);
×
1143
          this.setupEndpointsResponse = tlv.encode(
×
1144
            SetupEndpointsResponseTypes.SESSION_ID, uuid.write(sessionIdentifier),
1145
            SetupEndpointsResponseTypes.STATUS, SetupEndpointsStatus.ERROR,
1146
          ).toString("base64");
1147

1148
          this.handleSessionClosed();
×
1149
          callback(error);
×
1150
        } else {
1151
          this.generateSetupEndpointResponse(connection, sessionIdentifier, prepareRequest, response, callback);
×
1152
        }
1153
      }));
1154
    });
1155
  }
1156

1157
  private generateSetupEndpointResponse(
1158
    connection: HAPConnection,
1159
    identifier: StreamSessionIdentifier,
1160
    request: PrepareStreamRequest,
1161
    response: PrepareStreamResponse,
1162
    callback: CharacteristicSetCallback,
1163
  ): void {
1164
    let address: string;
1165
    let addressVersion = request.addressVersion;
×
1166

1167
    let videoPort: number;
1168
    let audioPort: number;
1169

1170
    let videoCryptoSuite: SRTPCryptoSuites;
1171
    let videoSRTPKey: Buffer;
1172
    let videoSRTPSalt: Buffer;
1173
    let audioCryptoSuite: SRTPCryptoSuites;
1174
    let audioSRTPKey: Buffer;
1175
    let audioSRTPSalt: Buffer;
1176

1177
    let videoSSRC: number;
1178
    let audioSSRC: number;
1179

1180
    if (!this.videoOnly && !response.audio) {
×
1181
      throw new Error("Audio was enabled but not supplied in PrepareStreamResponse!");
×
1182
    }
1183

1184
    // Provide default values if audio was not supplied
1185
    const audio: SourceResponse | ProxiedSourceResponse = response.audio || {
×
1186
      port: request.audio.port,
1187
      ssrc: CameraController.generateSynchronisationSource(),
1188
      srtp_key: request.audio.srtp_key,
1189
      srtp_salt: request.audio.srtp_salt,
1190
    };
1191

1192
    if (!this.requireProxy) {
×
1193
      const videoInfo = response.video as SourceResponse;
×
1194
      const audioInfo = audio as SourceResponse;
×
1195

1196
      if (response.addressOverride) {
×
1197
        addressVersion = net.isIPv4(response.addressOverride)? "ipv4": "ipv6";
×
1198
        address = response.addressOverride;
×
1199
      } else {
1200
        address = connection.getLocalAddress(addressVersion);
×
1201
      }
1202

1203
      if (request.addressVersion !== addressVersion) {
×
1204
        throw new Error(`Incoming and outgoing ip address versions must match! Expected ${request.addressVersion} but got ${addressVersion}`);
×
1205
      }
1206

1207
      videoPort = videoInfo.port;
×
1208
      audioPort = audioInfo.port;
×
1209

1210

1211
      if (request.video.srtpCryptoSuite !== SRTPCryptoSuites.NONE
×
1212
          && (videoInfo.srtp_key === undefined || videoInfo.srtp_salt === undefined)) {
1213
        throw new Error("SRTP was selected for the prepared video stream, but no 'srtp_key' or 'srtp_salt' was specified!");
×
1214
      }
1215
      if (request.audio.srtpCryptoSuite !== SRTPCryptoSuites.NONE
×
1216
          && (audioInfo.srtp_key === undefined || audioInfo.srtp_salt === undefined)) {
1217
        throw new Error("SRTP was selected for the prepared audio stream, but no 'srtp_key' or 'srtp_salt' was specified!");
×
1218
      }
1219

1220
      videoCryptoSuite = request.video.srtpCryptoSuite;
×
1221
      videoSRTPKey = videoInfo.srtp_key || Buffer.alloc(0); // key and salt are zero-length for cryptoSuite = NONE
×
1222
      videoSRTPSalt = videoInfo.srtp_salt || Buffer.alloc(0);
×
1223

1224
      audioCryptoSuite = request.audio.srtpCryptoSuite;
×
1225
      audioSRTPKey = audioInfo.srtp_key || Buffer.alloc(0); // key and salt are zero-length for cryptoSuite = NONE
×
1226
      audioSRTPSalt = audioInfo.srtp_salt || Buffer.alloc(0);
×
1227

1228

1229
      videoSSRC = videoInfo.ssrc;
×
1230
      audioSSRC = audioInfo.ssrc;
×
1231
    } else {
1232
      const videoInfo = response.video as ProxiedSourceResponse;
×
1233

1234
      address = connection.getLocalAddress(request.addressVersion);
×
1235

1236

1237
      videoCryptoSuite = SRTPCryptoSuites.NONE;
×
1238
      videoSRTPKey = Buffer.alloc(0);
×
1239
      videoSRTPSalt = Buffer.alloc(0);
×
1240

1241
      audioCryptoSuite = SRTPCryptoSuites.NONE;
×
1242
      audioSRTPKey = Buffer.alloc(0);
×
1243
      audioSRTPSalt = Buffer.alloc(0);
×
1244

1245

1246
      this.videoProxy!.setIncomingPayloadType(videoInfo.proxy_pt);
×
1247
      this.videoProxy!.setServerAddress(videoInfo.proxy_server_address);
×
1248
      this.videoProxy!.setServerRTPPort(videoInfo.proxy_server_rtp);
×
1249
      this.videoProxy!.setServerRTCPPort(videoInfo.proxy_server_rtcp);
×
1250

1251
      videoPort = this.videoProxy!.outgoingLocalPort();
×
1252
      videoSSRC = this.videoProxy!.outgoingSSRC;
×
1253

1254
      if (!this.disableAudioProxy) {
×
1255
        const audioInfo = response.audio as ProxiedSourceResponse;
×
1256
        this.audioProxy!.setIncomingPayloadType(audioInfo.proxy_pt);
×
1257
        this.audioProxy!.setServerAddress(audioInfo.proxy_server_address);
×
1258
        this.audioProxy!.setServerRTPPort(audioInfo.proxy_server_rtp);
×
1259
        this.audioProxy!.setServerRTCPPort(audioInfo.proxy_server_rtcp);
×
1260

1261
        audioPort = this.audioProxy!.outgoingLocalPort();
×
1262
        audioSSRC = this.audioProxy!.outgoingSSRC;
×
1263
      } else {
1264
        const audioInfo = response.audio as SourceResponse;
×
1265

1266
        audioPort = audioInfo.port;
×
1267
        audioSSRC = audioInfo.ssrc;
×
1268
      }
1269
    }
1270
    this.ipVersion = addressVersion; // we need to save this in order to calculate some default mtu values later
×
1271

1272
    const accessoryAddress = tlv.encode(
×
1273
      AddressTypes.ADDRESS_VERSION, addressVersion === "ipv4"? IPAddressVersion.IPV4: IPAddressVersion.IPV6,
×
1274
      AddressTypes.ADDRESS, address,
1275
      AddressTypes.VIDEO_RTP_PORT, tlv.writeUInt16(videoPort),
1276
      AddressTypes.AUDIO_RTP_PORT, tlv.writeUInt16(audioPort),
1277
    );
1278

1279
    const videoSRTPParameters = tlv.encode(
×
1280
      SRTPParametersTypes.SRTP_CRYPTO_SUITE, videoCryptoSuite,
1281
      SRTPParametersTypes.MASTER_KEY, videoSRTPKey,
1282
      SRTPParametersTypes.MASTER_SALT, videoSRTPSalt,
1283
    );
1284

1285
    const audioSRTPParameters = tlv.encode(
×
1286
      SRTPParametersTypes.SRTP_CRYPTO_SUITE, audioCryptoSuite,
1287
      SRTPParametersTypes.MASTER_KEY, audioSRTPKey,
1288
      SRTPParametersTypes.MASTER_SALT, audioSRTPSalt,
1289
    );
1290

1291
    this.setupEndpointsResponse = tlv.encode(
×
1292
      SetupEndpointsResponseTypes.SESSION_ID, uuid.write(identifier),
1293
      SetupEndpointsResponseTypes.STATUS, SetupEndpointsStatus.SUCCESS,
1294
      SetupEndpointsResponseTypes.ACCESSORY_ADDRESS, accessoryAddress,
1295
      SetupEndpointsResponseTypes.VIDEO_SRTP_PARAMETERS, videoSRTPParameters,
1296
      SetupEndpointsResponseTypes.AUDIO_SRTP_PARAMETERS, audioSRTPParameters,
1297
      SetupEndpointsResponseTypes.VIDEO_SSRC, tlv.writeUInt32(videoSSRC),
1298
      SetupEndpointsResponseTypes.AUDIO_SSRC, tlv.writeUInt32(audioSSRC),
1299
    ).toString("base64");
1300
    callback();
×
1301
  }
1302

1303
  private _updateStreamStatus(status: StreamingStatus): void {
1304
    this.streamStatus = status;
432✔
1305

1306
    this.service.updateCharacteristic(Characteristic.StreamingStatus, tlv.encode(
432✔
1307
      StreamingStatusTypes.STATUS, this.streamStatus,
1308
    ).toString("base64"));
1309
  }
1310

1311
  private static _supportedRTPConfiguration(supportedCryptoSuites: SRTPCryptoSuites[]): string {
1312
    if (supportedCryptoSuites.length === 1 && supportedCryptoSuites[0] === SRTPCryptoSuites.NONE) {
432!
1313
      debug("Client claims it doesn't support SRTP. The stream may stops working with future iOS releases.");
×
1314
    }
1315

1316
    return tlv.encode(SupportedRTPConfigurationTypes.SRTP_CRYPTO_SUITE, supportedCryptoSuites).toString("base64");
432✔
1317
  }
1318

1319
  private static _supportedVideoStreamConfiguration(videoOptions: VideoStreamingOptions): string {
1320
    if (!videoOptions.codec) {
432!
1321
      throw new Error("Video codec cannot be undefined");
×
1322
    }
1323
    if (!videoOptions.resolutions) {
432!
1324
      throw new Error("Video resolutions cannot be undefined");
×
1325
    }
1326

1327
    let codecParameters = tlv.encode(
432✔
1328
      VideoCodecParametersTypes.PROFILE_ID, videoOptions.codec.profiles,
1329
      VideoCodecParametersTypes.LEVEL, videoOptions.codec.levels,
1330
      VideoCodecParametersTypes.PACKETIZATION_MODE, VideoCodecPacketizationMode.NON_INTERLEAVED,
1331
    );
1332

1333
    if (videoOptions.cvoId != null) {
432!
1334
      codecParameters = Buffer.concat([
×
1335
        codecParameters,
1336
        tlv.encode(
1337
          VideoCodecParametersTypes.CVO_ENABLED, VideoCodecCVO.SUPPORTED,
1338
          VideoCodecParametersTypes.CVO_ID, videoOptions.cvoId,
1339
        ),
1340
      ]);
1341
    }
1342

1343
    const videoStreamConfiguration = tlv.encode(
432✔
1344
      VideoCodecConfigurationTypes.CODEC_TYPE, VideoCodecType.H264,
1345
      VideoCodecConfigurationTypes.CODEC_PARAMETERS, codecParameters,
1346
      VideoCodecConfigurationTypes.ATTRIBUTES, videoOptions.resolutions.map(resolution => {
1347
        if (resolution.length !== 3) {
4,752!
1348
          throw new Error("Unexpected video resolution");
×
1349
        }
1350

1351
        const width = Buffer.alloc(2);
4,752✔
1352
        const height = Buffer.alloc(2);
4,752✔
1353
        const frameRate = Buffer.alloc(1);
4,752✔
1354

1355
        width.writeUInt16LE(resolution[0], 0);
4,752✔
1356
        height.writeUInt16LE(resolution[1], 0);
4,752✔
1357
        frameRate.writeUInt8(resolution[2], 0);
4,752✔
1358

1359
        return tlv.encode(
4,752✔
1360
          VideoAttributesTypes.IMAGE_WIDTH, width,
1361
          VideoAttributesTypes.IMAGE_HEIGHT, height,
1362
          VideoAttributesTypes.FRAME_RATE, frameRate,
1363
        );
1364
      }),
1365
    );
1366

1367
    return tlv.encode(
432✔
1368
      SupportedVideoStreamConfigurationTypes.VIDEO_CODEC_CONFIGURATION, videoStreamConfiguration,
1369
    ).toString("base64");
1370
  }
1371

1372
  private checkForLegacyAudioCodecRepresentation(codecs: AudioStreamingCodec[]) { // we basically merge the samplerates here
1373
    const codecMap: Record<string, AudioStreamingCodec> = {};
432✔
1374

1375
    codecs.slice().forEach(codec => {
432✔
1376
      const previous = codecMap[codec.type];
432✔
1377

1378
      if (previous) {
432!
1379
        if (typeof previous.samplerate === "number") {
×
1380
          previous.samplerate = [previous.samplerate];
×
1381
        }
1382

1383
        previous.samplerate = previous.samplerate.concat(codec.samplerate);
×
1384

1385
        const index = codecs.indexOf(codec);
×
1386
        if (index >= 0) {
×
1387
          codecs.splice(index, 1);
×
1388
        }
1389
      } else {
1390
        codecMap[codec.type] = codec;
432✔
1391
      }
1392
    });
1393
  }
1394

1395
  private _supportedAudioStreamConfiguration(audioOptions?: AudioStreamingOptions): string {
1396
    // Only AAC-ELD and OPUS are accepted by iOS currently, and we need to give it something it will accept
1397
    // for it to start the video stream.
1398

1399
    const comfortNoise = audioOptions && !!audioOptions.comfort_noise;
432✔
1400
    const supportedCodecs: AudioStreamingCodec[] = (audioOptions && audioOptions.codecs) || [];
432!
1401
    this.checkForLegacyAudioCodecRepresentation(supportedCodecs);
432✔
1402

1403
    if (supportedCodecs.length === 0) { // Fake a Codec if we haven't got anything
432!
1404
      debug("Client doesn't support any audio codec that HomeKit supports.");
×
1405
      this.videoOnly = true;
×
1406

1407
      supportedCodecs.push({
×
1408
        type: AudioStreamingCodecType.OPUS, // Opus @16K required by Apple Watch AFAIK
1409
        samplerate: [AudioStreamingSamplerate.KHZ_16, AudioStreamingSamplerate.KHZ_24], // 16 and 24 must be supported
1410
      });
1411
    }
1412

1413
    const codecConfigurations: Buffer[] = supportedCodecs.map(codec => {
432✔
1414
      let type: AudioCodecTypes;
1415

1416
      switch (codec.type) {
432!
1417
      case AudioStreamingCodecType.OPUS:
1418
        type = AudioCodecTypes.OPUS;
×
1419
        break;
×
1420
      case AudioStreamingCodecType.AAC_ELD:
1421
        type = AudioCodecTypes.AAC_ELD;
432✔
1422
        break;
432✔
1423
      case AudioStreamingCodecType.PCMA:
1424
        type = AudioCodecTypes.PCMA;
×
1425
        break;
×
1426
      case AudioStreamingCodecType.PCMU:
1427
        type = AudioCodecTypes.PCMU;
×
1428
        break;
×
1429
      case AudioStreamingCodecType.MSBC:
1430
        type = AudioCodecTypes.MSBC;
×
1431
        break;
×
1432
      case AudioStreamingCodecType.AMR:
1433
        type = AudioCodecTypes.AMR;
×
1434
        break;
×
1435
      case AudioStreamingCodecType.AMR_WB:
1436
        type = AudioCodecTypes.AMR_WB;
×
1437
        break;
×
1438
      default:
1439
        throw new Error("Unsupported codec: " + codec.type);
×
1440
      }
1441

1442
      const providedSamplerates = (typeof codec.samplerate === "number"? [codec.samplerate]: codec.samplerate).map(rate => {
432!
1443
        let samplerate;
1444
        switch (rate) {
432!
1445
        case AudioStreamingSamplerate.KHZ_8:
1446
          samplerate = AudioSamplerate.KHZ_8;
×
1447
          break;
×
1448
        case AudioStreamingSamplerate.KHZ_16:
1449
          samplerate = AudioSamplerate.KHZ_16;
×
1450
          break;
×
1451
        case AudioStreamingSamplerate.KHZ_24:
1452
          samplerate = AudioSamplerate.KHZ_24;
432✔
1453
          break;
432✔
1454
        default:
1455
          console.log("Unsupported sample rate: ", codec.samplerate);
×
1456
          samplerate = -1;
×
1457
        }
1458
        return samplerate;
432✔
1459
      }).filter(rate => rate !== -1);
432✔
1460

1461
      if (providedSamplerates.length === 0) {
432!
1462
        throw new Error("Audio samplerate cannot be empty!");
×
1463
      }
1464

1465
      const audioParameters = tlv.encode(
432✔
1466
        AudioCodecParametersTypes.CHANNEL, Math.max(1, codec.audioChannels || 1),
864✔
1467
        AudioCodecParametersTypes.BIT_RATE, codec.bitrate || AudioBitrate.VARIABLE,
864✔
1468
        AudioCodecParametersTypes.SAMPLE_RATE, providedSamplerates,
1469
      );
1470

1471
      return tlv.encode(
432✔
1472
        AudioCodecConfigurationTypes.CODEC_TYPE, type,
1473
        AudioCodecConfigurationTypes.CODEC_PARAMETERS, audioParameters,
1474
      );
1475
    });
1476

1477
    return tlv.encode(
432✔
1478
      SupportedAudioStreamConfigurationTypes.AUDIO_CODEC_CONFIGURATION, codecConfigurations,
1479
      SupportedAudioStreamConfigurationTypes.COMFORT_NOISE_SUPPORT, comfortNoise? 1: 0,
432!
1480
    ).toString("base64");
1481
  }
1482

1483
  private resetSetupEndpointsResponse(): void {
1484
    this.setupEndpointsResponse = tlv.encode(
480✔
1485
      SetupEndpointsResponseTypes.STATUS, SetupEndpointsStatus.ERROR,
1486
    ).toString("base64");
1487
    this.service.updateCharacteristic(Characteristic.SetupEndpoints, this.setupEndpointsResponse);
480✔
1488
  }
1489

1490
  private resetSelectedStreamConfiguration(): void {
1491
    this.selectedConfiguration = tlv.encode(
480✔
1492
      SelectedRTPStreamConfigurationTypes.SESSION_CONTROL, tlv.encode(
1493
        SessionControlTypes.COMMAND, SessionControlCommand.SUSPEND_SESSION,
1494
      ),
1495
    ).toString("base64");
1496
    this.service.updateCharacteristic(Characteristic.SelectedRTPStreamConfiguration, this.selectedConfiguration);
480✔
1497
  }
1498

1499
  /**
1500
   * @private
1501
   */
1502
  serialize(): RTPStreamManagementState | undefined {
1503
    const characteristicValue = this.service.getCharacteristic(Characteristic.Active).value;
96✔
1504
    if (characteristicValue === true) {
96!
1505
      return undefined;
×
1506
    }
1507

1508
    return {
96✔
1509
      id: this.id,
1510
      active: !!characteristicValue,
1511
    };
1512
  }
1513

1514
  /**
1515
   * @private
1516
   */
1517
  deserialize(serialized: RTPStreamManagementState): void {
1518
    assert(serialized.id === this.id, `Tried to initialize RTPStreamManagement ${this.id} with data from management with id ${serialized.id}!`);
36✔
1519

1520
    this.service.updateCharacteristic(Characteristic.Active, serialized.active);
36✔
1521
  }
1522

1523
  /**
1524
   * @private
1525
   */
1526
  setupStateChangeDelegate(delegate?: StateChangeDelegate): void {
1527
    this.stateChangeDelegate = delegate;
54✔
1528
  }
1529
}
1530

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc