@scrypted/prebuffer-mixin 0.1.259 → 0.1.262
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 +57 -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)
|
|
@@ -851,7 +730,7 @@ class PrebufferSession {
|
|
|
851
730
|
return;
|
|
852
731
|
|
|
853
732
|
let { isDefault } = this.getParser(rtspMode, sessionMso);
|
|
854
|
-
if (!isDefault) {
|
|
733
|
+
if (!isDefault || sessionMso.tool === 'scrypted') {
|
|
855
734
|
this.console.warn('SEI packet detected while operating with Scrypted Parser. If there are issues streaming, consider using the Default parser.');
|
|
856
735
|
return;
|
|
857
736
|
}
|
|
@@ -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
|
|
|
@@ -1170,6 +1053,7 @@ class PrebufferSession {
|
|
|
1170
1053
|
const isActiveClient = options?.refresh !== false;
|
|
1171
1054
|
|
|
1172
1055
|
this.handleRebroadcasterClient({
|
|
1056
|
+
requestedContainer: options?.container,
|
|
1173
1057
|
isActiveClient,
|
|
1174
1058
|
container,
|
|
1175
1059
|
requestedPrebuffer,
|
|
@@ -1185,7 +1069,7 @@ class PrebufferSession {
|
|
|
1185
1069
|
if (this.audioDisabled) {
|
|
1186
1070
|
mediaStreamOptions.audio = null;
|
|
1187
1071
|
}
|
|
1188
|
-
else if (reencodeAudio) {
|
|
1072
|
+
else if (reencodeAudio && muxingMp4) {
|
|
1189
1073
|
mediaStreamOptions.audio = {
|
|
1190
1074
|
codec: 'aac',
|
|
1191
1075
|
encoder: 'aac',
|
|
@@ -1627,6 +1511,7 @@ export class RebroadcastPlugin extends AutoenableMixinProvider implements MixinP
|
|
|
1627
1511
|
const requestedPrebuffer = Math.max(4000, (idrInterval || 4000)) * 1.5;
|
|
1628
1512
|
|
|
1629
1513
|
prebufferSession.handleRebroadcasterClient({
|
|
1514
|
+
requestedContainer: 'rtsp',
|
|
1630
1515
|
isActiveClient: true,
|
|
1631
1516
|
container: 'rtsp',
|
|
1632
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
|
+
}
|