@camera.ui/camera-ui-homekit 0.0.15 → 0.0.17

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.
Files changed (51) hide show
  1. package/dist/camera/accessory.js +4 -13
  2. package/dist/camera/accessory.js.map +1 -1
  3. package/dist/camera/recordingDelegate.d.ts +2 -2
  4. package/dist/camera/recordingDelegate.js +10 -9
  5. package/dist/camera/recordingDelegate.js.map +1 -1
  6. package/dist/camera/services.d.ts +2 -2
  7. package/dist/camera/services.js.map +1 -1
  8. package/dist/camera/sessionWrapper.d.ts +14 -11
  9. package/dist/camera/sessionWrapper.js +277 -222
  10. package/dist/camera/sessionWrapper.js.map +1 -1
  11. package/dist/camera/streamingDelegate.d.ts +2 -2
  12. package/dist/camera/streamingDelegate.js +1 -2
  13. package/dist/camera/streamingDelegate.js.map +1 -1
  14. package/dist/camera/streamingServer.d.ts +2 -2
  15. package/dist/camera/streamingServer.js.map +1 -1
  16. package/dist/constants.d.ts +1 -4
  17. package/dist/constants.js +6 -2
  18. package/dist/constants.js.map +1 -1
  19. package/dist/index.d.ts +3 -3
  20. package/dist/index.js.map +1 -1
  21. package/dist/types.d.ts +1 -4
  22. package/dist/utils/processor.d.ts +45 -0
  23. package/dist/utils/processor.js +133 -0
  24. package/dist/utils/processor.js.map +1 -0
  25. package/dist/utils/return-audio-transcoder.d.ts +2 -3
  26. package/dist/utils/return-audio-transcoder.js +1 -3
  27. package/dist/utils/return-audio-transcoder.js.map +1 -1
  28. package/dist/utils/srtp.js +1 -1
  29. package/dist/utils/srtp.js.map +1 -1
  30. package/dist/utils/utils.d.ts +3 -1
  31. package/dist/utils/utils.js +5 -13
  32. package/dist/utils/utils.js.map +1 -1
  33. package/package.json +9 -8
  34. package/dist/utils/ffmpeg-process.d.ts +0 -23
  35. package/dist/utils/ffmpeg-process.js +0 -72
  36. package/dist/utils/ffmpeg-process.js.map +0 -1
  37. package/dist/utils/ffmpeg.d.ts +0 -2
  38. package/dist/utils/ffmpeg.js +0 -17
  39. package/dist/utils/ffmpeg.js.map +0 -1
  40. package/dist/utils/mp4.d.ts +0 -10
  41. package/dist/utils/mp4.js +0 -53
  42. package/dist/utils/mp4.js.map +0 -1
  43. package/dist/utils/ports.d.ts +0 -7
  44. package/dist/utils/ports.js +0 -43
  45. package/dist/utils/ports.js.map +0 -1
  46. package/dist/utils/rtp-splitter.d.ts +0 -34
  47. package/dist/utils/rtp-splitter.js +0 -76
  48. package/dist/utils/rtp-splitter.js.map +0 -1
  49. package/dist/utils/rtp.d.ts +0 -3
  50. package/dist/utils/rtp.js +0 -16
  51. package/dist/utils/rtp.js.map +0 -1
@@ -1,29 +1,15 @@
1
+ import { timeoutPromise } from '@camera.ui/common';
2
+ import { RtpSplitter } from '@camera.ui/common/cameraUtils';
1
3
  import { CameraController } from 'hap-nodejs';
2
4
  import { spawn } from 'node:child_process';
3
5
  import { isIPv6 } from 'node:net';
4
6
  import { networkInterfaces } from 'node:os';
5
- import { ReplaySubject, Subject, firstValueFrom, interval, merge, of } from 'rxjs';
7
+ import { interval, merge, of, Subject } from 'rxjs';
6
8
  import { debounceTime, delay, take } from 'rxjs/operators';
7
9
  import { RtcpSenderInfo, RtcpSrPacket, RtpPacket, SrtcpSession, SrtpSession } from 'werift';
8
- import { OpusRepacketizer } from '../utils/opus-repacketizer.js';
10
+ import { AudioProcessor, getSessionConfig, VideoProcessor } from '../utils/processor.js';
9
11
  import { ReturnAudioTranscoder } from '../utils/return-audio-transcoder.js';
10
- import { RtpSplitter } from '../utils/rtp-splitter.js';
11
12
  import { generateSrtpOptions } from '../utils/srtp.js';
