@scrypted/prebuffer-mixin 0.1.258 → 0.1.261
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 +3 -3
- package/src/main.ts +61 -172
- package/src/rfc4571.ts +67 -113
- package/src/rtsp-session.ts +251 -0
- package/src/sps-resolution.ts +50 -0
- package/src/stream-settings.ts +1 -1
- package/src/sps-parser/bitstream.ts +0 -75
- package/src/sps-parser/index.ts +0 -237
- package/src/sps-parser/vui.ts +0 -126
package/src/main.ts
CHANGED
|
@@ -2,20 +2,19 @@
|
|
|
2
2
|
import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-provider';
|
|
3
3
|
import { getH264EncoderArgs, LIBX264_ENCODER_TITLE } from '@scrypted/common/src/ffmpeg-hardware-acceleration';
|
|
4
4
|
import { handleRebroadcasterClient, ParserOptions, ParserSession, startParserSession } from '@scrypted/common/src/ffmpeg-rebroadcast';
|
|
5
|
-
import { closeQuiet,
|
|
5
|
+
import { closeQuiet, listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
|
|
6
6
|
import { readLength } from '@scrypted/common/src/read-stream';
|
|
7
|
-
import { createRtspParser, findH264NaluType, H264_NAL_TYPE_IDR, H264_NAL_TYPE_SEI,
|
|
7
|
+
import { createRtspParser, findH264NaluType, H264_NAL_TYPE_IDR, H264_NAL_TYPE_SEI, RtspServer } from '@scrypted/common/src/rtsp-server';
|
|
8
8
|
import { addTrackControls, parseSdp } from '@scrypted/common/src/sdp-utils';
|
|
9
9
|
import { StorageSettings } from '@scrypted/common/src/settings';
|
|
10
10
|
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "@scrypted/common/src/settings-mixin";
|
|
11
|
-
import { sleep } from '@scrypted/common/src/sleep';
|
|
12
11
|
import { createFragmentedMp4Parser, createMpegTsParser, StreamChunk, StreamParser } from '@scrypted/common/src/stream-parser';
|
|
13
12
|
import sdk, { BufferConverter, DeviceProvider, FFmpegInput, MediaObject, MediaStreamOptions, MixinProvider, RequestMediaStreamOptions, ResponseMediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, VideoCameraConfiguration } from '@scrypted/sdk';
|
|
14
13
|
import crypto from 'crypto';
|
|
15
|
-
import dgram from 'dgram';
|
|
16
14
|
import net from 'net';
|
|
17
15
|
import { Duplex } from 'stream';
|
|
18
|
-
import { connectRFC4571Parser,
|
|
16
|
+
import { connectRFC4571Parser, startRFC4571Parser } from './rfc4571';
|
|
17
|
+
import { startRtspSession } from './rtsp-session';
|
|
19
18
|
import { createStreamSettings, getPrebufferedStreams } from './stream-settings';
|
|
20
19
|
import { getTranscodeMixinProviderId, REBROADCAST_MIXIN_INTERFACE_TOKEN, TranscodeMixinProvider, TRANSCODE_MIXIN_PROVIDER_NATIVE_ID } from './transcode-settings';
|
|
21
20
|
|
|
@@ -267,7 +266,7 @@ class PrebufferSession {
|
|
|
267
266
|
|
|
268
267
|
return {
|
|
269
268
|
rtspMode: mode?.startsWith('RTSP'),
|
|
270
|
-
muxingMp4: !rtspMode
|
|
269
|
+
muxingMp4: !rtspMode,
|
|
271
270
|
};
|
|
272
271
|
}
|
|
273
272
|
|
|
@@ -306,7 +305,7 @@ class PrebufferSession {
|
|
|
306
305
|
}
|
|
307
306
|
);
|
|
308
307
|
|
|
309
|
-
const
|
|
308
|
+
const addFFmpegAudioSettings = () => {
|
|
310
309
|
settings.push(
|
|
311
310
|
{
|
|
312
311
|
title: 'Audio Codec Transcoding',
|
|
@@ -322,6 +321,11 @@ class PrebufferSession {
|
|
|
322
321
|
TRANSCODE_AUDIO_DESCRIPTION,
|
|
323
322
|
],
|
|
324
323
|
},
|
|
324
|
+
);
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const addFFmpegInputSettings = () => {
|
|
328
|
+
settings.push(
|
|
325
329
|
{
|
|
326
330
|
title: 'FFmpeg Input Arguments Prefix',
|
|
327
331
|
group,
|
|
@@ -337,7 +341,9 @@ class PrebufferSession {
|
|
|
337
341
|
combobox: true,
|
|
338
342
|
},
|
|
339
343
|
)
|
|
340
|
-
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let usingFFmpeg = muxingMp4;
|
|
341
347
|
|
|
342
348
|
if (this.canUseRtspParser(this.advertisedMediaStreamOptions)) {
|
|
343
349
|
const canUseScryptedParser = rtspMode;
|
|
@@ -349,13 +355,15 @@ class PrebufferSession {
|
|
|
349
355
|
SCRYPTED_PARSER_UDP,
|
|
350
356
|
] : [];
|
|
351
357
|
|
|
358
|
+
const currentParser = this.storage.getItem(this.rtspParserKey) || STRING_DEFAULT;
|
|
359
|
+
|
|
352
360
|
settings.push(
|
|
353
361
|
{
|
|
354
362
|
key: this.rtspParserKey,
|
|
355
363
|
group,
|
|
356
364
|
title: 'RTSP Parser',
|
|
357
365
|
description: `The RTSP Parser used to read the stream. The default is "${defaultValue}" for this container.`,
|
|
358
|
-
value:
|
|
366
|
+
value: currentParser,
|
|
359
367
|
choices: [
|
|
360
368
|
STRING_DEFAULT,
|
|
361
369
|
...scryptedOptions,
|
|
@@ -364,9 +372,17 @@ class PrebufferSession {
|
|
|
364
372
|
],
|
|
365
373
|
}
|
|
366
374
|
);
|
|
375
|
+
|
|
376
|
+
if (!(currentParser === STRING_DEFAULT ? defaultValue : currentParser).includes('Scrypted')) {
|
|
377
|
+
usingFFmpeg = true;
|
|
378
|
+
}
|
|
367
379
|
}
|
|
368
|
-
|
|
369
|
-
|
|
380
|
+
|
|
381
|
+
if (muxingMp4) {
|
|
382
|
+
addFFmpegAudioSettings();
|
|
383
|
+
}
|
|
384
|
+
if (usingFFmpeg) {
|
|
385
|
+
addFFmpegInputSettings();
|
|
370
386
|
}
|
|
371
387
|
|
|
372
388
|
if (session) {
|
|
@@ -666,160 +682,23 @@ class PrebufferSession {
|
|
|
666
682
|
let { parser, isDefault } = this.getParser(rtspMode, sessionMso);
|
|
667
683
|
usingScryptedParser = parser === SCRYPTED_PARSER_TCP || parser === SCRYPTED_PARSER_UDP;
|
|
668
684
|
if (isDefault && (parser === SCRYPTED_PARSER_TCP || parser === SCRYPTED_PARSER_UDP) && this.getLastH264Probe().seiDetected) {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
685
|
+
if (sessionMso.tool === 'scrypted') {
|
|
686
|
+
this.console.warn('SEI packet detected was in video stream, but stream is marked safe by Scrypted. The Default Scrypted RTSP Parser will be used. This can be overriden by setting the RTSP Parser to Scrypted.');
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
this.console.warn('SEI packet detected was in video stream, the Default Scrypted RTSP Parser will not be used. Falling back to FFmpeg. This can be overriden by setting the RTSP Parser to Scrypted.');
|
|
690
|
+
usingScryptedParser = false;
|
|
691
|
+
parser = FFMPEG_PARSER_TCP;
|
|
692
|
+
}
|
|
672
693
|
}
|
|
673
694
|
|
|
674
695
|
if (usingScryptedParser) {
|
|
675
|
-
this.console.
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
closeQuiet(server);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
try {
|
|
686
|
-
rtspClient.requestTimeout = 10000;
|
|
687
|
-
await rtspClient.options();
|
|
688
|
-
const sdpResponse = await rtspClient.describe();
|
|
689
|
-
const contentBase = sdpResponse.headers['content-base'];
|
|
690
|
-
if (contentBase) {
|
|
691
|
-
const url = new URL(contentBase);
|
|
692
|
-
const existing = new URL(rtspClient.url);
|
|
693
|
-
url.username = existing.username;
|
|
694
|
-
url.password = existing.password;
|
|
695
|
-
rtspClient.url = url.toString();
|
|
696
|
-
}
|
|
697
|
-
let sdp = sdpResponse.body.toString().trim();
|
|
698
|
-
this.console.log('sdp', sdp);
|
|
699
|
-
|
|
700
|
-
const parsedSdp = parseSdp(sdp);
|
|
701
|
-
let channel = 0;
|
|
702
|
-
const mapping: RtspChannelCodecMapping = {};
|
|
703
|
-
const useUdp = parser === SCRYPTED_PARSER_UDP;
|
|
704
|
-
let udpSessionTimeout: number;
|
|
705
|
-
const checkUdpSessionTimeout = (headers: { [key: string]: string }) => {
|
|
706
|
-
if (useUdp && headers.session && !udpSessionTimeout) {
|
|
707
|
-
const sessionDict = parseSemicolonDelimited(headers.session);
|
|
708
|
-
udpSessionTimeout = parseInt(sessionDict['timeout']);
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
const doSetup = async (control: string, codec: string) => {
|
|
713
|
-
let setupChannel = channel;
|
|
714
|
-
let udp: dgram.Socket;
|
|
715
|
-
if (useUdp) {
|
|
716
|
-
const rtspChannel = channel;
|
|
717
|
-
const { port, server } = await createBindZero();
|
|
718
|
-
udp = server;
|
|
719
|
-
servers.push(server);
|
|
720
|
-
setupChannel = port;
|
|
721
|
-
server.on('message', data => {
|
|
722
|
-
const prefix = Buffer.alloc(4);
|
|
723
|
-
prefix.writeUInt8(RTSP_FRAME_MAGIC, 0);
|
|
724
|
-
prefix.writeUInt8(rtspChannel, 1);
|
|
725
|
-
prefix.writeUInt16BE(data.length, 2);
|
|
726
|
-
const chunk: StreamChunk = {
|
|
727
|
-
chunks: [prefix, data],
|
|
728
|
-
type: codec,
|
|
729
|
-
};
|
|
730
|
-
session?.emit('rtsp', chunk);
|
|
731
|
-
session?.resetActivityTimer?.();
|
|
732
|
-
})
|
|
733
|
-
}
|
|
734
|
-
const setupResult = await rtspClient.setup(setupChannel, control, useUdp);
|
|
735
|
-
checkUdpSessionTimeout(setupResult.headers);
|
|
736
|
-
|
|
737
|
-
if (udp) {
|
|
738
|
-
const punch = Buffer.alloc(1);
|
|
739
|
-
const transport = setupResult.headers['transport'];
|
|
740
|
-
const match = transport.match(/.*?server_port=([0-9]+)-([0-9]+)/);
|
|
741
|
-
const [_, rtp, rtcp] = match;
|
|
742
|
-
const { hostname } = new URL(rtspClient.url);
|
|
743
|
-
udp.send(punch, parseInt(rtp), hostname)
|
|
744
|
-
|
|
745
|
-
mapping[channel] = codec;
|
|
746
|
-
}
|
|
747
|
-
else {
|
|
748
|
-
if (setupResult.interleaved)
|
|
749
|
-
mapping[setupResult.interleaved.begin] = codec;
|
|
750
|
-
else
|
|
751
|
-
mapping[channel] = codec;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
channel += 2;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
let setupVideoSection = false;
|
|
758
|
-
|
|
759
|
-
parsedSdp.msections = parsedSdp.msections.filter(section => {
|
|
760
|
-
if (section.type === 'video') {
|
|
761
|
-
if (setupVideoSection) {
|
|
762
|
-
this.console.warn('additional video section found. skipping.');
|
|
763
|
-
return false;
|
|
764
|
-
}
|
|
765
|
-
setupVideoSection = true;
|
|
766
|
-
}
|
|
767
|
-
else if (section.type !== 'audio') {
|
|
768
|
-
this.console.warn('unknown section', section.type);
|
|
769
|
-
return false;
|
|
770
|
-
}
|
|
771
|
-
else if (audioSoftMuted) {
|
|
772
|
-
return false;
|
|
773
|
-
}
|
|
774
|
-
return true;
|
|
775
|
-
});
|
|
776
|
-
|
|
777
|
-
for (const section of parsedSdp.msections) {
|
|
778
|
-
await doSetup(section.control, section.codec)
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
// sdp may contain multiple audio/video sections. take only the first video section.
|
|
782
|
-
sdp = [...parsedSdp.header.lines, ...parsedSdp.msections.map(msection => msection.lines).flat()].join('\r\n');
|
|
783
|
-
|
|
784
|
-
this.sdp = Promise.resolve(sdp);
|
|
785
|
-
|
|
786
|
-
const play = await rtspClient.play();
|
|
787
|
-
checkUdpSessionTimeout(play.headers);
|
|
788
|
-
|
|
789
|
-
requeueRtspVideoData(rtspClient);
|
|
790
|
-
|
|
791
|
-
session = startRFC4571Parser(this.console, rtspClient.client, sdp, ffmpegInput.mediaStreamOptions, rbo, {
|
|
792
|
-
channelMap: mapping,
|
|
793
|
-
rtspClient,
|
|
794
|
-
udpSessionTimeout,
|
|
795
|
-
});
|
|
796
|
-
const sessionKill = session.kill.bind(session);
|
|
797
|
-
let issuedTeardown = false;
|
|
798
|
-
session.kill = async () => {
|
|
799
|
-
try {
|
|
800
|
-
cleanupServers();
|
|
801
|
-
// issue a teardown to upstream to close gracefully but don't rely on it responding.
|
|
802
|
-
if (!issuedTeardown) {
|
|
803
|
-
issuedTeardown = true;
|
|
804
|
-
rtspClient.writeTeardown();
|
|
805
|
-
}
|
|
806
|
-
await sleep(500);
|
|
807
|
-
}
|
|
808
|
-
finally {
|
|
809
|
-
rtspClient.client.destroy();
|
|
810
|
-
sessionKill();
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
if (!session.isActive)
|
|
814
|
-
throw new Error('parser was killed before rtsp client started');
|
|
815
|
-
|
|
816
|
-
rtspClient.client.on('close', () => session.kill());
|
|
817
|
-
}
|
|
818
|
-
catch (e) {
|
|
819
|
-
cleanupServers();
|
|
820
|
-
rtspClient.client.destroy();
|
|
821
|
-
throw e;
|
|
822
|
-
}
|
|
696
|
+
session = await startRtspSession(this.console, ffmpegInput.url, ffmpegInput.mediaStreamOptions, {
|
|
697
|
+
useUdp: parser === SCRYPTED_PARSER_UDP,
|
|
698
|
+
audioSoftMuted,
|
|
699
|
+
rtspRequestTimeout: 10000,
|
|
700
|
+
});
|
|
701
|
+
this.sdp = session.sdp.then(buffers => Buffer.concat(buffers).toString());
|
|
823
702
|
}
|
|
824
703
|
else {
|
|
825
704
|
if (parser === FFMPEG_PARSER_UDP)
|
|
@@ -1004,6 +883,7 @@ class PrebufferSession {
|
|
|
1004
883
|
}
|
|
1005
884
|
|
|
1006
885
|
async handleRebroadcasterClient(options: {
|
|
886
|
+
requestedContainer: string,
|
|
1007
887
|
isActiveClient: boolean,
|
|
1008
888
|
container: PrebufferParsers,
|
|
1009
889
|
session: ParserSession<PrebufferParsers>,
|
|
@@ -1051,7 +931,16 @@ class PrebufferSession {
|
|
|
1051
931
|
session.once('killed', cleanup);
|
|
1052
932
|
|
|
1053
933
|
const prebufferContainer: PrebufferStreamChunk[] = this.prebuffers[container];
|
|
1054
|
-
if
|
|
934
|
+
// if the requested container or the source container is not rtsp, use an exact seek.
|
|
935
|
+
// this works better when the requested container is mp4, and rtsp is the source.
|
|
936
|
+
// if starting on a sync frame, ffmpeg will skip the first segment while initializing
|
|
937
|
+
// on live sources like rtsp. the buffer before the sync frame stream will be enough
|
|
938
|
+
// for ffmpeg to analyze and start up in time for the sync frame.
|
|
939
|
+
// may be worth considering playing with a few other things to avoid this:
|
|
940
|
+
// mpeg-ts as a container (would need to write a muxer)
|
|
941
|
+
// specifying the buffer before the sync frame with probesize.
|
|
942
|
+
if (container !== 'rtsp'
|
|
943
|
+
|| (options?.requestedContainer && options?.requestedContainer !== 'rtsp')) {
|
|
1055
944
|
for (const chunk of prebufferContainer) {
|
|
1056
945
|
if (chunk.time < now - requestedPrebuffer)
|
|
1057
946
|
continue;
|
|
@@ -1099,17 +988,11 @@ class PrebufferSession {
|
|
|
1099
988
|
requestedPrebuffer = idrInterval * 1.5;
|
|
1100
989
|
}
|
|
1101
990
|
|
|
1102
|
-
const { rtspMode } = this.getRebroadcastContainer();
|
|
991
|
+
const { rtspMode, muxingMp4 } = this.getRebroadcastContainer();
|
|
1103
992
|
const defaultContainer = rtspMode ? 'rtsp' : 'mpegts';
|
|
1104
993
|
|
|
1105
994
|
let container: PrebufferParsers = this.parsers[options?.container] ? options?.container as PrebufferParsers : defaultContainer;
|
|
1106
995
|
|
|
1107
|
-
// If a mp4 prebuffer was explicitly requested, but an mp4 prebuffer is not available (rtsp mode),
|
|
1108
|
-
// rewind a little bit earlier to gaurantee a valid full segment of that length is sent.
|
|
1109
|
-
if (requestedPrebuffer && container !== 'mp4' && options?.container === 'mp4') {
|
|
1110
|
-
requestedPrebuffer += idrInterval * 1.5;
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
996
|
const mediaStreamOptions: ResponseMediaStreamOptions = session.negotiateMediaStream(options);
|
|
1114
997
|
let sdp = await this.sdp;
|
|
1115
998
|
|
|
@@ -1120,7 +1003,11 @@ class PrebufferSession {
|
|
|
1120
1003
|
|
|
1121
1004
|
if (container === 'rtsp') {
|
|
1122
1005
|
const parsedSdp = parseSdp(sdp);
|
|
1123
|
-
|
|
1006
|
+
const videoSection = parsedSdp.msections.find(msection => msection.codec && msection.codec === mediaStreamOptions.video?.codec) || parsedSdp.msections.find(msection => msection.type === 'video');
|
|
1007
|
+
let audioSection = parsedSdp.msections.find(msection => msection.codec && msection.codec === mediaStreamOptions.audio?.codec) || parsedSdp.msections.find(msection => msection.type === 'audio');
|
|
1008
|
+
if (mediaStreamOptions.audio === null)
|
|
1009
|
+
audioSection = undefined;
|
|
1010
|
+
parsedSdp.msections = parsedSdp.msections.filter(msection => msection === videoSection || msection === audioSection);
|
|
1124
1011
|
const filterPrebufferAudio = options?.prebuffer === undefined;
|
|
1125
1012
|
const videoCodec = parsedSdp.msections.find(msection => msection.type === 'video')?.codec;
|
|
1126
1013
|
sdp = parsedSdp.toSdp();
|
|
@@ -1166,6 +1053,7 @@ class PrebufferSession {
|
|
|
1166
1053
|
const isActiveClient = options?.refresh !== false;
|
|
1167
1054
|
|
|
1168
1055
|
this.handleRebroadcasterClient({
|
|
1056
|
+
requestedContainer: options?.container,
|
|
1169
1057
|
isActiveClient,
|
|
1170
1058
|
container,
|
|
1171
1059
|
requestedPrebuffer,
|
|
@@ -1181,7 +1069,7 @@ class PrebufferSession {
|
|
|
1181
1069
|
if (this.audioDisabled) {
|
|
1182
1070
|
mediaStreamOptions.audio = null;
|
|
1183
1071
|
}
|
|
1184
|
-
else if (reencodeAudio) {
|
|
1072
|
+
else if (reencodeAudio && muxingMp4) {
|
|
1185
1073
|
mediaStreamOptions.audio = {
|
|
1186
1074
|
codec: 'aac',
|
|
1187
1075
|
encoder: 'aac',
|
|
@@ -1623,6 +1511,7 @@ export class RebroadcastPlugin extends AutoenableMixinProvider implements MixinP
|
|
|
1623
1511
|
const requestedPrebuffer = Math.max(4000, (idrInterval || 4000)) * 1.5;
|
|
1624
1512
|
|
|
1625
1513
|
prebufferSession.handleRebroadcasterClient({
|
|
1514
|
+
requestedContainer: 'rtsp',
|
|
1626
1515
|
isActiveClient: true,
|
|
1627
1516
|
container: 'rtsp',
|
|
1628
1517
|
session,
|
package/src/rfc4571.ts
CHANGED
|
@@ -1,14 +1,59 @@
|
|
|
1
1
|
import { cloneDeep } from "@scrypted/common/src/clone-deep";
|
|
2
2
|
import { ParserOptions, ParserSession, setupActivityTimer } from "@scrypted/common/src/ffmpeg-rebroadcast";
|
|
3
|
-
import { read16BELengthLoop
|
|
4
|
-
import { findH264NaluType, H264_NAL_TYPE_SPS,
|
|
3
|
+
import { read16BELengthLoop } from "@scrypted/common/src/read-stream";
|
|
4
|
+
import { findH264NaluType, H264_NAL_TYPE_SPS, RTSP_FRAME_MAGIC } from "@scrypted/common/src/rtsp-server";
|
|
5
5
|
import { parseSdp } from "@scrypted/common/src/sdp-utils";
|
|
6
6
|
import { sleep } from "@scrypted/common/src/sleep";
|
|
7
7
|
import { StreamChunk } from "@scrypted/common/src/stream-parser";
|
|
8
|
-
import { ResponseMediaStreamOptions } from "@scrypted/sdk";
|
|
8
|
+
import { MediaStreamOptions, ResponseMediaStreamOptions } from "@scrypted/sdk";
|
|
9
|
+
import { parse as spsParse } from "h264-sps-parser";
|
|
9
10
|
import net from 'net';
|
|
10
11
|
import { EventEmitter, Readable } from "stream";
|
|
11
|
-
import { getSpsResolution
|
|
12
|
+
import { getSpsResolution } from "./sps-resolution";
|
|
13
|
+
|
|
14
|
+
export function negotiateMediaStream(sdp: string, mediaStreamOptions: MediaStreamOptions, inputVideoCodec: string, inputAudioCodec: string, requestMediaStream: MediaStreamOptions) {
|
|
15
|
+
const parsedSdp = parseSdp(sdp);
|
|
16
|
+
const ret: ResponseMediaStreamOptions = cloneDeep(mediaStreamOptions) || {
|
|
17
|
+
id: undefined,
|
|
18
|
+
name: undefined,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// if the source doesn't provide a video codec, dummy one up
|
|
22
|
+
if (ret.video === undefined)
|
|
23
|
+
ret.video = {};
|
|
24
|
+
|
|
25
|
+
// the requests does not want video
|
|
26
|
+
if (requestMediaStream?.video === null)
|
|
27
|
+
ret.video = null;
|
|
28
|
+
|
|
29
|
+
if (ret.video)
|
|
30
|
+
ret.video.codec = inputVideoCodec;
|
|
31
|
+
|
|
32
|
+
// some rtsp like unifi offer alternate audio tracks (aac and opus).
|
|
33
|
+
if (requestMediaStream?.audio?.codec && requestMediaStream?.audio?.codec !== inputAudioCodec) {
|
|
34
|
+
const alternateAudio = parsedSdp.msections.find(msection => msection.type === 'audio' && msection.codec === requestMediaStream?.audio?.codec);
|
|
35
|
+
if (alternateAudio) {
|
|
36
|
+
ret.audio = {
|
|
37
|
+
codec: requestMediaStream?.audio?.codec,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return ret;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// reported codecs may be wrong/cached/etc, so before blindly copying the audio codec info,
|
|
45
|
+
// verify what was found.
|
|
46
|
+
if (ret?.audio?.codec === inputAudioCodec) {
|
|
47
|
+
ret.audio = mediaStreamOptions?.audio;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
ret.audio = {
|
|
51
|
+
codec: inputAudioCodec,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return ret;
|
|
56
|
+
}
|
|
12
57
|
|
|
13
58
|
export function connectRFC4571Parser(url: string) {
|
|
14
59
|
const u = new URL(url);
|
|
@@ -19,21 +64,7 @@ export function connectRFC4571Parser(url: string) {
|
|
|
19
64
|
return socket;
|
|
20
65
|
}
|
|
21
66
|
|
|
22
|
-
export
|
|
23
|
-
|
|
24
|
-
const RTSP_BUFFER = Buffer.from('RTSP');
|
|
25
|
-
|
|
26
|
-
export function requeueRtspVideoData(rtspClient: RtspClient) {
|
|
27
|
-
const videoData = rtspClient.rfc4571.read();
|
|
28
|
-
if (videoData)
|
|
29
|
-
rtspClient.client.unshift(videoData);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function startRFC4571Parser(console: Console, socket: Readable, sdp: string, mediaStreamOptions: ResponseMediaStreamOptions, options?: ParserOptions<"rtsp">, rtspOptions?: {
|
|
33
|
-
channelMap: RtspChannelCodecMapping,
|
|
34
|
-
rtspClient: RtspClient,
|
|
35
|
-
udpSessionTimeout: number,
|
|
36
|
-
}): ParserSession<"rtsp"> {
|
|
67
|
+
export function startRFC4571Parser(console: Console, socket: Readable, sdp: string, mediaStreamOptions: ResponseMediaStreamOptions, options?: ParserOptions<"rtsp">): ParserSession<"rtsp"> {
|
|
37
68
|
let isActive = true;
|
|
38
69
|
const events = new EventEmitter();
|
|
39
70
|
// need this to prevent kill from throwing due to uncaught Error during cleanup
|
|
@@ -98,65 +129,27 @@ export function startRFC4571Parser(console: Console, socket: Readable, sdp: stri
|
|
|
98
129
|
// don't start parsing until next tick, to prevent missed packets.
|
|
99
130
|
await sleep(0);
|
|
100
131
|
|
|
101
|
-
if (rtspOptions?.udpSessionTimeout) {
|
|
102
|
-
while (true) {
|
|
103
|
-
await sleep(rtspOptions.udpSessionTimeout * 1000 - 5000);
|
|
104
|
-
await rtspOptions.rtspClient.getParameter();
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const headerLength = rtspOptions?.channelMap ? 4 : 2;
|
|
109
|
-
const offset = rtspOptions?.channelMap ? 2 : 0;
|
|
110
|
-
const skipHeader = (header: Buffer, resumeRead: () => void) => {
|
|
111
|
-
if (!rtspOptions?.rtspClient.needKeepAlive)
|
|
112
|
-
return false;
|
|
113
|
-
|
|
114
|
-
socket.unshift(header);
|
|
115
|
-
rtspOptions.rtspClient.needKeepAlive = false;
|
|
116
|
-
rtspOptions.rtspClient.getParameter().then(() => {
|
|
117
|
-
requeueRtspVideoData(rtspOptions.rtspClient);
|
|
118
|
-
resumeRead();
|
|
119
|
-
})
|
|
120
|
-
.catch(e => {
|
|
121
|
-
console.error('error during RTSP keepalive', e);
|
|
122
|
-
kill();
|
|
123
|
-
});
|
|
124
|
-
return true;
|
|
125
|
-
}
|
|
126
132
|
await read16BELengthLoop(socket, {
|
|
127
|
-
headerLength,
|
|
128
|
-
|
|
129
|
-
skipHeader,
|
|
133
|
+
headerLength: 2,
|
|
134
|
+
skipHeader: undefined,
|
|
130
135
|
callback: (header, data) => {
|
|
131
136
|
let type: string;
|
|
137
|
+
const pt = data[1] & 0x7f;
|
|
132
138
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const rtpType = rtspOptions?.channelMap[channel - 1];
|
|
138
|
-
if (rtpType)
|
|
139
|
-
type = `rtcp-${rtpType}`;
|
|
140
|
-
}
|
|
139
|
+
const prefix = Buffer.alloc(2);
|
|
140
|
+
prefix[0] = RTSP_FRAME_MAGIC;
|
|
141
|
+
if (pt === audioPt) {
|
|
142
|
+
prefix[1] = 0;
|
|
141
143
|
}
|
|
142
|
-
else {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const prefix = Buffer.alloc(2);
|
|
146
|
-
prefix[0] = RTSP_FRAME_MAGIC;
|
|
147
|
-
if (pt === audioPt) {
|
|
148
|
-
prefix[1] = 0;
|
|
149
|
-
}
|
|
150
|
-
else if (pt === videoPt) {
|
|
151
|
-
prefix[1] = 2;
|
|
152
|
-
}
|
|
153
|
-
header = Buffer.concat([prefix, header]);
|
|
154
|
-
|
|
155
|
-
if (pt === audioPt)
|
|
156
|
-
type = inputAudioCodec;
|
|
157
|
-
else if (pt === videoPt)
|
|
158
|
-
type = inputVideoCodec;
|
|
144
|
+
else if (pt === videoPt) {
|
|
145
|
+
prefix[1] = 2;
|
|
159
146
|
}
|
|
147
|
+
header = Buffer.concat([prefix, header]);
|
|
148
|
+
|
|
149
|
+
if (pt === audioPt)
|
|
150
|
+
type = inputAudioCodec;
|
|
151
|
+
else if (pt === videoPt)
|
|
152
|
+
type = inputVideoCodec;
|
|
160
153
|
|
|
161
154
|
const chunk: StreamChunk = {
|
|
162
155
|
chunks: [header, data],
|
|
@@ -209,46 +202,7 @@ export function startRFC4571Parser(console: Console, socket: Readable, sdp: stri
|
|
|
209
202
|
killed,
|
|
210
203
|
resetActivityTimer,
|
|
211
204
|
negotiateMediaStream: (requestMediaStream) => {
|
|
212
|
-
|
|
213
|
-
id: undefined,
|
|
214
|
-
name: undefined,
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
// if the source doesn't provide a video codec, dummy one up
|
|
218
|
-
if (ret.video === undefined)
|
|
219
|
-
ret.video = {};
|
|
220
|
-
|
|
221
|
-
// the requests does not want video
|
|
222
|
-
if (requestMediaStream?.video === null)
|
|
223
|
-
ret.video = null;
|
|
224
|
-
|
|
225
|
-
if (ret.video)
|
|
226
|
-
ret.video.codec = inputVideoCodec;
|
|
227
|
-
|
|
228
|
-
// some rtsp like unifi offer alternate audio tracks (aac and opus).
|
|
229
|
-
if (requestMediaStream?.audio?.codec && requestMediaStream?.audio?.codec !== inputAudioCodec) {
|
|
230
|
-
const alternateAudio = parsedSdp.msections.find(msection => msection.type === 'audio' && msection.codec === requestMediaStream?.audio?.codec);
|
|
231
|
-
if (alternateAudio) {
|
|
232
|
-
ret.audio = {
|
|
233
|
-
codec: requestMediaStream?.audio?.codec,
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
return ret;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// reported codecs may be wrong/cached/etc, so before blindly copying the audio codec info,
|
|
241
|
-
// verify what was found.
|
|
242
|
-
if (ret?.audio?.codec === inputAudioCodec) {
|
|
243
|
-
ret.audio = mediaStreamOptions?.audio;
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
ret.audio = {
|
|
247
|
-
codec: inputAudioCodec,
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return ret;
|
|
205
|
+
return negotiateMediaStream(sdp, mediaStreamOptions, inputVideoCodec, inputAudioCodec, requestMediaStream);
|
|
252
206
|
},
|
|
253
207
|
emit(container: 'rtsp', chunk: StreamChunk) {
|
|
254
208
|
events.emit(container, chunk);
|
|
@@ -267,4 +221,4 @@ export function startRFC4571Parser(console: Console, socket: Readable, sdp: stri
|
|
|
267
221
|
return this;
|
|
268
222
|
}
|
|
269
223
|
}
|
|
270
|
-
}
|
|
224
|
+
}
|