@camera.ui/camera-ui-homekit 0.0.16 → 0.0.18

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 +264 -221
  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,18 +114,71 @@ 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;
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
+ }
174
182
  const ffmpegInput = [
175
183
  '-hide_banner',
176
184
  '-loglevel',
@@ -185,31 +193,13 @@ export class StreamingSessionWrapper {
185
193
  '1024',
186
194
  ];
187
195
  if (isPrebufferingEnabled) {
188
- ffmpegInput.push('-f', 'mpegts', '-i', mpegtsPrebufferingState.url, ...this.silentAudioSource());
196
+ ffmpegInput.push('-f', 'mpegts', '-i', mpegtsPrebufferingState.url);
189
197
  ffmpegInput.push('-enc_time_base', '-1', '-muxdelay', '0', '-video_track_timescale', '90000');
190
198
  }
191
199
  else {
192
- ffmpegInput.push('-avioflags', 'direct', '-rtsp_transport', 'tcp', '-i', this.cameraDevice.streamSource.urls.rtsp.default, ...this.silentAudioSource());
200
+ ffmpegInput.push('-avioflags', 'direct', '-rtsp_transport', 'tcp', '-i', this.cameraDevice.streamSource.urls.rtsp.default);
193
201
  }
194
202
  ffmpegInput.push('-fps_mode', 'passthrough', '-reset_timestamps', '1');
195
- const audioCodec = [
196
- '-flags',
197
- '+global_header',
198
- '-ac',
199
- `${startStreamRequest.audio.channel}`,
200
- '-ar',
201
- `${startStreamRequest.audio.sample_rate}k`,
202
- '-b:a',
203
- `${startStreamRequest.audio.max_bit_rate}k`,
204
- '-bufsize',
205
- `${startStreamRequest.audio.max_bit_rate * 4}k`,
206
- ];
207
- if (startStreamRequest.audio.codec === "OPUS" /* AudioStreamingCodecType.OPUS */) {
208
- audioCodec.unshift('-acodec', 'libopus', '-frame_duration', startStreamRequest.audio.packet_time.toString(), '-application', 'lowdelay');
209
- }
210
- else {
211
- 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');
212
- }
213
203
  const videoCodec = [];
214
204
  if (transcodeVideoStream) {
215
205
  const idrInterval = 4;
@@ -239,21 +229,17 @@ export class StreamingSessionWrapper {
239
229
  'rtp',
240
230
  `rtp://${this.videoSplitter.address}:${this.videoSplitter.port}?pkt_size=${videoMtu}`,
241
231
  ];
242
- const audioArgs = [
243
- '-async',
244
- '1',
245
- ...audioCodec,
246
- '-payload_type',
247
- startStreamRequest.audio.pt.toString(),
248
- '-ssrc',
249
- this.audioSsrc.toString(),
250
- '-vn',
251
- '-sn',
252
- '-dn',
253
- '-f',
254
- 'rtp',
255
- `rtp://${this.repacketizeAudioSplitter.address}:${this.repacketizeAudioSplitter.port}?pkt_size=${audioMtu}`,
256
- ];
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
+ }
257
243
  const ffmpegOutput = [];
258
244
  const ffmpegArgs = [...ffmpegInput, ...videoArgs, ...audioArgs, ...ffmpegOutput];
259
245
  this.logger.debug(this.cameraDevice.name, 'Starting RTSP session with ffmpeg', ffmpegPath, ffmpegArgs.join(' '));
@@ -276,71 +262,85 @@ export class StreamingSessionWrapper {
276
262
  }
277
263
  });
278
264
  }