12
- import { timeoutPromise } from '../utils/utils.js';
13
- function getSessionConfig(srtpOptions) {
14
- return {
15
- keys: {
16
- localMasterKey: srtpOptions.srtpKey,
17
- localMasterSalt: srtpOptions.srtpSalt,
18
- remoteMasterKey: srtpOptions.srtpKey,
19
- remoteMasterSalt: srtpOptions.srtpSalt,
20
- },
21
- profile: 1,
22
- };
23
- }
24
- function getDurationSeconds(start) {
25
- return (Date.now() - start) / 1000;
26
- }
27
13
  export class StreamingSessionWrapper {
28
14
  start;
29
15
  audioSsrc = CameraController.generateSynchronisationSource();
@@ -35,53 +21,21 @@ export class StreamingSessionWrapper {
35
21
  cameraAccessory;
36
22
  cameraDevice;
37
23
  streamingSession;
38
- ffmpegProcess;
39
24
  prepareStreamRequest;
25
+ ffmpegProcess;
40
26
  logger;
41
27
  repacketizeAudioSplitter = new RtpSplitter();
42
- rtcpSubject = new ReplaySubject(1);
43
- get receivedFirstRtcpPacket() {
44
- return firstValueFrom(this.rtcpSubject);
45
- }
46
- constructor(cameraAccessory, cameraDevice, streamingSession, prepareStreamRequest, start, logger) {
28
+ packetReceivedSubject = new Subject();
29
+ constructor(cameraAccessory, cameraDevice, prepareStreamRequest, start, logger) {
47
30
  this.cameraAccessory = cameraAccessory;
48
31
  this.cameraDevice = cameraDevice;
49
- this.streamingSession = streamingSession;
32
+ this.streamingSession = this.cameraDevice.streamSource.createSession();
50
33
  this.prepareStreamRequest = prepareStreamRequest;
51
34
  this.start = start;
52
35
  this.logger = logger;
53
36
  }
54
37
  async prepare() {
55
- const { sessionID, targetAddress, addressVersion } = this.prepareStreamRequest;
56
- let { sourceAddress } = this.prepareStreamRequest;
57
- const socketType = addressVersion === 'ipv6' ? 'udp6' : 'udp4';
58
- if (socketType === 'udp4' && sourceAddress.startsWith('::ffff:')) {
59
- sourceAddress = sourceAddress.replace('::ffff:', '');
60
- }
61
- const serverAddresses = await this.cameraAccessory.api.coreManager.getServerAddresses();
62
- const found = serverAddresses.find((address) => address.includes(sourceAddress));
63
- if (!found && serverAddresses.length) {
64
- this.logger.debug(this.cameraDevice.name, `Source address ${sourceAddress} not found in server addresses`);
65
- const infos = Object.values(networkInterfaces())
66
- .flat()
67
- .map((i) => i?.address);
68
- const targetAddresses = serverAddresses.filter((address) => {
69
- if (socketType === 'udp4') {
70
- return !isIPv6(address);
71
- }
72
- else {
73
- return isIPv6(address);
74
- }
75
- });
76
- const targetAddressFound = infos.find((address) => targetAddresses.includes(address));
77
- if (targetAddressFound) {
78
- this.logger.debug(this.cameraDevice.name, `Using target address ${targetAddressFound}`);
79
- sourceAddress = targetAddressFound;
80
- }
81
- }
82
- else if (found) {
83
- this.logger.debug(this.cameraDevice.name, `Using source address ${sourceAddress}`);
84
- }
38
+ const { socketType, sessionID, sourceAddress, targetAddress, addressVersion } = await this.setupAddress();
85
39
  this.logger.debug(this.cameraDevice.name, 'Preparing stream:', { sessionID, sourceAddress, targetAddress, addressVersion });
86
40
  await this.videoSplitter.prepare(socketType, sourceAddress);
87
41
  await this.audioSplitter.prepare(socketType, sourceAddress);
@@ -89,31 +43,33 @@ export class StreamingSessionWrapper {
89
43
  if (!this.videoSplitter.port || !this.audioSplitter.port || !this.repacketizeAudioSplitter.port) {
90
44
  throw new Error('Failed to prepare stream');
91
45
  }
92
- // used to encrypt rtcp to HomeKit for keepalive
93
46
  const videoSrtcpSession = new SrtcpSession(getSessionConfig(this.videoSrtp));
94
- const onReturnPacketReceived = new Subject();
95
- // Watch return packets to detect a dead stream from the HomeKit side
96
- // This can happen if the user force-quits the Home app
47
+ let firstRtcp = false;
48
+ const logFirstRtcp = () => {
49
+ this.logger.debug(this.cameraDevice.name, 'Received RTCP packet from HomeKit');
50
+ };
97
51
  this.videoSplitter.addMessageHandler(() => {
98
- // this.logger.debug(this.cameraDevice.name, 'Received RTCP packet from HomeKit', RtpPacket.deSerialize(rtp.message));
99
- // return packet from HomeKit
100
- this.rtcpSubject.next(true);
101
- onReturnPacketReceived.next(null);
52
+ if (!firstRtcp) {
53
+ firstRtcp = true;
54
+ logFirstRtcp();
55
+ }
56
+ this.packetReceivedSubject.next();
102
57
  return null;
103
58
  });
104
59
  this.audioSplitter.addMessageHandler(() => {
105
- // return packet from HomeKit
106
- onReturnPacketReceived.next(null);
60
+ if (!firstRtcp) {
61
+ firstRtcp = true;
62
+ logFirstRtcp();
63
+ }
64
+ this.packetReceivedSubject.next();
107
65
  return null;
108
66
  });
109
- this.streamingSession.addSubscriptions(merge(of(true).pipe(delay(15000)), onReturnPacketReceived)
67
+ this.streamingSession.addSubscriptions(merge(of(null).pipe(delay(15000)), this.packetReceivedSubject.asObservable())
110
68
  .pipe(debounceTime(5000))
111
69
  .subscribe(() => {
112
- this.logger.log(this.cameraDevice.name, `Live stream appears to be inactive. (${getDurationSeconds(this.start)}s)`);
70
+ this.logger.log(this.cameraDevice.name, `Live stream appears to be inactive. (${this.getDurationSeconds()}s)`);
113
71
  this.streamingSession.stop();
114
72
  }));
115
- // Periodically send a blank RTCP packet to the HomeKit video port
116
- // Without this, HomeKit assumes the stream is dead after 30 second and sends a stop request
117
73
  this.streamingSession.addSubscriptions(interval(500).subscribe(() => {
118
74
  const senderInfo = new RtcpSenderInfo({
119
75
  ntpTimestamp: BigInt(0),
@@ -125,8 +81,8 @@ export class StreamingSessionWrapper {
125
81
  ssrc: this.videoSsrc,
126
82
  senderInfo: senderInfo,
127
83
  });
128
- const message = videoSrtcpSession.encrypt(senderReport.serialize());
129
- this.videoSplitter.send(message, {
84
+ const encryptedPacket = videoSrtcpSession.encrypt(senderReport.serialize());
85
+ this.videoSplitter.send(encryptedPacket, {
130
86
  port: this.prepareStreamRequest.video.port,
131
87
  address: this.prepareStreamRequest.targetAddress,
132
88
  });
@@ -135,13 +91,13 @@ export class StreamingSessionWrapper {
135
91
  async activate(startStreamRequest) {
136
92
  this.logger.debug(this.cameraDevice.name, 'Starting stream:', startStreamRequest);
137
93
  if (!this.isLowBandwidth(startStreamRequest)) {
138
- this.run(startStreamRequest);
94
+ await this.run(startStreamRequest);
139
95
  return;
140
96
  }
141
97
  try {
142
98
  this.logger.debug(this.cameraDevice.name, 'Low bandwidth detected, waiting for initial RTCP');
143
- await timeoutPromise(5000, this.receivedFirstRtcpPacket);
144
- this.run(startStreamRequest);
99
+ await timeoutPromise(5000, this.packetReceivedSubject.asObservable().pipe(take(1)).toPromise());
100
+ await this.run(startStreamRequest);
145
101
  }
146
102
  catch {
147
103
  this.logger.error(this.cameraDevice.name, 'Failed to receive initial RTCP packet');
@@ -150,7 +106,6 @@ export class StreamingSessionWrapper {
150
106
  }
151
107
  stop() {
152
108
  this.logger.debug(this.cameraDevice.name, 'Stopping stream');
153
- this.rtcpSubject = new ReplaySubject(1);
154
109
  this.ffmpegProcess?.kill('SIGKILL');
155
110
  this.ffmpegProcess = undefined;
156
111
  this.audioSplitter.close();
@@ -159,45 +114,92 @@ export class StreamingSessionWrapper {
159
114
  this.streamingSession.stop();
160
115
  }
161
116
  async run(startStreamRequest) {
162
- this.listenForAudioPackets(startStreamRequest);
163
- this.listenForVideoPackets(startStreamRequest);
164
- this.startRtspSession(startStreamRequest);
165
- this.createTwoWayAudioTranscoder(startStreamRequest);
117
+ const probeStream = await this.cameraDevice.streamSource.probeStream();
118
+ const audioStream = probeStream?.audio.find((a) => a.direction === 'sendonly');
119
+ const videoStream = probeStream?.video;
120
+ const talkbackStream = probeStream?.audio.find((a) => a.direction === 'recvonly');
121
+ const mpegtsPrebufferingState = await this.cameraDevice.streamSource.getPrebufferingState('mpegts');
122
+ const transcodeVideoStream = this.cameraAccessory.cameraStorage.values.transcodeStreaming;
123
+ const useRtsp = transcodeVideoStream || mpegtsPrebufferingState?.url || videoStream?.codec !== 'H264';
124
+ if (audioStream) {
125
+ this.listenForAudioPackets(startStreamRequest);
126
+ }
127
+ if (useRtsp) {
128
+ this.listenForVideoRtspPackets(startStreamRequest);
129
+ await this.startRtspSession(startStreamRequest);
130
+ }
131
+ else {
132
+ this.listenForVideoPackets(startStreamRequest);
133
+ }
134
+ if (audioStream) {
135
+ await this.startAudioSession(startStreamRequest);
136
+ }
137
+ else {
138
+ this.logger.debug(this.cameraDevice.name, 'No audio stream detected, skipping audio transcoding and talkback');
139
+ }
140
+ if (talkbackStream) {
141
+ await this.createTwoWayAudioTranscoder(startStreamRequest, talkbackStream);
142
+ }
143
+ else {
144
+ this.logger.debug(this.cameraDevice.name, 'No talkback stream detected, skipping two-way audio');
145
+ }
146
+ }
147
+ async startAudioSession(startStreamRequest) {
148
+ const audioMtu = 400;
149
+ const ffmpegPath = await this.cameraAccessory.api.coreManager.getFFmpegPath();
150
+ const audioArgs = [];
151
+ if (startStreamRequest.audio.codec === "OPUS" /* AudioStreamingCodecType.OPUS */) {
152
+ audioArgs.push(...this.opusTranscodeArgs(startStreamRequest));
153
+ }
154
+ else {
155
+ audioArgs.push(...this.aacTranscodeArgs(startStreamRequest));
156
+ }
157
+ await this.streamingSession.startTranscoding({
158
+ ffmpegPath,
159
+ input: ['-vn', '-sn', '-dn'],
160
+ audio: [...audioArgs, '-f', 'rtp', `rtp://${this.repacketizeAudioSplitter.address}:${this.repacketizeAudioSplitter.port}?pkt_size=${audioMtu}`],
161
+ output: [],
162
+ logger: {
163
+ info: (...args) => this.logger.trace(this.cameraDevice.name, 'Audio:', ...args),
164
+ error: (...args) => this.logger.error(this.cameraDevice.name, 'Audio:', ...args),
165
+ },
166
+ });
166
167
  }
167
168
  async startRtspSession(startStreamRequest) {
168
169
  const audioMtu = startStreamRequest.audio.codec === "OPUS" /* AudioStreamingCodecType.OPUS */ ? 400 : 3840 / startStreamRequest.audio.sample_rate;
169
170
  const videoMtu = startStreamRequest.video.mtu;
171
+ const probeStream = await this.cameraDevice.streamSource.probeStream();
172
+ const audioStream = probeStream?.audio.find((a) => a.direction === 'sendonly');
173
+ const videoStream = probeStream?.video;
170
174
  const ffmpegPath = await this.cameraAccessory.api.coreManager.getFFmpegPath();
171
175
  const mpegtsPrebufferingState = await this.cameraDevice.streamSource.getPrebufferingState('mpegts');
172
- const isPrebufferingEnabled = mpegtsPrebufferingState?.state;
173
- const transcodeVideoStream = this.cameraAccessory.cameraStorage.values.transcodeStreaming;
174
- const ffmpegInput = ['-hide_banner', '-loglevel', 'verbose', '-fflags', '+discardcorrupt', '-max_delay', '500000', '-flags', 'low_delay'];
175
- if (isPrebufferingEnabled) {
176
- ffmpegInput.push('-f', 'mpegts', '-i', mpegtsPrebufferingState.url, ...this.silentAudioSource());
177
- ffmpegInput.push('-enc_time_base', '-1', '-fps_mode', 'passthrough', '-muxdelay', '0', '-video_track_timescale', '90000');
178
- }
179
- else {
180
- ffmpegInput.push('-avioflags', 'direct', '-rtsp_transport', 'tcp', '-i', this.cameraDevice.streamSource.urls.rtsp.default, ...this.silentAudioSource());
176
+ const isPrebufferingEnabled = mpegtsPrebufferingState?.url;
177
+ const isH264 = videoStream?.codec === 'H264';
178
+ const transcodeVideoStream = this.cameraAccessory.cameraStorage.values.transcodeStreaming || !isH264;
179
+ if (!isH264) {
180
+ this.logger.warn(this.cameraDevice.name, 'Stream is not H264, streaming will be transcoded');
181
181
  }
182
- ffmpegInput.push('-reset_timestamps', '1');
183
- const audioCodec = [
182
+ const ffmpegInput = [
183
+ '-hide_banner',
184
+ '-loglevel',
185
+ 'verbose',
186
+ '-fflags',
187
+ '+discardcorrupt',
188
+ '-max_delay',
189
+ '500000',
184
190
  '-flags',
185
- '+global_header',
186
- '-ac',
187
- `${startStreamRequest.audio.channel}`,
188
- '-ar',
189
- `${startStreamRequest.audio.sample_rate}k`,
190
- '-b:a',
191
- `${startStreamRequest.audio.max_bit_rate}k`,
192
- '-bufsize',
193
- `${startStreamRequest.audio.max_bit_rate * 4}k`,
191
+ 'low_delay',
192
+ '-thread_queue_size',
193
+ '1024',
194
194
  ];
195
- if (startStreamRequest.audio.codec === "OPUS" /* AudioStreamingCodecType.OPUS */) {
196
- audioCodec.unshift('-acodec', 'libopus', '-frame_duration', startStreamRequest.audio.packet_time.toString(), '-application', 'lowdelay');
195
+ if (isPrebufferingEnabled) {
196
+ ffmpegInput.push('-f', 'mpegts', '-i', mpegtsPrebufferingState.url);
197
+ ffmpegInput.push('-enc_time_base', '-1', '-muxdelay', '0', '-video_track_timescale', '90000');
197
198
  }
198
199
  else {
199
- audioCodec.unshift('-acodec', 'libfdk_aac', '-profile:a', 'aac_eld', '-eld_sbr:a', '1', '-eld_v2', '1', '-frame_size', (startStreamRequest.audio.packet_time * startStreamRequest.audio.sample_rate).toString(), '-f', 'null');
200
+ ffmpegInput.push('-avioflags', 'direct', '-rtsp_transport', 'tcp', '-i', this.cameraDevice.streamSource.urls.rtsp.default);
200
201
  }
202
+ ffmpegInput.push('-fps_mode', 'passthrough', '-reset_timestamps', '1');
201
203
  const videoCodec = [];
202
204
  if (transcodeVideoStream) {
203
205
  const idrInterval = 4;
@@ -227,21 +229,17 @@ export class StreamingSessionWrapper {
227
229
  'rtp',
228
230
  `rtp://${this.videoSplitter.address}:${this.videoSplitter.port}?pkt_size=${videoMtu}`,
229
231
  ];
230
- const audioArgs = [
231
- '-async',
232
- '1',
233
- ...audioCodec,
234
- '-payload_type',
235
- startStreamRequest.audio.pt.toString(),
236
- '-ssrc',
237
- this.audioSsrc.toString(),
238
- '-vn',
239
- '-sn',
240
- '-dn',
241
- '-f',
242
- 'rtp',
243
- `rtp://${this.repacketizeAudioSplitter.address}:${this.repacketizeAudioSplitter.port}?pkt_size=${audioMtu}`,
244
- ];
232
+ const audioArgs = [];
233
+ if (audioStream) {
234
+ const audioCodecArgs = [];
235
+ if (startStreamRequest.audio.codec === "OPUS" /* AudioStreamingCodecType.OPUS */) {
236
+ audioCodecArgs.push(...this.opusTranscodeArgs(startStreamRequest));
237
+ }
238
+ else {
239
+ audioCodecArgs.push(...this.aacTranscodeArgs(startStreamRequest));
240
+ }
241
+ audioArgs.push(...audioCodecArgs, '-vn', '-sn', '-dn', '-f', 'rtp', `rtp://${this.repacketizeAudioSplitter.address}:${this.repacketizeAudioSplitter.port}?pkt_size=${audioMtu}`);
242
+ }
245
243
  const ffmpegOutput = [];
246
244
  const ffmpegArgs = [...ffmpegInput, ...videoArgs, ...audioArgs, ...ffmpegOutput];
247
245
  this.logger.debug(this.cameraDevice.name, 'Starting RTSP session with ffmpeg', ffmpegPath, ffmpegArgs.join(' '));
@@ -264,71 +262,85 @@ export class StreamingSessionWrapper {
264
262
  }
265
263
  });
266
264
  }
267
- async createTwoWayAudioTranscoder(startStreamRequest) {
268
- const twoWayAudio = await this.cameraAccessory.cameraStorage.getValue('twoWayAudio', true);
269
- let returnAudioTranscodedSplitter;
270
- let returnAudioTranscoder;
265
+ async createTwoWayAudioTranscoder(startStreamRequest, talkbackStream) {
271
266
  const returnAudioCodecs = [];
272
- if (twoWayAudio) {
273
- // used to send return audio from HomeKit to Camera
274
- returnAudioTranscodedSplitter = new RtpSplitter();
275
- await returnAudioTranscodedSplitter.prepare('udp4', '127.0.0.1', ({ message }) => {
276
- // deserialize and send to Camera - werift will handle encryption and other header params
277
- try {
278
- const rtp = RtpPacket.deSerialize(message);
279
- this.streamingSession.sendAudioPacket(rtp);
280
- }
281
- catch {
282
- // deSerialize will sometimes fail, but the errors can be ignored
283
- }
284
- return null;
285
- });
286
- const returnAudioCodec = this.cameraAccessory.cameraStorage.values.returnAudioCodec;
287
- const isCameraUsingOpus = (await this.streamingSession.isUsingOpus) || (returnAudioCodec && returnAudioCodec === 'opus');
288
- if (returnAudioCodec === 'opus' || (!returnAudioCodec && isCameraUsingOpus)) {
289
- returnAudioCodecs.push('-acodec', 'libopus', '-ac', '2', '-ar', '24k', '-b:a', '24k', '-application', 'lowdelay');
290
- }
291
- if (returnAudioCodec === 'pcmu' || (!returnAudioCodec && !isCameraUsingOpus)) {
292
- returnAudioCodecs.push('-acodec', 'pcm_mulaw', '-ac', '1', '-ar', '8k');
267
+ const returnAudioCodec = talkbackStream.ffmpegCodec;
268
+ switch (returnAudioCodec) {
269
+ case 'aac':
270
+ returnAudioCodecs.push('-acodec', returnAudioCodec, '-ac', '1', '-ar', '16k', '-b:a', '32k');
271
+ break;
272
+ case 'libopus':
273
+ returnAudioCodecs.push('-acodec', returnAudioCodec, '-ac', '2', '-ar', '24k', '-b:a', '24k', '-application', 'lowdelay');
274
+ break;
275
+ case 'pcm_mulaw':
276
+ returnAudioCodecs.push('-acodec', returnAudioCodec, '-ac', '1', '-ar', '8k');
277
+ break;
278
+ case 'pcm_alaw':
279
+ returnAudioCodecs.push('-acodec', returnAudioCodec, '-ac', '1', '-ar', '8k');
280
+ break;
281
+ case 'pcm_s16le':
282
+ returnAudioCodecs.push('-acodec', returnAudioCodec, '-ac', '1', '-ar', '8k');
283
+ break;
284
+ case 'pcm_s16be':
285
+ returnAudioCodecs.push('-acodec', returnAudioCodec, '-ac', '1', '-ar', '8k');
286
+ break;
287
+ case 'g722':
288
+ returnAudioCodecs.push('-acodec', returnAudioCodec, '-ac', '1', '-ar', '8k');
289
+ break;
290
+ case 'mp3':
291
+ returnAudioCodecs.push('-acodec', returnAudioCodec, '-ac', '1', '-ar', '8k');
292
+ break;
293
+ case 'flac':
294
+ returnAudioCodecs.push('-acodec', returnAudioCodec, '-ac', '1', '-ar', '8k');
295
+ break;
296
+ default:
297
+ returnAudioCodecs.push('-acodec', returnAudioCodec, '-ac', '1', '-ar', '8k');
298
+ }
299
+ const returnAudioTranscodedSplitter = new RtpSplitter();
300
+ await returnAudioTranscodedSplitter.prepare('udp4', '127.0.0.1', ({ message }) => {
301
+ try {
302
+ const rtp = RtpPacket.deSerialize(message);
303
+ this.streamingSession.sendAudioPacket(rtp);
293
304
  }
294
- if (returnAudioCodec === 'pcma') {
295
- returnAudioCodecs.push('-acodec', 'pcm_alaw', '-ac', '1', '-ar', '8k');
305
+ catch {
306
+ // Ignore deserialization errors
296
307
  }
297
- const ffmpegPath = await this.cameraAccessory.api.coreManager.getFFmpegPath();
298
- returnAudioTranscoder = new ReturnAudioTranscoder({
299
- prepareStreamRequest: this.prepareStreamRequest,
300
- startStreamRequest,
301
- incomingAudioOptions: {
302
- ssrc: this.audioSsrc,
303
- rtcpPort: 0, // we don't care about rtcp for incoming audio
304
- },
305
- outputArgs: [
306
- ...returnAudioCodecs,
307
- '-flags',
308
- '+global_header',
309
- '-f',
310
- 'rtp',
311
- `rtp://${returnAudioTranscodedSplitter.address}:${returnAudioTranscodedSplitter.port}`,
312
- ],
313
- ffmpegPath,
314
- returnAudioSplitter: this.audioSplitter,
315
- logger: {
316
- info: () => { },
317
- // info: (message: string) => this.logger.debug(this.cameraDevice.name, message),
318
- error: (message) => this.logger.error(this.cameraDevice.name, 'Return Audio:', message),
319
- },
320
- });
321
- this.streamingSession.onCallEnded.pipe(take(1)).subscribe(() => {
322
- returnAudioTranscoder?.stop();
323
- returnAudioTranscodedSplitter?.close();
324
- });
325
- }
326
- await returnAudioTranscoder?.start();
308
+ return null;
309
+ });
310
+ const ffmpegPath = await this.cameraAccessory.api.coreManager.getFFmpegPath();
311
+ const returnAudioTranscoder = new ReturnAudioTranscoder({
312
+ prepareStreamRequest: this.prepareStreamRequest,
313
+ startStreamRequest,
314
+ incomingAudioOptions: {
315
+ ssrc: this.audioSsrc,
316
+ rtcpPort: 0,
317
+ },
318
+ outputArgs: [...returnAudioCodecs, '-flags', '+global_header', '-f', 'rtp', `rtp://${returnAudioTranscodedSplitter.address}:${returnAudioTranscodedSplitter.port}`],
319
+ ffmpegPath,
320
+ returnAudioSplitter: this.audioSplitter,
321
+ logger: {
322
+ info: (...args) => this.logger.trace(this.cameraDevice.name, 'Return Audio:', ...args),
323
+ error: (...args) => this.logger.error(this.cameraDevice.name, 'Return Audio:', ...args),
324
+ },
325
+ });
326
+ this.streamingSession.onCallEnded.pipe(take(1)).subscribe(() => {
327
+ returnAudioTranscoder?.stop();
328
+ returnAudioTranscodedSplitter?.close();
329
+ });
330
+ await returnAudioTranscoder.start();
327
331
  }
328
332
  listenForVideoPackets(startStreamRequest) {
333
+ const { targetAddress, video: { port }, } = this.prepareStreamRequest;
334
+ const { video: { mtu, pt: payloadType }, } = startStreamRequest;
335
+ const processor = new VideoProcessor(this.videoSsrc, this.videoSrtp, this.videoSplitter, targetAddress, port, mtu, payloadType);
336
+ this.streamingSession.addSubscriptions(this.streamingSession.onVideoRtp.subscribe((rtp) => {
337
+ processor.processPacket(rtp);
338
+ }));
339
+ this.streamingSession.requestKeyFrame();
340
+ }
341
+ listenForVideoRtspPackets(startStreamRequest) {
329
342
  let sentVideo = false;
330
343
  const { targetAddress, video: { port: videoPort }, } = this.prepareStreamRequest;
331
- // use to encrypt video to HomeKit
332
344
  const videoSrtpSession = new SrtpSession(getSessionConfig(this.videoSrtp));
333
345
  this.videoSplitter.addMessageHandler(({ message }) => {
334
346
  const rtp = RtpPacket.deSerialize(message);
@@ -337,7 +349,7 @@ export class StreamingSessionWrapper {
337
349
  const encryptedPacket = videoSrtpSession.encrypt(rtp.payload, rtp.header);
338
350
  if (!sentVideo) {
339
351
  sentVideo = true;
340
- this.logger.debug(this.cameraDevice.name, `Received stream data (${getDurationSeconds(this.start)}s)`);
352
+ this.logger.debug(this.cameraDevice.name, `Received stream data (${this.getDurationSeconds()}s)`);
341
353
  }
342
354
  this.videoSplitter.send(encryptedPacket, {
343
355
  port: videoPort,
@@ -346,59 +358,96 @@ export class StreamingSessionWrapper {
346
358
  return null;
347
359
  });
348
360
  }
349
- async listenForAudioPackets(startStreamRequest) {
361
+ listenForAudioPackets(startStreamRequest) {
350
362
  const { targetAddress, audio: { port: audioPort }, } = this.prepareStreamRequest;
351
- const { audio: { codec: audioCodec, sample_rate: audioSampleRate, packet_time: audioPacketTime }, } = startStreamRequest;
352
- // Repacketize the audio stream after it's been transcoded
353
- const opusRepacketizer = new OpusRepacketizer(audioPacketTime / 20);
354
- const audioIntervalScale = ((audioSampleRate / 8) * audioPacketTime) / 20;
355
- const audioSrtpSession = new SrtpSession(getSessionConfig(this.audioSrtp));
356
- let firstTimestamp;
357
- let audioPacketCount = 0;
363
+ const { audio: { pt: payloadType, codec, packet_time: packetTime, sample_rate: sampleRate }, } = startStreamRequest;
364
+ const isOpus = codec === "OPUS" /* AudioStreamingCodecType.OPUS */;
365
+ const processor = new AudioProcessor(this.audioSsrc, this.audioSrtp, this.audioSplitter, targetAddress, audioPort, payloadType, sampleRate, packetTime, isOpus);
358
366
  this.repacketizeAudioSplitter.addMessageHandler(({ message }) => {
359
- let rtp = RtpPacket.deSerialize(message);
360
- if (audioCodec === "OPUS" /* AudioStreamingCodecType.OPUS */) {
361
- // borrowed from scrypted
362
- // Original source: https://github.com/koush/scrypted/blob/c13ba09889c3e0d9d3724cb7d49253c9d787fb97/plugins/homekit/src/types/camera/camera-streaming-srtp-sender.ts#L124-L143
363
- rtp = opusRepacketizer.repacketize(rtp);
364
- if (!rtp) {
365
- return null;
367
+ processor.processPacket(message);
368
+ return null;
369
+ });
370
+ }
371
+ async setupAddress() {
372
+ const { sessionID, targetAddress, addressVersion } = this.prepareStreamRequest;
373
+ let { sourceAddress } = this.prepareStreamRequest;
374
+ const socketType = addressVersion === 'ipv6' ? 'udp6' : 'udp4';
375
+ if (socketType === 'udp4' && sourceAddress.startsWith('::ffff:')) {
376
+ sourceAddress = sourceAddress.replace('::ffff:', '');
377
+ }
378
+ const serverAddresses = await this.cameraAccessory.api.coreManager.getServerAddresses();
379
+ const found = serverAddresses.find((address) => address.includes(sourceAddress));
380
+ if (!found && serverAddresses.length) {
381
+ this.logger.debug(this.cameraDevice.name, `Source address ${sourceAddress} not found in server addresses`);
382
+ const infos = Object.values(networkInterfaces())
383
+ .flat()
384
+ .map((i) => i?.address);
385
+ const targetAddresses = serverAddresses.filter((address) => {
386
+ if (socketType === 'udp4') {
387
+ return !isIPv6(address);
366
388
  }
367
- if (!firstTimestamp) {
368
- firstTimestamp = rtp.header.timestamp;
389
+ else {
390
+ return isIPv6(address);
369
391
  }
370
- // from HAP spec:
371
- // RTP Payload Format for Opus Speech and Audio Codec RFC 7587 with an exception
372
- // that Opus audio RTP Timestamp shall be based on RFC 3550.
373
- // RFC 3550 indicates that PCM audio based with a sample rate of 8k and a packet
374
- // time of 20ms would have a monotonic interval of 8k / (1000 / 20) = 160.
375
- // So 24k audio would have a monotonic interval of (24k / 8k) * 160 = 480.
376
- // HAP spec also states that it may request packet times of 20, 30, 40, or 60.
377
- // In practice, HAP has been seen to request 20 on LAN and 60 over LTE.
378
- // So the RTP timestamp must scale accordingly.
379
- // Further investigation indicates that HAP doesn't care about the actual sample rate at all,
380
- // that's merely a suggestion. When encoding Opus, it can seemingly be an arbitrary sample rate,
381
- // audio will work so long as the rtp timestamps are created properly: which is a construct of the sample rate
382
- // HAP requests, and the packet time is respected,
383
- // opus 48khz will work just fine.
384
- rtp.header.timestamp = (firstTimestamp + audioPacketCount * 160 * audioIntervalScale) % 0xffffffff;
385
- audioPacketCount++;
386
- }
387
- // encrypt the packet
388
- const encryptedPacket = audioSrtpSession.encrypt(rtp.payload, rtp.header);
389
- // send the encrypted packet to HomeKit
390
- this.audioSplitter.send(encryptedPacket, {
391
- port: audioPort,
392
- address: targetAddress,
393
392
  });
394
- return null;
395
- });
393
+ const targetAddressFound = infos.find((address) => targetAddresses.includes(address));
394
+ if (targetAddressFound) {
395
+ this.logger.debug(this.cameraDevice.name, `Using target address ${targetAddressFound}`);
396
+ sourceAddress = targetAddressFound;
397
+ }
398
+ }
399
+ else if (found) {
400
+ this.logger.debug(this.cameraDevice.name, `Using source address ${sourceAddress}`);
401
+ }
402
+ return { socketType, sessionID, sourceAddress, targetAddress, addressVersion };
396
403
  }
397
- isLowBandwidth(startStreamRequest) {
398
- return startStreamRequest.audio.packet_time >= 60;
404
+ opusTranscodeArgs(startStreamRequest) {
405
+ return [
406
+ '-acodec',
407
+ 'libopus',
408
+ '-min_comp',
409
+ '0',
410
+ '-application',
411
+ 'lowdelay',
412
+ '-frame_duration',
413
+ startStreamRequest.audio.packet_time.toString(),
414
+ '-flags',
415
+ '+global_header',
416
+ '-ac',
417
+ `${startStreamRequest.audio.channel}`,
418
+ '-ar',
419
+ `${startStreamRequest.audio.sample_rate}k`,
420
+ '-b:a',
421
+ `${startStreamRequest.audio.max_bit_rate}k`,
422
+ '-bufsize',
423
+ `${startStreamRequest.audio.max_bit_rate * 4}k`,
424
+ ];
399
425
  }
400
- silentAudioSource() {
401
- return ['-f', 'lavfi', '-i', 'anullsrc=cl=mono', '-shortest'];
426
+ aacTranscodeArgs(startStreamRequest) {
427
+ return [
428
+ '-acodec',
429
+ 'libfdk_aac',
430
+ '-profile:a',
431
+ 'aac_eld',
432
+ '-eld_sbr:a',
433
+ '1',
434
+ '-eld_v2',
435
+ '1',
436
+ '-frame_size',
437
+ (startStreamRequest.audio.packet_time * startStreamRequest.audio.sample_rate).toString(),
438
+ '-f',
439
+ 'null',
440
+ '-flags',
441
+ '+global_header',
442
+ '-ac',
443
+ `${startStreamRequest.audio.channel}`,
444
+ '-ar',
445
+ `${startStreamRequest.audio.sample_rate}k`,
446
+ '-b:a',
447
+ `${startStreamRequest.audio.max_bit_rate}k`,
448
+ '-bufsize',
449
+ `${startStreamRequest.audio.max_bit_rate * 4}k`,
450
+ ];
402
451
  }
403
452
  getH264Level(level, numeric = false) {
404
453
  switch (level) {
@@ -424,5 +473,11 @@ export class StreamingSessionWrapper {
424
473
  return numeric ? '77' : 'main';
425
474
  }
426
475
  }
476
+ isLowBandwidth(startStreamRequest) {
477
+ return startStreamRequest.audio.packet_time >= 60;
478
+ }
479
+ getDurationSeconds() {
480
+ return (Date.now() - this.start) / 1000;
481
+ }
427
482
  }
428
483
  //# sourceMappingURL=sessionWrapper.js.map