@scrypted/prebuffer-mixin 0.1.224 → 0.1.227

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/main.ts CHANGED
@@ -2,21 +2,23 @@
2
2
  import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-provider';
3
3
  import { startFFMPegFragmentedMP4Session } from '@scrypted/common/src/ffmpeg-mp4-parser-session';
4
4
  import { handleRebroadcasterClient, ParserOptions, ParserSession, setupActivityTimer, startParserSession } from '@scrypted/common/src/ffmpeg-rebroadcast';
5
- import { closeQuiet, listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
6
- import { safeKillFFmpeg } from '@scrypted/common/src/media-helpers';
5
+ import { closeQuiet, createBindZero, listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
6
+ import { ffmpegLogInitialOutput, safeKillFFmpeg } from '@scrypted/common/src/media-helpers';
7
7
  import { readLength } from '@scrypted/common/src/read-stream';
8
- import { createRtspParser, RtspClient, RtspServer } from '@scrypted/common/src/rtsp-server';
9
- import { addTrackControls, parsePayloadTypes, parseSdp } from '@scrypted/common/src/sdp-utils';
8
+ import { createRtspParser, H264_NAL_TYPE_IDR, findH264NaluType, RtspClient, RtspServer, RTSP_FRAME_MAGIC, parseSemicolonDelimited } from '@scrypted/common/src/rtsp-server';
9
+ import { addTrackControls, parseSdp } from '@scrypted/common/src/sdp-utils';
10
10
  import { StorageSettings } from '@scrypted/common/src/settings';
11
11
  import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "@scrypted/common/src/settings-mixin";
12
12
  import { sleep } from '@scrypted/common/src/sleep';
13
13
  import { createFragmentedMp4Parser, createMpegTsParser, parseMp4StreamChunks, StreamChunk, StreamParser } from '@scrypted/common/src/stream-parser';
14
- import sdk, { BufferConverter, FFMpegInput, MediaObject, MediaStreamOptions, MixinProvider, RequestMediaStreamOptions, ResponseMediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, VideoCameraConfiguration } from '@scrypted/sdk';
14
+ import sdk, { BufferConverter, FFmpegInput, MediaObject, MediaStreamOptions, MixinProvider, RequestMediaStreamOptions, ResponseMediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, VideoCameraConfiguration } from '@scrypted/sdk';
15
15
  import crypto from 'crypto';
16
+ import dgram from 'dgram';
16
17
  import net from 'net';
17
18
  import { Duplex } from 'stream';
18
- import { connectRFC4571Parser, startRFC4571Parser } from './rfc4571';
19
+ import { connectRFC4571Parser, RtspChannelCodecMapping, startRFC4571Parser } from './rfc4571';
19
20
  import { createStreamSettings, getPrebufferedStreams } from './stream-settings';
21
+ import { getH264EncoderArgs, LIBX264_ENCODER_TITLE } from '@scrypted/common/src/ffmpeg-hardware-acceleration';
20
22
 
21
23
  const { mediaManager, log, systemManager, deviceManager } = sdk;
22
24
 
@@ -32,7 +34,8 @@ const COMPATIBLE_AUDIO_CODECS = ['aac', 'mp3', 'mp2', 'opus'];
32
34
  const DEFAULT_FFMPEG_INPUT_ARGUMENTS = '-fflags +genpts';
33
35
 
34
36
  const SCRYPTED_PARSER = 'Scrypted';
35
- const FFMPEG_PARSER = 'FFmpeg';
37
+ const FFMPEG_PARSER_TCP = 'FFmpeg (TCP)';
38
+ const FFMPEG_PARSER_UDP = 'FFmpeg (UDP)';
36
39
  const STRING_DEFAULT = 'Default';
37
40
 
38
41
  const VALID_AUDIO_CONFIGS = [
@@ -67,8 +70,7 @@ class PrebufferSession {
67
70
  parsers: { [container: string]: StreamParser };
68
71
  sdp: Promise<string>;
69
72
 
70
- detectedIdrInterval = 0;
71
- prevIdr = 0;
73
+ detectedIdrInterval: number;
72
74
  audioDisabled = false;
73
75
 
74
76
  mixinDevice: VideoCamera & VideoCameraConfiguration;
@@ -181,18 +183,20 @@ class PrebufferSession {
181
183
 
182
184
  getParser(rtspMode: boolean, muxingMp4: boolean, mediaStreamOptions: MediaStreamOptions) {
183
185
  if (!this.canUseRtspParser(muxingMp4, mediaStreamOptions))
184
- return FFMPEG_PARSER;
186
+ return STRING_DEFAULT;
185
187
 
186
188
  const defaultValue = rtspMode
187
189
  && mediaStreamOptions?.tool === 'scrypted' ?
188
- SCRYPTED_PARSER : FFMPEG_PARSER;
190
+ SCRYPTED_PARSER : STRING_DEFAULT;
189
191
  const rtspParser = this.storage.getItem(this.rtspParserKey);
190
192
  if (!rtspParser || rtspParser === STRING_DEFAULT)
191
193
  return defaultValue;
192
194
  if (rtspParser === SCRYPTED_PARSER)
193
195
  return SCRYPTED_PARSER;
194
- if (rtspParser === FFMPEG_PARSER)
195
- return FFMPEG_PARSER;
196
+ if (rtspParser === FFMPEG_PARSER_TCP)
197
+ return FFMPEG_PARSER_TCP;
198
+ if (rtspParser === FFMPEG_PARSER_UDP)
199
+ return FFMPEG_PARSER_UDP;
196
200
  return defaultValue;
197
201
  }
198
202
 
@@ -232,7 +236,7 @@ class PrebufferSession {
232
236
  const elapsed = Date.now() - start;
233
237
  const bitrate = Math.round(total / elapsed * 8);
234
238
 
235
- const group = this.streamName ? `Rebroadcast: ${this.streamName}` : 'Rebroadcast';
239
+ const group = this.streamName ? `Stream: ${this.streamName}` : 'Stream';
236
240
 
237
241
  settings.push(
238
242
  {
@@ -258,7 +262,6 @@ class PrebufferSession {
258
262
  STRING_DEFAULT,
259
263
  'MPEG-TS',
260
264
  'RTSP',
261
- // 'RTSP+MP4',
262
265
  ],
263
266
  key: this.rebroadcastModeKey,
264
267
  value: this.storage.getItem(this.rebroadcastModeKey) || STRING_DEFAULT,
@@ -290,17 +293,21 @@ class PrebufferSession {
290
293
  && this.advertisedMediaStreamOptions?.container === 'rtsp') {
291
294
 
292
295
  const value = this.getParser(rtspMode, muxingMp4, this.advertisedMediaStreamOptions);
296
+ const defaultValue = rtspMode
297
+ && this.advertisedMediaStreamOptions?.tool === 'scrypted' ?
298
+ SCRYPTED_PARSER : 'FFmpeg';
293
299
 
294
300
  settings.push(
295
301
  {
296
302
  key: this.rtspParserKey,
297
303
  group,
298
304
  title: 'RTSP Parser',
299
- description: `Experimental: The RTSP Parser used to read the stream. FFmpeg is stable. The Scrypted parser is lower latency. The Scrypted Parser is only available when the Audo Codec is not Transcoding and the Rebroadcast Container is RTSP. The default is "${value}" for this camera.`,
305
+ description: `Experimental: The RTSP Parser used to read the stream. FFmpeg is stable. The Scrypted parser is lower latency. The Scrypted Parser is only available when the Audo Codec is not Transcoding and the Rebroadcast Container is RTSP. The default is "${defaultValue}" for this camera.`,
300
306
  value: this.storage.getItem(this.rtspParserKey) || STRING_DEFAULT,
301
307
  choices: [
302
308
  STRING_DEFAULT,
303
- FFMPEG_PARSER,
309
+ FFMPEG_PARSER_TCP,
310
+ FFMPEG_PARSER_UDP,
304
311
  SCRYPTED_PARSER,
305
312
  ],
306
313
  }
@@ -316,6 +323,9 @@ class PrebufferSession {
316
323
  }
317
324
 
318
325
  if (session) {
326
+ const resolution = session.inputVideoResolution?.width && session.inputVideoResolution?.height
327
+ ? `${session.inputVideoResolution?.width}x${session.inputVideoResolution?.height}`
328
+ : 'unknown';
319
329
 
320
330
  settings.push(
321
331
  {
@@ -323,7 +333,7 @@ class PrebufferSession {
323
333
  group,
324
334
  title: 'Detected Resolution and Bitrate',
325
335
  readonly: true,
326
- value: `${session?.inputVideoResolution?.[0] || "unknown"} @ ${bitrate || "unknown"} Kb/s`,
336
+ value: `${resolution} @ ${bitrate || "unknown"} Kb/s`,
327
337
  description: 'Configuring your camera to 1920x1080, 2000Kb/S, Variable Bit Rate, is recommended.',
328
338
  },
329
339
  {
@@ -591,12 +601,12 @@ class PrebufferSession {
591
601
  const json = await mediaManager.convertMediaObjectToJSON<any>(mo, 'x-scrypted/x-rfc4571');
592
602
  const { url, sdp, mediaStreamOptions } = json;
593
603
 
594
- session = await startRFC4571Parser(this.console, connectRFC4571Parser(url), sdp, mediaStreamOptions, false, rbo);
604
+ session = await startRFC4571Parser(this.console, connectRFC4571Parser(url), sdp, mediaStreamOptions, rbo);
595
605
  this.sdp = session.sdp.then(buffers => Buffer.concat(buffers).toString());
596
606
  }
597
607
  else {
598
608
  const moBuffer = await mediaManager.convertMediaObjectToBuffer(mo, ScryptedMimeTypes.FFmpegInput);
599
- const ffmpegInput = JSON.parse(moBuffer.toString()) as FFMpegInput;
609
+ const ffmpegInput = JSON.parse(moBuffer.toString()) as FFmpegInput;
600
610
  sessionMso = ffmpegInput.mediaStreamOptions;
601
611
 
602
612
  const parser = this.getParser(rtspMode, muxingMp4, ffmpegInput.mediaStreamOptions);
@@ -604,6 +614,14 @@ class PrebufferSession {
604
614
  usingScryptedParser = true;
605
615
  this.console.log('bypassing ffmpeg: using scrypted rtsp/rfc4571 parser');
606
616
  const rtspClient = new RtspClient(ffmpegInput.url, this.console);
617
+
618
+ let servers: dgram.Socket[] = [];
619
+ const cleanupServers = () => {
620
+ for (const server of servers) {
621
+ closeQuiet(server);
622
+ }
623
+ }
624
+
607
625
  try {
608
626
  rtspClient.requestTimeout = 10000;
609
627
  await rtspClient.options();
@@ -612,50 +630,100 @@ class PrebufferSession {
612
630
  this.console.log('sdp', sdp);
613
631
 
614
632
  const parsedSdp = parseSdp(sdp);
615
- const audioSection = parsedSdp.msections.find(msection => msection.type === 'audio');
633
+ let channel = 0;
634
+ const mapping: RtspChannelCodecMapping = {};
635
+ const useUdp = false;
636
+
637
+ const doSetup = async (control: string, codec: string) => {
638
+ let setupChannel = channel;
639
+ if (useUdp) {
640
+ const rtspChannel = channel;
641
+ const { port, server } = await createBindZero();
642
+ servers.push(server);
643
+ setupChannel = port;
644
+ server.on('message', data => {
645
+ const prefix = Buffer.alloc(4);
646
+ prefix.writeUInt8(RTSP_FRAME_MAGIC, 0);
647
+ prefix.writeUInt8(rtspChannel, 1);
648
+ prefix.writeUInt16BE(data.length, 2);
649
+ const chunk: StreamChunk = {
650
+ chunks: [prefix, data],
651
+ type: codec,
652
+ };
653
+ session?.emit('rtsp', chunk);
654
+ session?.resetActivityTimer?.();
655
+ })
656
+ }
657
+ await rtspClient.setup(setupChannel, control, useUdp);
658
+ mapping[channel] = codec;
659
+ channel += 2;
660
+ }
661
+
662
+ // grab all available audio sections
663
+ if (!audioSoftMuted) {
664
+ for (const audioSection of parsedSdp.msections.filter(msection => msection.type === 'audio')) {
665
+ await doSetup(audioSection.control, audioSection.codec)
666
+ }
667
+
668
+ if (channel === 0)
669
+ this.console.warn('sdp did not contain audio track and audio was not reported as missing.');
670
+ }
671
+
616
672
  const videoSection = parsedSdp.msections.find(msection => msection.type === 'video');
617
673
 
618
- // sdp may contain multiple audio/video sections. take only the first.
619
- parsedSdp.msections = parsedSdp.msections.filter(msection => msection === audioSection || msection === videoSection);
674
+ // sdp may contain multiple audio/video sections. take only the first video section.
675
+ parsedSdp.msections = parsedSdp.msections.filter(msection => msection === videoSection || msection.type === 'audio');
620
676
  sdp = [...parsedSdp.header.lines, ...parsedSdp.msections.map(msection => msection.lines).flat()].join('\r\n');
621
677
 
622
678
  this.sdp = Promise.resolve(sdp);
623
- let channel = 0;
624
- if (!audioSoftMuted) {
625
- if (audioSection) {
626
- await rtspClient.setup(channel, audioSection.control);
627
- channel += 2;
628
- }
629
- else {
630
- this.console.warn('sdp did not contain audio track and audio was not reported as missing.');
679
+ await doSetup(videoSection.control, videoSection.codec);
680
+ rtspClient.writePlay();
681
+
682
+ session = await startRFC4571Parser(this.console, rtspClient.client, sdp, ffmpegInput.mediaStreamOptions, rbo, {
683
+ channelMap: mapping,
684
+ handleRTSP: async () => {
685
+ await rtspClient.readMessage();
686
+ },
687
+ onLoop: async () => {
688
+ if (rtspClient.needKeepAlive) {
689
+ rtspClient.needKeepAlive = false;
690
+ rtspClient.writeGetParameter();
691
+ }
631
692
  }
632
- }
633
- await rtspClient.setup(channel, videoSection.control);
634
- await rtspClient.play();
635
- session = await startRFC4571Parser(this.console, rtspClient.rfc4571, sdp, ffmpegInput.mediaStreamOptions, true, rbo);
693
+ });
636
694
  const sessionKill = session.kill.bind(session);
637
695
  let issuedTeardown = false;
638
696
  session.kill = async () => {
639
- // issue a teardown to upstream to close gracefully but don't rely on it responding.
640
- if (!issuedTeardown) {
641
- issuedTeardown = true;
642
- rtspClient.teardown().finally(sessionKill);
697
+ try {
698
+ cleanupServers();
699
+ // issue a teardown to upstream to close gracefully but don't rely on it responding.
700
+ if (!issuedTeardown) {
701
+ issuedTeardown = true;
702
+ rtspClient.writeTeardown();
703
+ }
704
+ await sleep(500);
705
+ }
706
+ finally {
707
+ rtspClient.client.destroy();
708
+ sessionKill();
643
709
  }
644
- await sleep(500);
645
- rtspClient.client.destroy();
646
- sessionKill();
647
710
  }
648
711
  if (!session.isActive)
649
712
  throw new Error('parser was killed before rtsp client started');
650
713
 
651
- rtspClient.readLoop().finally(() => session.kill());
714
+ rtspClient.client.on('close', () => session.kill());
652
715
  }
653
716
  catch (e) {
717
+ cleanupServers();
654
718
  rtspClient.client.destroy();
655
719
  throw e;
656
720
  }
657
721
  }
658
722
  else {
723
+ if (parser === FFMPEG_PARSER_UDP)
724
+ ffmpegInput.inputArguments = ['-rtsp_transport', 'udp', '-i', ffmpegInput.url];
725
+ else if (parser === FFMPEG_PARSER_TCP)
726
+ ffmpegInput.inputArguments = ['-rtsp_transport', 'tcp', '-i', ffmpegInput.url];
659
727
  // create missing pts from dts so mpegts and mp4 muxing does not fail
660
728
  const extraInputArguments = this.storage.getItem(this.ffmpegInputArgumentsKey) || DEFAULT_FFMPEG_INPUT_ARGUMENTS;
661
729
  ffmpegInput.inputArguments.unshift(...extraInputArguments.split(' '));
@@ -670,9 +738,8 @@ class PrebufferSession {
670
738
  id: this.streamId,
671
739
  refresh: false,
672
740
  })
673
- .then(async (stream) => {
741
+ .then(async (ffmpegInput) => {
674
742
  const extraInputArguments = this.storage.getItem(this.ffmpegInputArgumentsKey) || DEFAULT_FFMPEG_INPUT_ARGUMENTS;
675
- const ffmpegInput = await mediaManager.convertMediaObjectToJSON<FFMpegInput>(stream, ScryptedMimeTypes.FFmpegInput);
676
743
  ffmpegInput.inputArguments.unshift(...extraInputArguments.split(' '));
677
744
  const mp4Session = await startFFMPegFragmentedMP4Session(ffmpegInput.inputArguments, acodec, vcodec, this.console);
678
745
 
@@ -750,7 +817,7 @@ class PrebufferSession {
750
817
  return;
751
818
  const mo = await this.mixinDevice.getVideoStream(mso);
752
819
  const moBuffer = await mediaManager.convertMediaObjectToBuffer(mo, ScryptedMimeTypes.FFmpegInput);
753
- const ffmpegInput = JSON.parse(moBuffer.toString()) as FFMpegInput;
820
+ const ffmpegInput = JSON.parse(moBuffer.toString()) as FFmpegInput;
754
821
  mso = ffmpegInput.mediaStreamOptions;
755
822
 
756
823
  scheduleRefresh(mso);
@@ -776,33 +843,34 @@ class PrebufferSession {
776
843
  for (const container of PrebufferParserValues) {
777
844
  let shifts = 0;
778
845
 
846
+ let prevIdr: number;
847
+
848
+ const updateIdr = (now: number) => {
849
+ if (prevIdr) {
850
+ const sendEvent = typeof this.detectedIdrInterval !== 'number';
851
+ this.detectedIdrInterval = now - prevIdr;
852
+ // only on the first idr update should we send a settings refresh.
853
+ if (sendEvent)
854
+ deviceManager.onMixinEvent(this.mixin.id, this.mixin.mixinProviderNativeId, ScryptedInterface.Settings, undefined);
855
+ }
856
+ prevIdr = now;
857
+ }
858
+
859
+ this.detectedIdrInterval = undefined;
779
860
  session.on(container, (chunk: StreamChunk) => {
780
861
  const prebufferContainer: PrebufferStreamChunk[] = this.prebuffers[container];
781
862
  const now = Date.now();
782
863
 
783
- const updateIdr = () => {
784
- if (this.prevIdr) {
785
- const sendEvent = typeof this.detectedIdrInterval !== 'number';
786
- this.detectedIdrInterval = now - this.prevIdr;
787
- // only on the first idr update should we send a settings refresh.
788
- if (sendEvent)
789
- deviceManager.onMixinEvent(this.mixin.id, this.mixin.mixinProviderNativeId, ScryptedInterface.Settings, undefined);
790
- }
791
- this.prevIdr = now;
792
- }
793
-
794
864
  // this is only valid for mp4, so its no op for everything else
795
865
  // used to detect idr interval.
796
866
  if (chunk.type === 'mdat') {
797
- updateIdr();
867
+ updateIdr(now);
798
868
  }
799
- if (chunk.type === 'rtp-video') {
800
- const fragmentType = chunk.chunks[1].readUInt8(12) & 0x1f;
801
- const second = chunk.chunks[1].readUInt8(13);
802
- const nalType = second & 0x1f;
803
- const startBit = second & 0x80;
804
- if (((fragmentType === 28 || fragmentType === 29) && nalType === 5 && startBit == 128) || fragmentType == 5) {
805
- updateIdr();
869
+ else if (chunk.type === 'h264') {
870
+ if (findH264NaluType(chunk, H264_NAL_TYPE_IDR)) {
871
+ // only update the rtsp computed idr once a minute.
872
+ // per packet bitscan is not great.
873
+ updateIdr(now);
806
874
  }
807
875
  }
808
876
 
@@ -865,6 +933,7 @@ class PrebufferSession {
865
933
  session: ParserSession<PrebufferParsers>,
866
934
  socketPromise: Promise<Duplex>,
867
935
  requestedPrebuffer: number,
936
+ filter?: (chunk: StreamChunk) => StreamChunk,
868
937
  }) {
869
938
  const { isActiveClient, container, session, socketPromise, requestedPrebuffer } = options;
870
939
  if (requestedPrebuffer)
@@ -884,6 +953,11 @@ class PrebufferSession {
884
953
  const now = Date.now();
885
954
 
886
955
  const safeWriteData = (chunk: StreamChunk) => {
956
+ if (options.filter) {
957
+ chunk = options.filter(chunk);
958
+ if (!chunk)
959
+ return;
960
+ }
887
961
  const buffered = writeData(chunk);
888
962
  if (buffered > 100000000) {
889
963
  this.console.log('more than 100MB has been buffered, did downstream die? killing connection.', this.streamName);
@@ -901,7 +975,7 @@ class PrebufferSession {
901
975
  session.once('killed', cleanup);
902
976
 
903
977
  const prebufferContainer: PrebufferStreamChunk[] = this.prebuffers[container];
904
- if (true) {
978
+ if (container !== 'rtsp') {
905
979
  for (const prebuffer of prebufferContainer) {
906
980
  if (prebuffer.time < now - requestedPrebuffer)
907
981
  continue;
@@ -930,7 +1004,7 @@ class PrebufferSession {
930
1004
  })
931
1005
  }
932
1006
 
933
- async getVideoStream(options?: RequestMediaStreamOptions): Promise<MediaObject> {
1007
+ async getVideoStream(options?: RequestMediaStreamOptions) {
934
1008
  if (options?.refresh === false && !this.parserSessionPromise)
935
1009
  throw new Error('Stream is currently unavailable and will not be started for this request. RequestMediaStreamOptions.refresh === false');
936
1010
 
@@ -955,50 +1029,70 @@ class PrebufferSession {
955
1029
  requestedPrebuffer += (this.detectedIdrInterval || 4000) * 1.5;
956
1030
  }
957
1031
 
958
- const createContainerServer = async (container: PrebufferParsers) => {
959
-
960
- let socketPromise: Promise<Duplex>;
961
- let containerUrl: string;
962
-
963
- if (container === 'rtsp') {
964
- const client = await listenZeroSingleClient();
965
- socketPromise = client.clientPromise.then(async (socket) => {
966
- let sdp = await this.sdp;
967
- sdp = addTrackControls(sdp);
968
- const server = new RtspServer(socket, sdp);
969
- //server.console = this.console;
970
- await server.handlePlayback();
971
- return socket;
972
- })
973
- containerUrl = client.url.replace('tcp://', 'rtsp://');
974
- }
975
- else {
976
- const client = await listenZeroSingleClient();
977
- socketPromise = client.clientPromise;
978
- containerUrl = `tcp://127.0.0.1:${client.port}`
1032
+ const mediaStreamOptions: ResponseMediaStreamOptions = session.negotiateMediaStream(options);
1033
+ let sdp = await this.sdp;
1034
+
1035
+ let socketPromise: Promise<Duplex>;
1036
+ let url: string;
1037
+ let filter: (chunk: StreamChunk) => StreamChunk;
1038
+ const codecMap = new Map<string, number>();
1039
+
1040
+ if (container === 'rtsp') {
1041
+ const parsedSdp = parseSdp(sdp);
1042
+ if (parsedSdp.msections.length > 2) {
1043
+ parsedSdp.msections = parsedSdp.msections.filter(msection => msection.codec === mediaStreamOptions.video?.codec || msection.codec === mediaStreamOptions.audio?.codec);
1044
+ sdp = parsedSdp.toSdp();
1045
+ filter = chunk => {
1046
+ const channel = codecMap.get(chunk.type);
1047
+ if (channel == undefined)
1048
+ return;
1049
+ const chunks = chunk.chunks.slice();
1050
+ const header = Buffer.from(chunks[0]);
1051
+ header.writeUInt8(channel, 1);
1052
+ chunks[0] = header;
1053
+ return {
1054
+ startStream: chunk.startStream,
1055
+ chunks,
1056
+ }
1057
+ }
979
1058
  }
980
1059
 
981
- const isActiveClient = options?.refresh !== false;
1060
+ const client = await listenZeroSingleClient();
1061
+ socketPromise = client.clientPromise.then(async (socket) => {
1062
+ sdp = addTrackControls(sdp);
1063
+ const server = new RtspServer(socket, sdp);
1064
+ server.console = this.console;
1065
+ await server.handlePlayback();
1066
+ for (const track of Object.values(server.setupTracks)) {
1067
+ codecMap.set(track.codec, track.destination);
1068
+ }
1069
+ return socket;
1070
+ })
1071
+ url = client.url.replace('tcp://', 'rtsp://');
1072
+ }
1073
+ else {
1074
+ const client = await listenZeroSingleClient();
1075
+ socketPromise = client.clientPromise;
1076
+ url = `tcp://127.0.0.1:${client.port}`
1077
+ }
982
1078
 
983
- this.handleRebroadcasterClient({
984
- isActiveClient,
985
- container,
986
- requestedPrebuffer,
987
- socketPromise,
988
- session,
989
- });
1079
+ mediaStreamOptions.sdp = sdp;
990
1080
 
991
- return containerUrl;
992
- }
1081
+ const isActiveClient = options?.refresh !== false;
993
1082
 
994
- const mediaStreamOptions: ResponseMediaStreamOptions = Object.assign({}, session.mediaStreamOptions);
995
- mediaStreamOptions.sdp = (await session.sdp)?.toString();
1083
+ this.handleRebroadcasterClient({
1084
+ isActiveClient,
1085
+ container,
1086
+ requestedPrebuffer,
1087
+ socketPromise,
1088
+ session,
1089
+ filter,
1090
+ });
996
1091
 
997
1092
  mediaStreamOptions.prebuffer = requestedPrebuffer;
998
1093
 
999
1094
  const { reencodeAudio } = this.getAudioConfig();
1000
1095
 
1001
- let codecCopy = false;
1002
1096
  if (this.audioDisabled) {
1003
1097
  mediaStreamOptions.audio = null;
1004
1098
  }
@@ -1009,33 +1103,9 @@ class PrebufferSession {
1009
1103
  profile: 'aac_low',
1010
1104
  }
1011
1105
  }
1012
- else {
1013
- codecCopy = true;
1014
- }
1015
1106
 
1016
- if (codecCopy) {
1017
- // reported codecs may be wrong/cached/etc, so before blindly copying the audio codec info,
1018
- // verify what was found.
1019
- if (session?.mediaStreamOptions?.audio?.codec === session?.inputAudioCodec) {
1020
- mediaStreamOptions.audio = session?.mediaStreamOptions?.audio;
1021
- }
1022
- else {
1023
- mediaStreamOptions.audio = {
1024
- codec: session?.inputAudioCodec,
1025
- }
1026
- }
1027
- }
1028
-
1029
- if (!mediaStreamOptions.video)
1030
- mediaStreamOptions.video = {};
1031
-
1032
- mediaStreamOptions.video.codec = session.inputVideoCodec;
1033
-
1034
- if (session.inputVideoResolution?.[2] && session.inputVideoResolution?.[3]) {
1035
- Object.assign(mediaStreamOptions.video, {
1036
- width: parseInt(session.inputVideoResolution[2]),
1037
- height: parseInt(session.inputVideoResolution[3]),
1038
- });
1107
+ if (session.inputVideoResolution?.width && session.inputVideoResolution?.height) {
1108
+ Object.assign(mediaStreamOptions.video, session.inputVideoResolution);
1039
1109
  }
1040
1110
 
1041
1111
  const now = Date.now();
@@ -1051,8 +1121,7 @@ class PrebufferSession {
1051
1121
 
1052
1122
  const length = Math.max(500000, available).toString();
1053
1123
 
1054
- const url = await createContainerServer(container);
1055
- const ffmpegInput: FFMpegInput = {
1124
+ const ffmpegInput: FFmpegInput = {
1056
1125
  url,
1057
1126
  container,
1058
1127
  inputArguments: [
@@ -1064,7 +1133,7 @@ class PrebufferSession {
1064
1133
  mediaStreamOptions,
1065
1134
  }
1066
1135
 
1067
- return this.mixin.createMediaObject(ffmpegInput, ScryptedMimeTypes.FFmpegInput);
1136
+ return ffmpegInput;
1068
1137
  }
1069
1138
  }
1070
1139
 
@@ -1074,7 +1143,7 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera & VideoCameraCo
1074
1143
 
1075
1144
  streamSettings = createStreamSettings(this);
1076
1145
 
1077
- constructor(public plugin: PrebufferProvider, options: SettingsMixinDeviceOptions<VideoCamera & VideoCameraConfiguration>) {
1146
+ constructor(public plugin: RebroadcastPlugin, options: SettingsMixinDeviceOptions<VideoCamera & VideoCameraConfiguration>) {
1078
1147
  super(options);
1079
1148
 
1080
1149
  this.delayStart();
@@ -1093,43 +1162,71 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera & VideoCameraCo
1093
1162
  await this.ensurePrebufferSessions();
1094
1163
 
1095
1164
  let id = options?.id;
1096
- if (options?.destination) {
1097
- const msos = await this.mixinDevice.getVideoStreamOptions();
1098
- let result: {
1099
- stream: ResponseMediaStreamOptions,
1100
- isDefault: boolean,
1101
- };
1165
+ let h264EncoderArguments: string[];
1166
+ let destinationVideoBitrate: number;
1102
1167
 
1103
- switch (options.destination) {
1168
+ const msos = await this.mixinDevice.getVideoStreamOptions();
1169
+ let result: {
1170
+ stream: ResponseMediaStreamOptions,
1171
+ isDefault: boolean,
1172
+ title: string;
1173
+ };
1174
+
1175
+ const defaultLocalBitrate = 2000000;
1176
+ const defaultLowResolutionBitrate = 512000;
1177
+ if (!id) {
1178
+ switch (options?.destination) {
1179
+ case 'medium-resolution':
1104
1180
  case 'remote':
1105
1181
  result = this.streamSettings.getRemoteStream(msos);
1182
+ destinationVideoBitrate = this.plugin.storageSettings.values.remoteStreamingBitrate;
1106
1183
  break;
1107
1184
  case 'low-resolution':
1108
1185
  result = this.streamSettings.getLowResolutionStream(msos);
1186
+ destinationVideoBitrate = defaultLowResolutionBitrate;
1109
1187
  break;
1110
1188
  case 'local-recorder':
1111
1189
  result = this.streamSettings.getRecordingStream(msos);
1190
+ destinationVideoBitrate = defaultLocalBitrate;
1112
1191
  break;
1113
1192
  case 'remote-recorder':
1114
1193
  result = this.streamSettings.getRemoteRecordingStream(msos);
1194
+ destinationVideoBitrate = defaultLocalBitrate;
1115
1195
  break;
1116
1196
  default:
1117
1197
  result = this.streamSettings.getDefaultStream(msos);
1198
+ destinationVideoBitrate = defaultLocalBitrate;
1118
1199
  break;
1119
1200
  }
1120
1201
 
1121
- if (!result.isDefault || !id) {
1122
- id = result.stream.id;
1123
- }
1124
- else {
1125
- this.console.log('Default stream overriden by legacy stream id setting for ', id);
1202
+ id = result.stream.id;
1203
+ this.console.log('Selected stream', result.stream.name);
1204
+ // transcoding video should never happen transparently since it is CPU intensive.
1205
+ // encourage users at every step to configure proper codecs.
1206
+ // for this reason, do not automatically supply h264 encoder arguments
1207
+ // even if h264 is requested, to force a visible failure.
1208
+ if (this.streamSettings.storageSettings.values.transcodeStreams?.includes(result.title)) {
1209
+ h264EncoderArguments = this.plugin.storageSettings.values.h264EncoderArguments?.split(' ');
1126
1210
  }
1127
1211
  }
1128
1212
 
1129
1213
  const session = this.sessions.get(id);
1130
1214
  if (!session)
1131
1215
  return this.mixinDevice.getVideoStream(options);
1132
- return session.getVideoStream(options);
1216
+
1217
+ const ffmpegInput = await session.getVideoStream(options);
1218
+ ffmpegInput.h264EncoderArguments = h264EncoderArguments;
1219
+ ffmpegInput.destinationVideoBitrate = destinationVideoBitrate;
1220
+
1221
+ if (this.streamSettings.storageSettings.values.missingCodecParameters) {
1222
+ ffmpegInput.h264FilterArguments = ffmpegInput.h264FilterArguments || [];
1223
+ ffmpegInput.h264FilterArguments.push("-bsf:v", "dump_extra");
1224
+ }
1225
+
1226
+ ffmpegInput.videoDecoderArguments = this.streamSettings.storageSettings.values.videoDecoderArguments?.split(' ');
1227
+ return mediaManager.createFFmpegMediaObject(ffmpegInput, {
1228
+ sourceId: this.id,
1229
+ });
1133
1230
  }
1134
1231
 
1135
1232
  async ensurePrebufferSessions() {
@@ -1241,13 +1338,19 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera & VideoCameraCo
1241
1338
  }
1242
1339
 
1243
1340
  async putMixinSetting(key: string, value: SettingValue): Promise<void> {
1244
- const sessions = this.sessions;
1245
- this.sessions = new Map();
1246
1341
  if (this.streamSettings.storageSettings.settings[key])
1247
1342
  await this.streamSettings.storageSettings.putSetting(key, value);
1248
1343
  else
1249
1344
  this.storage.setItem(key, value?.toString());
1250
1345
 
1346
+ // no prebuffer change necessary if the setting is a transcoding hint.
1347
+ if (this.streamSettings.storageSettings.settings[key]?.group === 'Transcoding')
1348
+ return;
1349
+
1350
+ const sessions = this.sessions;
1351
+ this.sessions = new Map();
1352
+
1353
+ // kill and reinitiate the prebuffers.
1251
1354
  for (const session of sessions.values()) {
1252
1355
  session?.parserSessionPromise?.then(session => session.kill());
1253
1356
  }
@@ -1308,12 +1411,26 @@ function millisUntilMidnight() {
1308
1411
  return (midnight.getTime() - new Date().getTime());
1309
1412
  }
1310
1413
 
1311
- class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider, BufferConverter {
1414
+ class RebroadcastPlugin extends AutoenableMixinProvider implements MixinProvider, BufferConverter, Settings {
1312
1415
  storageSettings = new StorageSettings(this, {
1313
1416
  rebroadcastPort: {
1314
1417
  title: 'Rebroadcast Port',
1315
1418
  description: 'The port of the RTSP server that will rebroadcast your streams.',
1316
1419
  type: 'number',
1420
+ },
1421
+ remoteStreamingBitrate: {
1422
+ title: 'Remote Streaming Bitrate',
1423
+ type: 'number',
1424
+ defaultValue: 1000000,
1425
+ description: 'The bitrate to use when remote streaming. This setting will only be used when transcoding or adaptive bitrate is enabled on a camera.',
1426
+ },
1427
+ h264EncoderArguments: {
1428
+ title: 'H264 Encoder Arguments',
1429
+ description: 'FFmpeg arguments used to encode h264 video. This is not camera specific and is used to setup the hardware accelerated encoder on your Scrypted server. This setting will only be used when transcoding is enabled on a camera.',
1430
+ choices: Object.keys(getH264EncoderArgs()),
1431
+ defaultValue: getH264EncoderArgs()[LIBX264_ENCODER_TITLE].join(' '),
1432
+ combobox: true,
1433
+ mapPut: (oldValue, newValue) => getH264EncoderArgs()[newValue]?.join(' ') || newValue || getH264EncoderArgs()[LIBX264_ENCODER_TITLE]?.join(' '),
1317
1434
  }
1318
1435
  });
1319
1436
  rtspServer: net.Server;
@@ -1350,6 +1467,14 @@ class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider
1350
1467
  this.startRtspServer();
1351
1468
  }
1352
1469
 
1470
+ getSettings(): Promise<Setting[]> {
1471
+ return this.storageSettings.getSettings();
1472
+ }
1473
+
1474
+ putSetting(key: string, value: SettingValue): Promise<void> {
1475
+ return this.storageSettings.putSetting(key, value);
1476
+ }
1477
+
1353
1478
  startRtspServer() {
1354
1479
  closeQuiet(this.rtspServer);
1355
1480
 
@@ -1413,13 +1538,19 @@ class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider
1413
1538
  const json = JSON.parse(data.toString());
1414
1539
  const { url, sdp } = json;
1415
1540
 
1416
- const { audioPayloadTypes, videoPayloadTypes } = parsePayloadTypes(sdp);
1541
+ const parsedSdp = parseSdp(sdp);
1542
+ const trackLookups = new Map<number, string>();
1543
+ for (const msection of parsedSdp.msections) {
1544
+ for (const pt of msection.payloadTypes) {
1545
+ trackLookups.set(pt, msection.control);
1546
+ }
1547
+ }
1417
1548
 
1418
1549
  const u = new URL(url);
1419
1550
  if (!u.protocol.startsWith('tcp'))
1420
1551
  throw new Error('rfc4751 url must be tcp');
1421
1552
  const { clientPromise, url: clientUrl } = await listenZeroSingleClient();
1422
- const ffmpeg: FFMpegInput = {
1553
+ const ffmpeg: FFmpegInput = {
1423
1554
  url: clientUrl,
1424
1555
  inputArguments: [
1425
1556
  "-rtsp_transport", "tcp",
@@ -1445,19 +1576,15 @@ class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider
1445
1576
  const length = header.readInt16BE(0);
1446
1577
  const data = await readLength(socket, length);
1447
1578
  const pt = data[1] & 0x7f;
1448
- if (audioPayloadTypes.has(pt)) {
1449
- rtsp.sendAudio(data, false);
1450
- }
1451
- else if (videoPayloadTypes.has(pt)) {
1452
- rtsp.sendVideo(data, false);
1453
- }
1454
- else {
1579
+ const track = trackLookups.get(pt);
1580
+ if (!track) {
1455
1581
  client.destroy();
1456
1582
  socket.destroy();
1457
1583
  throw new Error('unknown payload type ' + pt);
1458
1584
  }
1585
+ rtsp.sendTrack(track, data, false);
1459
1586
  }
1460
- })
1587
+ });
1461
1588
 
1462
1589
  return Buffer.from(JSON.stringify(ffmpeg));
1463
1590
  }
@@ -1478,7 +1605,7 @@ class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider
1478
1605
  mixinDeviceState,
1479
1606
  mixinProviderNativeId: this.nativeId,
1480
1607
  mixinDeviceInterfaces,
1481
- group: "Stream Selection",
1608
+ group: "Stream Management",
1482
1609
  groupKey: "prebuffer",
1483
1610
  });
1484
1611
  this.currentMixins.set(mixinDeviceState.id, ret);
@@ -1492,4 +1619,4 @@ class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider
1492
1619
  }
1493
1620
  }
1494
1621
 
1495
- export default new PrebufferProvider();
1622
+ export default new RebroadcastPlugin();