@scrypted/prebuffer-mixin 0.1.227 → 0.1.228

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
@@ -1,11 +1,12 @@
1
1
 
2
2
  import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-provider';
3
+ import { getH264EncoderArgs, LIBX264_ENCODER_TITLE } from '@scrypted/common/src/ffmpeg-hardware-acceleration';
3
4
  import { startFFMPegFragmentedMP4Session } from '@scrypted/common/src/ffmpeg-mp4-parser-session';
4
5
  import { handleRebroadcasterClient, ParserOptions, ParserSession, setupActivityTimer, startParserSession } from '@scrypted/common/src/ffmpeg-rebroadcast';
5
6
  import { closeQuiet, createBindZero, listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
6
- import { ffmpegLogInitialOutput, safeKillFFmpeg } from '@scrypted/common/src/media-helpers';
7
+ import { safeKillFFmpeg } from '@scrypted/common/src/media-helpers';
7
8
  import { readLength } from '@scrypted/common/src/read-stream';
8
- import { createRtspParser, H264_NAL_TYPE_IDR, findH264NaluType, RtspClient, RtspServer, RTSP_FRAME_MAGIC, parseSemicolonDelimited } from '@scrypted/common/src/rtsp-server';
9
+ import { createRtspParser, findH264NaluType, H264_NAL_TYPE_IDR, RtspClient, RtspServer, RTSP_FRAME_MAGIC } from '@scrypted/common/src/rtsp-server';
9
10
  import { addTrackControls, parseSdp } from '@scrypted/common/src/sdp-utils';
10
11
  import { StorageSettings } from '@scrypted/common/src/settings';
11
12
  import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "@scrypted/common/src/settings-mixin";
@@ -18,7 +19,6 @@ import net from 'net';
18
19
  import { Duplex } from 'stream';
19
20
  import { connectRFC4571Parser, RtspChannelCodecMapping, startRFC4571Parser } from './rfc4571';
20
21
  import { createStreamSettings, getPrebufferedStreams } from './stream-settings';
21
- import { getH264EncoderArgs, LIBX264_ENCODER_TITLE } from '@scrypted/common/src/ffmpeg-hardware-acceleration';
22
22
 
23
23
  const { mediaManager, log, systemManager, deviceManager } = sdk;
24
24
 
@@ -44,9 +44,8 @@ const VALID_AUDIO_CONFIGS = [
44
44
  TRANSCODE_AUDIO,
45
45
  ];
46
46
 
47
- interface PrebufferStreamChunk {
48
- chunk: StreamChunk;
49
- time: number;
47
+ interface PrebufferStreamChunk extends StreamChunk {
48
+ time?: number;
50
49
  }
51
50
 
52
51
  interface Prebuffers {
@@ -70,7 +69,6 @@ class PrebufferSession {
70
69
  parsers: { [container: string]: StreamParser };
71
70
  sdp: Promise<string>;
72
71
 
73
- detectedIdrInterval: number;
74
72
  audioDisabled = false;
75
73
 
76
74
  mixinDevice: VideoCamera & VideoCameraConfiguration;
@@ -107,6 +105,38 @@ class PrebufferSession {
107
105
  }
108
106
  }
109
107
 
108
+ getDetectedIdrInterval() {
109
+ const durations: number[] = [];
110
+ if (this.prebuffers.mp4.length) {
111
+ let last: number;
112
+
113
+ for (const chunk of this.prebuffers.mp4) {
114
+ if (chunk.type === 'mdat') {
115
+ if (last)
116
+ durations.push(chunk.time - last);
117
+ last = chunk.time;
118
+ }
119
+ }
120
+ }
121
+ else if (this.prebuffers.rtsp.length) {
122
+ let last: number;
123
+
124
+ for (const chunk of this.prebuffers.rtsp) {
125
+ if (findH264NaluType(chunk, H264_NAL_TYPE_IDR)) {
126
+ if (last)
127
+ durations.push(chunk.time - last);
128
+ last = chunk.time;
129
+ }
130
+ }
131
+ }
132
+
133
+ if (!durations.length)
134
+ return;
135
+
136
+ const total = durations.reduce((prev, current) => prev + current, 0);
137
+ return total / durations.length;
138
+ }
139
+
110
140
  get maxBitrate() {
111
141
  let ret = parseInt(this.storage.getItem(this.maxBitrateKey));
112
142
  if (!ret) {
@@ -229,7 +259,7 @@ class PrebufferSession {
229
259
  const { muxingMp4, rtspMode, defaultMode } = this.getRebroadcastMode();
230
260
  for (const prebuffer of (muxingMp4 ? this.prebuffers.mp4 : this.prebuffers.rtsp)) {
231
261
  start = start || prebuffer.time;
232
- for (const chunk of prebuffer.chunk.chunks) {
262
+ for (const chunk of prebuffer.chunks) {
233
263
  total += chunk.byteLength;
234
264
  }
235
265
  }
@@ -327,6 +357,7 @@ class PrebufferSession {
327
357
  ? `${session.inputVideoResolution?.width}x${session.inputVideoResolution?.height}`
328
358
  : 'unknown';
329
359
 
360
+ const idrInterval = this.getDetectedIdrInterval();
330
361
  settings.push(
331
362
  {
332
363
  key: 'detectedResolution',
@@ -350,7 +381,7 @@ class PrebufferSession {
350
381
  title: 'Detected Keyframe Interval',
351
382
  description: "Configuring your camera to 4 seconds is recommended (IDR aka Frame Interval = FPS * 4 seconds).",
352
383
  readonly: true,
353
- value: (this.detectedIdrInterval || 0) / 1000 || 'unknown',
384
+ value: (idrInterval || 0) / 1000 || 'unknown',
354
385
  },
355
386
  );
356
387
  }
@@ -684,7 +715,7 @@ class PrebufferSession {
684
715
  handleRTSP: async () => {
685
716
  await rtspClient.readMessage();
686
717
  },
687
- onLoop: async () => {
718
+ onLoop: () => {
688
719
  if (rtspClient.needKeepAlive) {
689
720
  rtspClient.needKeepAlive = false;
690
721
  rtspClient.writeGetParameter();
@@ -842,50 +873,22 @@ class PrebufferSession {
842
873
 
843
874
  for (const container of PrebufferParserValues) {
844
875
  let shifts = 0;
876
+ let prebufferContainer: PrebufferStreamChunk[] = this.prebuffers[container];
845
877
 
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;
860
- session.on(container, (chunk: StreamChunk) => {
861
- const prebufferContainer: PrebufferStreamChunk[] = this.prebuffers[container];
878
+ session.on(container, (chunk: PrebufferStreamChunk) => {
862
879
  const now = Date.now();
863
880
 
864
- // this is only valid for mp4, so its no op for everything else
865
- // used to detect idr interval.
866
- if (chunk.type === 'mdat') {
867
- updateIdr(now);
868
- }
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);
874
- }
875
- }
876
-
877
- prebufferContainer.push({
878
- time: now,
879
- chunk,
880
- });
881
+ chunk.time = now;
882
+ prebufferContainer.push(chunk);
881
883
 
882
884
  while (prebufferContainer.length && prebufferContainer[0].time < now - prebufferDurationMs) {
883
885
  prebufferContainer.shift();
884
886
  shifts++;
885
887
  }
886
888
 
887
- if (shifts > 1000) {
888
- this.prebuffers[container] = prebufferContainer.slice();
889
+ if (shifts > 100000) {
890
+ prebufferContainer = prebufferContainer.slice();
891
+ this.prebuffers[container] = prebufferContainer;
889
892
  shifts = 0;
890
893
  }
891
894
  });
@@ -976,17 +979,17 @@ class PrebufferSession {
976
979
 
977
980
  const prebufferContainer: PrebufferStreamChunk[] = this.prebuffers[container];
978
981
  if (container !== 'rtsp') {
979
- for (const prebuffer of prebufferContainer) {
980
- if (prebuffer.time < now - requestedPrebuffer)
982
+ for (const chunk of prebufferContainer) {
983
+ if (chunk.time < now - requestedPrebuffer)
981
984
  continue;
982
985
 
983
- safeWriteData(prebuffer.chunk);
986
+ safeWriteData(chunk);
984
987
  }
985
988
  }
986
989
  else {
987
990
  // for some reason this doesn't work as well as simply guessing and dumping.
988
991
  const parser = this.parsers[container];
989
- const availablePrebuffers = parser.findSyncFrame(prebufferContainer.filter(pb => pb.time >= now - requestedPrebuffer).map(pb => pb.chunk));
992
+ const availablePrebuffers = parser.findSyncFrame(prebufferContainer.filter(pb => pb.time >= now - requestedPrebuffer));
990
993
  for (const prebuffer of availablePrebuffers) {
991
994
  safeWriteData(prebuffer);
992
995
  }
@@ -1012,10 +1015,11 @@ class PrebufferSession {
1012
1015
 
1013
1016
  const session = await this.parserSessionPromise;
1014
1017
 
1018
+ const idrInterval = this.getDetectedIdrInterval();
1015
1019
  let requestedPrebuffer = options?.prebuffer;
1016
1020
  if (requestedPrebuffer == null) {
1017
1021
  // get into the general area of finding a sync frame.
1018
- requestedPrebuffer = Math.max(4000, (this.detectedIdrInterval || 4000)) * 1.5;
1022
+ requestedPrebuffer = Math.max(4000, (idrInterval || 4000)) * 1.5;
1019
1023
  }
1020
1024
 
1021
1025
  const { rtspMode } = this.getRebroadcastMode();
@@ -1026,7 +1030,7 @@ class PrebufferSession {
1026
1030
  // If a mp4 prebuffer was explicitly requested, but an mp4 prebuffer is not available (rtsp mode),
1027
1031
  // rewind a little bit earlier to gaurantee a valid full segment of that length is sent.
1028
1032
  if (options?.prebuffer && container !== 'mp4' && options?.container === 'mp4') {
1029
- requestedPrebuffer += (this.detectedIdrInterval || 4000) * 1.5;
1033
+ requestedPrebuffer += (idrInterval || 4000) * 1.5;
1030
1034
  }
1031
1035
 
1032
1036
  const mediaStreamOptions: ResponseMediaStreamOptions = session.negotiateMediaStream(options);
@@ -1114,7 +1118,7 @@ class PrebufferSession {
1114
1118
  for (const prebuffer of prebufferContainer) {
1115
1119
  if (prebuffer.time < now - requestedPrebuffer)
1116
1120
  continue;
1117
- for (const chunk of prebuffer.chunk.chunks) {
1121
+ for (const chunk of prebuffer.chunks) {
1118
1122
  available += chunk.length;
1119
1123
  }
1120
1124
  }
@@ -1510,7 +1514,8 @@ class RebroadcastPlugin extends AutoenableMixinProvider implements MixinProvider
1510
1514
  await server.handlePlayback();
1511
1515
  const session = await prebufferSession.parserSessionPromise;
1512
1516
 
1513
- const requestedPrebuffer = Math.max(4000, (prebufferSession.detectedIdrInterval || 4000)) * 1.5;
1517
+ const idrInterval = prebufferSession.getDetectedIdrInterval();
1518
+ const requestedPrebuffer = Math.max(4000, (idrInterval || 4000)) * 1.5;
1514
1519
 
1515
1520
  prebufferSession.handleRebroadcasterClient({
1516
1521
  isActiveClient: true,
package/src/rfc4571.ts CHANGED
@@ -23,7 +23,7 @@ export type RtspChannelCodecMapping = { [key: number]: string };
23
23
  export async function startRFC4571Parser(console: Console, socket: Readable, sdp: string, mediaStreamOptions: ResponseMediaStreamOptions, options?: ParserOptions<"rtsp">, rtspOptions?: {
24
24
  channelMap: RtspChannelCodecMapping,
25
25
  handleRTSP: () => Promise<void>,
26
- onLoop?: () => Promise<void>,
26
+ onLoop?: () => void,
27
27
  }): Promise<ParserSession<"rtsp">> {
28
28
  let isActive = true;
29
29
  const events = new EventEmitter();
@@ -85,73 +85,78 @@ export async function startRFC4571Parser(console: Console, socket: Readable, sdp
85
85
  (async () => {
86
86
  const headerLength = rtspOptions?.channelMap ? 4 : 2;
87
87
  const offset = rtspOptions?.channelMap ? 2 : 0;
88
- const skipHeader = async (header: Buffer) => {
88
+ const skipHeader = (header: Buffer, resumeRead: () => void) => {
89
89
  if (header.toString() !== 'RTSP')
90
90
  return false;
91
91
  socket.unshift(header);
92
- await rtspOptions.handleRTSP();
92
+ rtspOptions.handleRTSP().then(resumeRead);
93
93
  return true;
94
94
  }
95
- for await (let { header, data } of read16BELengthLoop(socket, headerLength, offset, skipHeader)) {
96
- let type: string;
97
-
98
- await rtspOptions?.onLoop?.();
99
-
100
- if (rtspOptions?.channelMap) {
101
- const channel = header.readUInt8(1);
102
- type = rtspOptions?.channelMap[channel];
103
- }
104
- else {
105
- const pt = data[1] & 0x7f;
106
-
107
- const prefix = Buffer.alloc(2);
108
- prefix[0] = RTSP_FRAME_MAGIC;
109
- if (pt === audioPt) {
110
- prefix[1] = 0;
111
- }
112
- else if (pt === videoPt) {
113
- prefix[1] = 2;
95
+ await read16BELengthLoop(socket, {
96
+ headerLength,
97
+ offset,
98
+ skipHeader,
99
+ callback: (header, data) => {
100
+ let type: string;
101
+
102
+ rtspOptions?.onLoop?.();
103
+
104
+ if (rtspOptions?.channelMap) {
105
+ const channel = header.readUInt8(1);
106
+ type = rtspOptions?.channelMap[channel];
114
107
  }
115
- header = Buffer.concat([prefix, header]);
108
+ else {
109
+ const pt = data[1] & 0x7f;
116
110
 
117
- if (pt === audioPt)
118
- type = inputAudioCodec;
119
- else if (pt === videoPt)
120
- type = inputVideoCodec;
121
- }
111
+ const prefix = Buffer.alloc(2);
112
+ prefix[0] = RTSP_FRAME_MAGIC;
113
+ if (pt === audioPt) {
114
+ prefix[1] = 0;
115
+ }
116
+ else if (pt === videoPt) {
117
+ prefix[1] = 2;
118
+ }
119
+ header = Buffer.concat([prefix, header]);
122
120
 
123
- const chunk: StreamChunk = {
124
- startStream,
125
- chunks: [header, data],
126
- type,
127
- };
121
+ if (pt === audioPt)
122
+ type = inputAudioCodec;
123
+ else if (pt === videoPt)
124
+ type = inputVideoCodec;
125
+ }
128
126
 
129
- if (!inputVideoResolution) {
130
- const sps = findH264NaluType(chunk, H264_NAL_TYPE_SPS);
131
- if (sps) {
132
- try {
133
- const parsedSps = spsParse(sps);
134
- inputVideoResolution = getSpsResolution(parsedSps);
135
- console.log(inputVideoResolution);
136
- console.log('parsed bitstream sps', parsedSps);
137
- }
138
- catch (e) {
139
- console.warn('sps parsing failed');
140
- inputVideoResolution = {
141
- width: NaN,
142
- height: NaN,
127
+ const chunk: StreamChunk = {
128
+ startStream,
129
+ chunks: [header, data],
130
+ type,
131
+ };
132
+
133
+ if (!inputVideoResolution) {
134
+ const sps = findH264NaluType(chunk, H264_NAL_TYPE_SPS);
135
+ if (sps) {
136
+ try {
137
+ const parsedSps = spsParse(sps);
138
+ inputVideoResolution = getSpsResolution(parsedSps);
139
+ console.log(inputVideoResolution);
140
+ console.log('parsed bitstream sps', parsedSps);
141
+ }
142
+ catch (e) {
143
+ console.warn('sps parsing failed');
144
+ inputVideoResolution = {
145
+ width: NaN,
146
+ height: NaN,
147
+ }
143
148
  }
144
149
  }
145
150
  }
146
- }
147
151
 
148
- events.emit('rtsp', chunk);
149
- resetActivityTimer();
150
- }
152
+ events.emit('rtsp', chunk);
153
+ resetActivityTimer();
154
+ }
155
+ });
151
156
  })()
152
- .catch(e => {
153
- throw e;
154
- })
157
+ .catch(e => {
158
+ throw e;
159
+ })
155
160
  .finally(kill);
156
161
 
157
162