279
- async createTwoWayAudioTranscoder(startStreamRequest) {
280
- const twoWayAudio = await this.cameraAccessory.cameraStorage.getValue('twoWayAudio', true);
281
- let returnAudioTranscodedSplitter;
282
- let returnAudioTranscoder;
265
+ async createTwoWayAudioTranscoder(startStreamRequest, talkbackStream) {
283
266
  const returnAudioCodecs = [];
284
- if (twoWayAudio) {
285
- // used to send return audio from HomeKit to Camera
286
- returnAudioTranscodedSplitter = new RtpSplitter();
287
- await returnAudioTranscodedSplitter.prepare('udp4', '127.0.0.1', ({ message }) => {
288
- // deserialize and send to Camera - werift will handle encryption and other header params
289
- try {
290
- const rtp = RtpPacket.deSerialize(message);
291
- this.streamingSession.sendAudioPacket(rtp);
292
- }
293
- catch {
294
- // deSerialize will sometimes fail, but the errors can be ignored
295
- }
296
- return null;
297
- });
298
- const returnAudioCodec = this.cameraAccessory.cameraStorage.values.returnAudioCodec;
299
- const isCameraUsingOpus = (await this.streamingSession.isUsingOpus) || (returnAudioCodec && returnAudioCodec === 'opus');
300
- if (returnAudioCodec === 'opus' || (!returnAudioCodec && isCameraUsingOpus)) {
301
- returnAudioCodecs.push('-acodec', 'libopus', '-ac', '2', '-ar', '24k', '-b:a', '24k', '-application', 'lowdelay');
302
- }
303
- if (returnAudioCodec === 'pcmu' || (!returnAudioCodec && !isCameraUsingOpus)) {
304
- 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);
305
304
  }
306
- if (returnAudioCodec === 'pcma') {
307
- returnAudioCodecs.push('-acodec', 'pcm_alaw', '-ac', '1', '-ar', '8k');
305
+ catch {
306
+ // Ignore deserialization errors
308
307
  }
309
- const ffmpegPath = await this.cameraAccessory.api.coreManager.getFFmpegPath();
310
- returnAudioTranscoder = new ReturnAudioTranscoder({
311
- prepareStreamRequest: this.prepareStreamRequest,
312
- startStreamRequest,
313
- incomingAudioOptions: {
314
- ssrc: this.audioSsrc,
315
- rtcpPort: 0, // we don't care about rtcp for incoming audio
316
- },
317
- outputArgs: [
318
- ...returnAudioCodecs,
319
- '-flags',
320
- '+global_header',
321
- '-f',
322
- 'rtp',
323
- `rtp://${returnAudioTranscodedSplitter.address}:${returnAudioTranscodedSplitter.port}`,
324
- ],
325
- ffmpegPath,
326
- returnAudioSplitter: this.audioSplitter,
327
- logger: {
328
- info: () => { },
329
- // info: (message: string) => this.logger.debug(this.cameraDevice.name, message),
330
- error: (message) => this.logger.error(this.cameraDevice.name, 'Return Audio:', message),
331
- },
332
- });
333
- this.streamingSession.onCallEnded.pipe(take(1)).subscribe(() => {
334
- returnAudioTranscoder?.stop();
335
- returnAudioTranscodedSplitter?.close();
336
- });
337
- }
338
- 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();
339
331
  }
340
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) {
341
342
  let sentVideo = false;
342
343
  const { targetAddress, video: { port: videoPort }, } = this.prepareStreamRequest;
343
- // use to encrypt video to HomeKit
344
344
  const videoSrtpSession = new SrtpSession(getSessionConfig(this.videoSrtp));
345
345
  this.videoSplitter.addMessageHandler(({ message }) => {
346
346
  const rtp = RtpPacket.deSerialize(message);
@@ -349,7 +349,7 @@ export class StreamingSessionWrapper {
349
349
  const encryptedPacket = videoSrtpSession.encrypt(rtp.payload, rtp.header);
350
350
  if (!sentVideo) {
351
351
  sentVideo = true;
352
- 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)`);
353
353
  }
354
354
  this.videoSplitter.send(encryptedPacket, {
355
355
  port: videoPort,
@@ -358,59 +358,96 @@ export class StreamingSessionWrapper {
358
358
  return null;
359
359
  });
360
360
  }
361
- async listenForAudioPackets(startStreamRequest) {
361
+ listenForAudioPackets(startStreamRequest) {
362
362
  const { targetAddress, audio: { port: audioPort }, } = this.prepareStreamRequest;
363
- const { audio: { codec: audioCodec, sample_rate: audioSampleRate, packet_time: audioPacketTime }, } = startStreamRequest;
364
- // Repacketize the audio stream after it's been transcoded
365
- const opusRepacketizer = new OpusRepacketizer(audioPacketTime / 20);
366
- const audioIntervalScale = ((audioSampleRate / 8) * audioPacketTime) / 20;
367
- const audioSrtpSession = new SrtpSession(getSessionConfig(this.audioSrtp));
368
- let firstTimestamp;
369
- 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);
370
366
  this.repacketizeAudioSplitter.addMessageHandler(({ message }) => {
371
- let rtp = RtpPacket.deSerialize(message);
372
- if (audioCodec === "OPUS" /* AudioStreamingCodecType.OPUS */) {
373
- // borrowed from scrypted
374
- // Original source: https://github.com/koush/scrypted/blob/c13ba09889c3e0d9d3724cb7d49253c9d787fb97/plugins/homekit/src/types/camera/camera-streaming-srtp-sender.ts#L124-L143
375
- rtp = opusRepacketizer.repacketize(rtp);
376
- if (!rtp) {
377
- 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);
378
388
  }
379
- if (!firstTimestamp) {
380
- firstTimestamp = rtp.header.timestamp;
389
+ else {
390
+ return isIPv6(address);
381
391
  }
382
- // from HAP spec:
383
- // RTP Payload Format for Opus Speech and Audio Codec RFC 7587 with an exception
384
- // that Opus audio RTP Timestamp shall be based on RFC 3550.
385
- // RFC 3550 indicates that PCM audio based with a sample rate of 8k and a packet
386
- // time of 20ms would have a monotonic interval of 8k / (1000 / 20) = 160.
387
- // So 24k audio would have a monotonic interval of (24k / 8k) * 160 = 480.
388
- // HAP spec also states that it may request packet times of 20, 30, 40, or 60.
389
- // In practice, HAP has been seen to request 20 on LAN and 60 over LTE.
390
- // So the RTP timestamp must scale accordingly.
391
- // Further investigation indicates that HAP doesn't care about the actual sample rate at all,
392
- // that's merely a suggestion. When encoding Opus, it can seemingly be an arbitrary sample rate,
393
- // audio will work so long as the rtp timestamps are created properly: which is a construct of the sample rate
394
- // HAP requests, and the packet time is respected,
395
- // opus 48khz will work just fine.
396
- rtp.header.timestamp = (firstTimestamp + audioPacketCount * 160 * audioIntervalScale) % 0xffffffff;
397
- audioPacketCount++;
398
- }
399
- // encrypt the packet
400
- const encryptedPacket = audioSrtpSession.encrypt(rtp.payload, rtp.header);
401
- // send the encrypted packet to HomeKit
402
- this.audioSplitter.send(encryptedPacket, {
403
- port: audioPort,
404
- address: targetAddress,
405
392
  });
406
- return null;
407
- });
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 };
408
403
  }
409
- isLowBandwidth(startStreamRequest) {
410
- 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
+ ];
411
425
  }
412
- silentAudioSource() {
413
- return ['-f', 'lavfi', '-i', 'anullsrc'];
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
+ ];
414
451
  }
415
452
  getH264Level(level, numeric = false) {
416
453
  switch (level) {
@@ -436,5 +473,11 @@ export class StreamingSessionWrapper {
436
473
  return numeric ? '77' : 'main';
437
474
  }
438
475
  }
476
+ isLowBandwidth(startStreamRequest) {
477
+ return startStreamRequest.audio.packet_time >= 60;
478
+ }
479
+ getDurationSeconds() {
480
+ return (Date.now() - this.start) / 1000;
481
+ }
439
482
  }
440
483
  //# sourceMappingURL=sessionWrapper.js.map