@scrypted/prebuffer-mixin 0.1.222 → 0.1.225

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, findTracksByType, 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,30 +630,60 @@ 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');
616
- const videoSection = parsedSdp.msections.find(msection => msection.type === 'video');
617
-
618
- // sdp may contain multiple audio/video sections. take only the first.
619
- parsedSdp.msections = parsedSdp.msections.filter(msection => msection === audioSection || msection === videoSection);
620
- sdp = [...parsedSdp.header.lines, ...parsedSdp.msections.map(msection => msection.lines).flat()].join('\r\n');
621
-
622
- this.sdp = Promise.resolve(sdp);
623
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
624
663
  if (!audioSoftMuted) {
625
- if (audioSection) {
626
- await rtspClient.setup(channel, audioSection.control);
627
- channel += 2;
664
+ for (const audioSection of parsedSdp.msections.filter(msection => msection.type === 'audio')) {
665
+ await doSetup(audioSection.control, audioSection.codec)
628
666
  }
629
- else {
667
+
668
+ if (channel === 0)
630
669
  this.console.warn('sdp did not contain audio track and audio was not reported as missing.');
631
- }
632
670
  }
633
- await rtspClient.setup(channel, videoSection.control);
671
+
672
+ const videoSection = parsedSdp.msections.find(msection => msection.type === 'video');
673
+
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');
676
+ sdp = [...parsedSdp.header.lines, ...parsedSdp.msections.map(msection => msection.lines).flat()].join('\r\n');
677
+
678
+ this.sdp = Promise.resolve(sdp);
679
+ await doSetup(videoSection.control, videoSection.codec);
634
680
  await rtspClient.play();
635
- session = await startRFC4571Parser(this.console, rtspClient.rfc4571, sdp, ffmpegInput.mediaStreamOptions, true, rbo);
681
+
682
+ session = await startRFC4571Parser(this.console, rtspClient.rfc4571, sdp, ffmpegInput.mediaStreamOptions, rbo, mapping);
636
683
  const sessionKill = session.kill.bind(session);
637
684
  let issuedTeardown = false;
638
685
  session.kill = async () => {
686
+ cleanupServers();
639
687
  // issue a teardown to upstream to close gracefully but don't rely on it responding.
640
688
  if (!issuedTeardown) {
641
689
  issuedTeardown = true;
@@ -651,11 +699,16 @@ class PrebufferSession {
651
699
  rtspClient.readLoop().finally(() => session.kill());
652
700
  }
653
701
  catch (e) {
702
+ cleanupServers();
654
703
  rtspClient.client.destroy();
655
704
  throw e;
656
705
  }
657
706
  }
658
707
  else {
708
+ if (parser === FFMPEG_PARSER_UDP)
709
+ ffmpegInput.inputArguments = ['-rtsp_transport', 'udp', '-i', ffmpegInput.url];
710
+ else if (parser === FFMPEG_PARSER_TCP)
711
+ ffmpegInput.inputArguments = ['-rtsp_transport', 'tcp', '-i', ffmpegInput.url];
659
712
  // create missing pts from dts so mpegts and mp4 muxing does not fail
660
713
  const extraInputArguments = this.storage.getItem(this.ffmpegInputArgumentsKey) || DEFAULT_FFMPEG_INPUT_ARGUMENTS;
661
714
  ffmpegInput.inputArguments.unshift(...extraInputArguments.split(' '));
@@ -670,9 +723,8 @@ class PrebufferSession {
670
723
  id: this.streamId,
671
724
  refresh: false,
672
725
  })
673
- .then(async (stream) => {
726
+ .then(async (ffmpegInput) => {
674
727
  const extraInputArguments = this.storage.getItem(this.ffmpegInputArgumentsKey) || DEFAULT_FFMPEG_INPUT_ARGUMENTS;
675
- const ffmpegInput = await mediaManager.convertMediaObjectToJSON<FFMpegInput>(stream, ScryptedMimeTypes.FFmpegInput);
676
728
  ffmpegInput.inputArguments.unshift(...extraInputArguments.split(' '));
677
729
  const mp4Session = await startFFMPegFragmentedMP4Session(ffmpegInput.inputArguments, acodec, vcodec, this.console);
678
730
 
@@ -750,7 +802,7 @@ class PrebufferSession {
750
802
  return;
751
803
  const mo = await this.mixinDevice.getVideoStream(mso);
752
804
  const moBuffer = await mediaManager.convertMediaObjectToBuffer(mo, ScryptedMimeTypes.FFmpegInput);
753
- const ffmpegInput = JSON.parse(moBuffer.toString()) as FFMpegInput;
805
+ const ffmpegInput = JSON.parse(moBuffer.toString()) as FFmpegInput;
754
806
  mso = ffmpegInput.mediaStreamOptions;
755
807
 
756
808
  scheduleRefresh(mso);
@@ -776,19 +828,21 @@ class PrebufferSession {
776
828
  for (const container of PrebufferParserValues) {
777
829
  let shifts = 0;
778
830
 
831
+ let prevIdr: number;
832
+ this.detectedIdrInterval = undefined;
779
833
  session.on(container, (chunk: StreamChunk) => {
780
834
  const prebufferContainer: PrebufferStreamChunk[] = this.prebuffers[container];
781
835
  const now = Date.now();
782
836
 
783
837
  const updateIdr = () => {
784
- if (this.prevIdr) {
838
+ if (prevIdr) {
785
839
  const sendEvent = typeof this.detectedIdrInterval !== 'number';
786
- this.detectedIdrInterval = now - this.prevIdr;
840
+ this.detectedIdrInterval = now - prevIdr;
787
841
  // only on the first idr update should we send a settings refresh.
788
842
  if (sendEvent)
789
843
  deviceManager.onMixinEvent(this.mixin.id, this.mixin.mixinProviderNativeId, ScryptedInterface.Settings, undefined);
790
844
  }
791
- this.prevIdr = now;
845
+ prevIdr = now;
792
846
  }
793
847
 
794
848
  // this is only valid for mp4, so its no op for everything else
@@ -796,12 +850,10 @@ class PrebufferSession {
796
850
  if (chunk.type === 'mdat') {
797
851
  updateIdr();
798
852
  }
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) {
853
+ else if (chunk.type === 'h264') {
854
+ if (findH264NaluType(chunk, H264_NAL_TYPE_IDR)) {
855
+ // only update the rtsp computed idr once a minute.
856
+ // per packet bitscan is not great.
805
857
  updateIdr();
806
858
  }
807
859
  }
@@ -865,6 +917,7 @@ class PrebufferSession {
865
917
  session: ParserSession<PrebufferParsers>,
866
918
  socketPromise: Promise<Duplex>,
867
919
  requestedPrebuffer: number,
920
+ filter?: (chunk: StreamChunk) => StreamChunk,
868
921
  }) {
869
922
  const { isActiveClient, container, session, socketPromise, requestedPrebuffer } = options;
870
923
  if (requestedPrebuffer)
@@ -884,6 +937,11 @@ class PrebufferSession {
884
937
  const now = Date.now();
885
938
 
886
939
  const safeWriteData = (chunk: StreamChunk) => {
940
+ if (options.filter) {
941
+ chunk = options.filter(chunk);
942
+ if (!chunk)
943
+ return;
944
+ }
887
945
  const buffered = writeData(chunk);
888
946
  if (buffered > 100000000) {
889
947
  this.console.log('more than 100MB has been buffered, did downstream die? killing connection.', this.streamName);
@@ -901,7 +959,7 @@ class PrebufferSession {
901
959
  session.once('killed', cleanup);
902
960
 
903
961
  const prebufferContainer: PrebufferStreamChunk[] = this.prebuffers[container];
904
- if (true) {
962
+ if (container !== 'rtsp') {
905
963
  for (const prebuffer of prebufferContainer) {
906
964
  if (prebuffer.time < now - requestedPrebuffer)
907
965
  continue;
@@ -930,7 +988,7 @@ class PrebufferSession {
930
988
  })
931
989
  }
932
990
 
933
- async getVideoStream(options?: RequestMediaStreamOptions): Promise<MediaObject> {
991
+ async getVideoStream(options?: RequestMediaStreamOptions) {
934
992
  if (options?.refresh === false && !this.parserSessionPromise)
935
993
  throw new Error('Stream is currently unavailable and will not be started for this request. RequestMediaStreamOptions.refresh === false');
936
994
 
@@ -955,50 +1013,70 @@ class PrebufferSession {
955
1013
  requestedPrebuffer += (this.detectedIdrInterval || 4000) * 1.5;
956
1014
  }
957
1015
 
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}`
1016
+ const mediaStreamOptions: ResponseMediaStreamOptions = session.negotiateMediaStream(options);
1017
+ let sdp = await this.sdp;
1018
+
1019
+ let socketPromise: Promise<Duplex>;
1020
+ let url: string;
1021
+ let filter: (chunk: StreamChunk) => StreamChunk;
1022
+ const codecMap = new Map<string, number>();
1023
+
1024
+ if (container === 'rtsp') {
1025
+ const parsedSdp = parseSdp(sdp);
1026
+ if (parsedSdp.msections.length > 2) {
1027
+ parsedSdp.msections = parsedSdp.msections.filter(msection => msection.codec === mediaStreamOptions.video?.codec || msection.codec === mediaStreamOptions.audio?.codec);
1028
+ sdp = parsedSdp.toSdp();
1029
+ filter = chunk => {
1030
+ const channel = codecMap.get(chunk.type);
1031
+ if (channel == undefined)
1032
+ return;
1033
+ const chunks = chunk.chunks.slice();
1034
+ const header = Buffer.from(chunks[0]);
1035
+ header.writeUInt8(channel, 1);
1036
+ chunks[0] = header;
1037
+ return {
1038
+ startStream: chunk.startStream,
1039
+ chunks,
1040
+ }
1041
+ }
979
1042
  }
980
1043
 
981
- const isActiveClient = options?.refresh !== false;
1044
+ const client = await listenZeroSingleClient();
1045
+ socketPromise = client.clientPromise.then(async (socket) => {
1046
+ sdp = addTrackControls(sdp);
1047
+ const server = new RtspServer(socket, sdp);
1048
+ server.console = this.console;
1049
+ await server.handlePlayback();
1050
+ for (const track of Object.values(server.setupTracks)) {
1051
+ codecMap.set(track.codec, track.destination);
1052
+ }
1053
+ return socket;
1054
+ })
1055
+ url = client.url.replace('tcp://', 'rtsp://');
1056
+ }
1057
+ else {
1058
+ const client = await listenZeroSingleClient();
1059
+ socketPromise = client.clientPromise;
1060
+ url = `tcp://127.0.0.1:${client.port}`
1061
+ }
982
1062
 
983
- this.handleRebroadcasterClient({
984
- isActiveClient,
985
- container,
986
- requestedPrebuffer,
987
- socketPromise,
988
- session,
989
- });
1063
+ mediaStreamOptions.sdp = sdp;
990
1064
 
991
- return containerUrl;
992
- }
1065
+ const isActiveClient = options?.refresh !== false;
993
1066
 
994
- const mediaStreamOptions: ResponseMediaStreamOptions = Object.assign({}, session.mediaStreamOptions);
995
- mediaStreamOptions.sdp = (await session.sdp)?.toString();
1067
+ this.handleRebroadcasterClient({
1068
+ isActiveClient,
1069
+ container,
1070
+ requestedPrebuffer,
1071
+ socketPromise,
1072
+ session,
1073
+ filter,
1074
+ });
996
1075
 
997
1076
  mediaStreamOptions.prebuffer = requestedPrebuffer;
998
1077
 
999
1078
  const { reencodeAudio } = this.getAudioConfig();
1000
1079
 
1001
- let codecCopy = false;
1002
1080
  if (this.audioDisabled) {
1003
1081
  mediaStreamOptions.audio = null;
1004
1082
  }
@@ -1009,33 +1087,9 @@ class PrebufferSession {
1009
1087
  profile: 'aac_low',
1010
1088
  }
1011
1089
  }
1012
- else {
1013
- codecCopy = true;
1014
- }
1015
-
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
1090
 
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
- });
1091
+ if (session.inputVideoResolution?.width && session.inputVideoResolution?.height) {
1092
+ Object.assign(mediaStreamOptions.video, session.inputVideoResolution);
1039
1093
  }
1040
1094
 
1041
1095
  const now = Date.now();
@@ -1051,8 +1105,7 @@ class PrebufferSession {
1051
1105
 
1052
1106
  const length = Math.max(500000, available).toString();
1053
1107
 
1054
- const url = await createContainerServer(container);
1055
- const ffmpegInput: FFMpegInput = {
1108
+ const ffmpegInput: FFmpegInput = {
1056
1109
  url,
1057
1110
  container,
1058
1111
  inputArguments: [
@@ -1064,7 +1117,7 @@ class PrebufferSession {
1064
1117
  mediaStreamOptions,
1065
1118
  }
1066
1119
 
1067
- return this.mixin.createMediaObject(ffmpegInput, ScryptedMimeTypes.FFmpegInput);
1120
+ return ffmpegInput;
1068
1121
  }
1069
1122
  }
1070
1123
 
@@ -1074,7 +1127,7 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera & VideoCameraCo
1074
1127
 
1075
1128
  streamSettings = createStreamSettings(this);
1076
1129
 
1077
- constructor(public plugin: PrebufferProvider, options: SettingsMixinDeviceOptions<VideoCamera & VideoCameraConfiguration>) {
1130
+ constructor(public plugin: RebroadcastPlugin, options: SettingsMixinDeviceOptions<VideoCamera & VideoCameraConfiguration>) {
1078
1131
  super(options);
1079
1132
 
1080
1133
  this.delayStart();
@@ -1093,41 +1146,71 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera & VideoCameraCo
1093
1146
  await this.ensurePrebufferSessions();
1094
1147
 
1095
1148
  let id = options?.id;
1096
- if (!id && options?.destination) {
1097
- const msos = await this.mixinDevice.getVideoStreamOptions();
1098
- let selected: ResponseMediaStreamOptions;
1149
+ let h264EncoderArguments: string[];
1150
+ let destinationVideoBitrate: number;
1151
+
1152
+ const msos = await this.mixinDevice.getVideoStreamOptions();
1153
+ let result: {
1154
+ stream: ResponseMediaStreamOptions,
1155
+ isDefault: boolean,
1156
+ title: string;
1157
+ };
1158
+
1159
+ const defaultLocalBitrate = 2000000;
1160
+ const defaultLowResolutionBitrate = 512000;
1161
+ if (!options.id) {
1099
1162
  switch (options.destination) {
1163
+ case 'medium-resolution':
1100
1164
  case 'remote':
1101
- selected = this.streamSettings.getRemoteStream(msos);
1165
+ result = this.streamSettings.getRemoteStream(msos);
1166
+ destinationVideoBitrate = this.plugin.storageSettings.values.remoteStreamingBitrate;
1102
1167
  break;
1103
1168
  case 'low-resolution':
1104
- selected = this.streamSettings.getLowResolutionStream(msos);
1169
+ result = this.streamSettings.getLowResolutionStream(msos);
1170
+ destinationVideoBitrate = defaultLowResolutionBitrate;
1105
1171
  break;
1106
1172
  case 'local-recorder':
1107
- selected = this.streamSettings.getRecordingStream(msos);
1173
+ result = this.streamSettings.getRecordingStream(msos);
1174
+ destinationVideoBitrate = defaultLocalBitrate;
1108
1175
  break;
1109
1176
  case 'remote-recorder':
1110
- selected = this.streamSettings.getRemoteRecordingStream(msos);
1177
+ result = this.streamSettings.getRemoteRecordingStream(msos);
1178
+ destinationVideoBitrate = defaultLocalBitrate;
1111
1179
  break;
1112
1180
  default:
1113
- selected = this.streamSettings.getDefaultStream(msos);
1181
+ result = this.streamSettings.getDefaultStream(msos);
1182
+ destinationVideoBitrate = defaultLocalBitrate;
1114
1183
  break;
1115
1184
  }
1116
1185
 
1117
- id = selected?.id;
1118
- if (id) {
1119
- this.console.log('Selected Stream', {
1120
- destination: options.destination,
1121
- id: selected.id,
1122
- name: selected.name,
1123
- });
1186
+ id = result.stream.id;
1187
+ this.console.log('Selected stream', result.stream.name);
1188
+ // transcoding video should never happen transparently since it is CPU intensive.
1189
+ // encourage users at every step to configure proper codecs.
1190
+ // for this reason, do not automatically supply h264 encoder arguments
1191
+ // even if h264 is requested, to force a visible failure.
1192
+ if (this.streamSettings.storageSettings.values.transcodeStreams?.includes(result.title)) {
1193
+ h264EncoderArguments = this.plugin.storageSettings.values.h264EncoderArguments?.split(' ');
1124
1194
  }
1125
1195
  }
1126
1196
 
1127
1197
  const session = this.sessions.get(id);
1128
1198
  if (!session)
1129
1199
  return this.mixinDevice.getVideoStream(options);
1130
- return session.getVideoStream(options);
1200
+
1201
+ const ffmpegInput = await session.getVideoStream(options);
1202
+ ffmpegInput.h264EncoderArguments = h264EncoderArguments;
1203
+ ffmpegInput.destinationVideoBitrate = destinationVideoBitrate;
1204
+
1205
+ if (this.streamSettings.storageSettings.values.missingCodecParameters) {
1206
+ ffmpegInput.h264FilterArguments = ffmpegInput.h264FilterArguments || [];
1207
+ ffmpegInput.h264FilterArguments.push("-bsf:v", "dump_extra");
1208
+ }
1209
+
1210
+ ffmpegInput.videoDecoderArguments = this.streamSettings.storageSettings.values.videoDecoderArguments?.split(' ');
1211
+ return mediaManager.createFFmpegMediaObject(ffmpegInput, {
1212
+ sourceId: this.id,
1213
+ });
1131
1214
  }
1132
1215
 
1133
1216
  async ensurePrebufferSessions() {
@@ -1239,13 +1322,19 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera & VideoCameraCo
1239
1322
  }
1240
1323
 
1241
1324
  async putMixinSetting(key: string, value: SettingValue): Promise<void> {
1242
- const sessions = this.sessions;
1243
- this.sessions = new Map();
1244
1325
  if (this.streamSettings.storageSettings.settings[key])
1245
1326
  await this.streamSettings.storageSettings.putSetting(key, value);
1246
1327
  else
1247
1328
  this.storage.setItem(key, value?.toString());
1248
1329
 
1330
+ // no prebuffer change necessary if the setting is a transcoding hint.
1331
+ if (this.streamSettings.storageSettings.settings[key]?.group === 'Transcoding')
1332
+ return;
1333
+
1334
+ const sessions = this.sessions;
1335
+ this.sessions = new Map();
1336
+
1337
+ // kill and reinitiate the prebuffers.
1249
1338
  for (const session of sessions.values()) {
1250
1339
  session?.parserSessionPromise?.then(session => session.kill());
1251
1340
  }
@@ -1306,12 +1395,26 @@ function millisUntilMidnight() {
1306
1395
  return (midnight.getTime() - new Date().getTime());
1307
1396
  }
1308
1397
 
1309
- class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider, BufferConverter {
1398
+ class RebroadcastPlugin extends AutoenableMixinProvider implements MixinProvider, BufferConverter, Settings {
1310
1399
  storageSettings = new StorageSettings(this, {
1311
1400
  rebroadcastPort: {
1312
1401
  title: 'Rebroadcast Port',
1313
1402
  description: 'The port of the RTSP server that will rebroadcast your streams.',
1314
1403
  type: 'number',
1404
+ },
1405
+ remoteStreamingBitrate: {
1406
+ title: 'Remote Streaming Bitrate',
1407
+ type: 'number',
1408
+ defaultValue: 1000000,
1409
+ description: 'The bitrate to use when remote streaming. This setting will only be used when transcoding or adaptive bitrate is enabled on a camera.',
1410
+ },
1411
+ h264EncoderArguments: {
1412
+ title: 'H264 Encoder Arguments',
1413
+ 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.',
1414
+ choices: Object.keys(getH264EncoderArgs()),
1415
+ defaultValue: getH264EncoderArgs()[LIBX264_ENCODER_TITLE].join(' '),
1416
+ combobox: true,
1417
+ mapPut: (oldValue, newValue) => getH264EncoderArgs()[newValue]?.join(' ') || newValue || getH264EncoderArgs()[LIBX264_ENCODER_TITLE]?.join(' '),
1315
1418
  }
1316
1419
  });
1317
1420
  rtspServer: net.Server;
@@ -1348,6 +1451,14 @@ class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider
1348
1451
  this.startRtspServer();
1349
1452
  }
1350
1453
 
1454
+ getSettings(): Promise<Setting[]> {
1455
+ return this.storageSettings.getSettings();
1456
+ }
1457
+
1458
+ putSetting(key: string, value: SettingValue): Promise<void> {
1459
+ return this.storageSettings.putSetting(key, value);
1460
+ }
1461
+
1351
1462
  startRtspServer() {
1352
1463
  closeQuiet(this.rtspServer);
1353
1464
 
@@ -1411,13 +1522,19 @@ class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider
1411
1522
  const json = JSON.parse(data.toString());
1412
1523
  const { url, sdp } = json;
1413
1524
 
1414
- const { audioPayloadTypes, videoPayloadTypes } = parsePayloadTypes(sdp);
1525
+ const parsedSdp = parseSdp(sdp);
1526
+ const trackLookups = new Map<number, string>();
1527
+ for (const msection of parsedSdp.msections) {
1528
+ for (const pt of msection.payloadTypes) {
1529
+ trackLookups.set(pt, msection.control);
1530
+ }
1531
+ }
1415
1532
 
1416
1533
  const u = new URL(url);
1417
1534
  if (!u.protocol.startsWith('tcp'))
1418
1535
  throw new Error('rfc4751 url must be tcp');
1419
1536
  const { clientPromise, url: clientUrl } = await listenZeroSingleClient();
1420
- const ffmpeg: FFMpegInput = {
1537
+ const ffmpeg: FFmpegInput = {
1421
1538
  url: clientUrl,
1422
1539
  inputArguments: [
1423
1540
  "-rtsp_transport", "tcp",
@@ -1443,19 +1560,15 @@ class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider
1443
1560
  const length = header.readInt16BE(0);
1444
1561
  const data = await readLength(socket, length);
1445
1562
  const pt = data[1] & 0x7f;
1446
- if (audioPayloadTypes.has(pt)) {
1447
- rtsp.sendAudio(data, false);
1448
- }
1449
- else if (videoPayloadTypes.has(pt)) {
1450
- rtsp.sendVideo(data, false);
1451
- }
1452
- else {
1563
+ const track = trackLookups.get(pt);
1564
+ if (!track) {
1453
1565
  client.destroy();
1454
1566
  socket.destroy();
1455
1567
  throw new Error('unknown payload type ' + pt);
1456
1568
  }
1569
+ rtsp.sendTrack(track, data, false);
1457
1570
  }
1458
- })
1571
+ });
1459
1572
 
1460
1573
  return Buffer.from(JSON.stringify(ffmpeg));
1461
1574
  }
@@ -1476,7 +1589,7 @@ class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider
1476
1589
  mixinDeviceState,
1477
1590
  mixinProviderNativeId: this.nativeId,
1478
1591
  mixinDeviceInterfaces,
1479
- group: "Stream Selection",
1592
+ group: "Stream Management",
1480
1593
  groupKey: "prebuffer",
1481
1594
  });
1482
1595
  this.currentMixins.set(mixinDeviceState.id, ret);
@@ -1490,4 +1603,4 @@ class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider
1490
1603
  }
1491
1604
  }
1492
1605
 
1493
- export default new PrebufferProvider();
1606
+ export default new RebroadcastPlugin();