@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/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/main.ts +58 -53
- package/src/rfc4571.ts +59 -54
- package/vscode-profile-2022-04-17-16-14-09.cpuprofile +0 -1
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 {
|
|
7
|
+
import { safeKillFFmpeg } from '@scrypted/common/src/media-helpers';
|
|
7
8
|
import { readLength } from '@scrypted/common/src/read-stream';
|
|
8
|
-
import { createRtspParser,
|
|
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
|
-
|
|
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.
|
|
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: (
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
865
|
-
|
|
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 >
|
|
888
|
-
|
|
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
|
|
980
|
-
if (
|
|
982
|
+
for (const chunk of prebufferContainer) {
|
|
983
|
+
if (chunk.time < now - requestedPrebuffer)
|
|
981
984
|
continue;
|
|
982
985
|
|
|
983
|
-
safeWriteData(
|
|
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)
|
|
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, (
|
|
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 += (
|
|
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.
|
|
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
|
|
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?: () =>
|
|
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 =
|
|
88
|
+
const skipHeader = (header: Buffer, resumeRead: () => void) => {
|
|
89
89
|
if (header.toString() !== 'RTSP')
|
|
90
90
|
return false;
|
|
91
91
|
socket.unshift(header);
|
|
92
|
-
|
|
92
|
+
rtspOptions.handleRTSP().then(resumeRead);
|
|
93
93
|
return true;
|
|
94
94
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
108
|
+
else {
|
|
109
|
+
const pt = data[1] & 0x7f;
|
|
116
110
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
121
|
+
if (pt === audioPt)
|
|
122
|
+
type = inputAudioCodec;
|
|
123
|
+
else if (pt === videoPt)
|
|
124
|
+
type = inputVideoCodec;
|
|
125
|
+
}
|
|
128
126
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
152
|
+
events.emit('rtsp', chunk);
|
|
153
|
+
resetActivityTimer();
|
|
154
|
+
}
|
|
155
|
+
});
|
|
151
156
|
})()
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
.catch(e => {
|
|
158
|
+
throw e;
|
|
159
|
+
})
|
|
155
160
|
.finally(kill);
|
|
156
161
|
|
|
157
162
|
|