@js-toolkit/web-utils 1.67.0 → 1.68.0
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/EventEmitterListener.d.ts +3 -3
- package/EventEmitterListener.js +222 -1
- package/EventEmitterListener.utils.d.ts +24 -13
- package/EventEmitterListener.utils.js +41 -1
- package/EventListeners.js +58 -1
- package/FullscreenController.js +193 -1
- package/README.md +159 -20
- package/WakeLockController.js +76 -1
- package/base64ToDataUrl.js +3 -1
- package/blobToDataUrl.js +10 -1
- package/checkPermission.js +8 -1
- package/copyToClipboard.js +37 -1
- package/createLoop.js +30 -1
- package/createRafLoop.js +56 -1
- package/dataUrlToBlob.js +13 -1
- package/fromBase64.js +10 -1
- package/fullscreen.js +167 -1
- package/fullscreenUtils.js +37 -1
- package/getAspectRatio.js +8 -1
- package/getBrowserLanguage.js +10 -1
- package/getCurrentScriptUrl.js +4 -1
- package/getEventAwaiter.js +41 -1
- package/getGeoCoordinates.js +6 -1
- package/getGeoLocality.js +19 -1
- package/getInnerRect.js +8 -1
- package/getInnerXDimensions.js +9 -1
- package/getInnerYDimensions.js +9 -1
- package/getPinchZoomHandlers.js +134 -1
- package/getRandomID.js +4 -1
- package/getScreenSize.js +23 -1
- package/getSecondsCounter.js +47 -1
- package/iframe/getAutoConnector.js +251 -1
- package/iframe/getOriginFromMessage.js +3 -1
- package/iframe/isIframeLoaded.js +9 -1
- package/iframe/messages.d.ts +2 -2
- package/iframe/messages.js +50 -1
- package/iframe/utils.js +33 -1
- package/imageToBlob.js +20 -1
- package/isImageTypeSupported.js +8 -1
- package/isWebPSupported.js +15 -1
- package/loadImage.js +29 -1
- package/loadScript.d.ts +1 -1
- package/loadScript.js +67 -1
- package/media/Capabilities.js +44 -1
- package/media/MediaNotAttachedError.d.ts +1 -1
- package/media/MediaNotAttachedError.js +6 -1
- package/media/MediaStreamController.js +84 -1
- package/media/PipController.d.ts +2 -2
- package/media/PipController.js +140 -1
- package/media/TextTracksController/TextTracksController.d.ts +0 -3
- package/media/TextTracksController/TextTracksController.js +251 -1
- package/media/TextTracksController/index.js +1 -1
- package/media/TextTracksController/utils.js +147 -1
- package/media/getDurationTime.js +3 -1
- package/media/getMediaSource.js +11 -1
- package/media/getSourceBuffer.js +5 -1
- package/media/isMediaSeekable.js +4 -1
- package/media/parseCueText.js +224 -1
- package/media/resetMedia.js +17 -1
- package/media/timeRanges.js +9 -1
- package/media/toggleNativeSubtitles.js +21 -1
- package/metrics/ga/DataLayerProxy.js +11 -1
- package/metrics/ga/getHandler.js +99 -1
- package/metrics/ga/types.js +1 -1
- package/metrics/types.js +1 -1
- package/metrics/yandex/DataLayerProxy.js +11 -1
- package/metrics/yandex/getHandler.js +63 -1
- package/metrics/yandex/types.js +2 -1
- package/onDOMReady.js +12 -1
- package/onPageReady.js +27 -1
- package/package.json +16 -13
- package/patchConsoleLogging.js +27 -1
- package/performance/getNavigationTiming.js +14 -1
- package/platform/Semver.js +23 -1
- package/platform/getChromeVersion.d.ts +2 -0
- package/platform/getChromeVersion.js +16 -0
- package/platform/getIOSVersion.js +18 -1
- package/platform/getPlatformInfo.js +49 -1
- package/platform/isAirPlayAvailable.js +3 -1
- package/platform/isAndroid.js +7 -1
- package/platform/isChrome.js +7 -1
- package/platform/isEMESupported.js +9 -1
- package/platform/isIOS.js +9 -1
- package/platform/isMSESupported.js +16 -1
- package/platform/isMacOS.js +12 -1
- package/platform/isMediaCapabilitiesSupported.js +6 -1
- package/platform/isMobile.js +16 -1
- package/platform/isMobileSimulation.js +7 -1
- package/platform/isSafari.js +14 -1
- package/platform/isScreenHDR.js +4 -1
- package/platform/isStandaloneApp.js +4 -1
- package/platform/isTelegramWebView.js +3 -1
- package/platform/isTouchSupported.js +4 -1
- package/preventDefault.d.ts +2 -2
- package/preventDefault.js +5 -1
- package/rafCallback.js +9 -1
- package/responsive/MediaQuery.js +40 -1
- package/responsive/MediaQueryListener.js +55 -1
- package/responsive/ViewSize.js +82 -1
- package/responsive/getViewSizeQueryMap.js +7 -1
- package/saveFileAs.js +22 -1
- package/serviceWorker/ServiceWorkerInstaller.d.ts +0 -1
- package/serviceWorker/ServiceWorkerInstaller.js +112 -1
- package/serviceWorker/utils.d.ts +1 -1
- package/serviceWorker/utils.js +86 -1
- package/stopPropagation.d.ts +2 -2
- package/stopPropagation.js +3 -1
- package/takeScreenshot.js +51 -1
- package/toBase64.js +9 -1
- package/toLocalPoint.js +9 -1
- package/types/index.js +2 -1
- package/types/refs.js +1 -1
- package/viewableTracker.js +69 -1
- package/webrtc/PeerConnection.js +212 -1
- package/webrtc/sdputils.js +417 -1
- package/ws/WSController.js +148 -1
package/webrtc/sdputils.js
CHANGED
|
@@ -1 +1,417 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-dynamic-delete */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-unnecessary-type-conversion */
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-use-before-define */
|
|
4
|
+
/* eslint-disable no-prototype-builtins, prefer-destructuring, no-param-reassign */
|
|
5
|
+
// This export function is used for logging.
|
|
6
|
+
export function trace(text) {
|
|
7
|
+
if (text.endsWith('\n')) {
|
|
8
|
+
text = text.substring(0, text.length - 1);
|
|
9
|
+
}
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
11
|
+
if (window.performance) {
|
|
12
|
+
const now = (window.performance.now() / 1000).toFixed(3);
|
|
13
|
+
console.log(`${now}: ${text}`);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
console.log(text);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function iceCandidateType(candidateStr) {
|
|
20
|
+
return candidateStr.split(' ')[7];
|
|
21
|
+
}
|
|
22
|
+
export function isValidIceCandidate({ candidate }, config) {
|
|
23
|
+
// const candidateStr = candidateObj.candidate;
|
|
24
|
+
// Always eat TCP candidates. Not needed in this context.
|
|
25
|
+
if (!candidate || candidate.includes('tcp')) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
// If we're trying to eat non-relay candidates, do that.
|
|
29
|
+
if (config?.iceTransportPolicy === 'relay' && iceCandidateType(candidate) !== 'relay') {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
function maybeSetOpusOptions(sdp, params) {
|
|
35
|
+
// Set Opus in Stereo, if stereo is true, unset it, if stereo is false, and
|
|
36
|
+
// do nothing if otherwise.
|
|
37
|
+
if (params.opusStereo === 'true') {
|
|
38
|
+
sdp = setCodecParam(sdp, 'opus/48000', 'stereo', '1');
|
|
39
|
+
}
|
|
40
|
+
else if (params.opusStereo === 'false') {
|
|
41
|
+
sdp = removeCodecParam(sdp, 'opus/48000', 'stereo');
|
|
42
|
+
}
|
|
43
|
+
// Set Opus FEC, if opusfec is true, unset it, if opusfec is false, and
|
|
44
|
+
// do nothing if otherwise.
|
|
45
|
+
if (params.opusFec === 'true') {
|
|
46
|
+
sdp = setCodecParam(sdp, 'opus/48000', 'useinbandfec', '1');
|
|
47
|
+
}
|
|
48
|
+
else if (params.opusFec === 'false') {
|
|
49
|
+
sdp = removeCodecParam(sdp, 'opus/48000', 'useinbandfec');
|
|
50
|
+
}
|
|
51
|
+
// Set Opus DTX, if opusdtx is true, unset it, if opusdtx is false, and
|
|
52
|
+
// do nothing if otherwise.
|
|
53
|
+
if (params.opusDtx === 'true') {
|
|
54
|
+
sdp = setCodecParam(sdp, 'opus/48000', 'usedtx', '1');
|
|
55
|
+
}
|
|
56
|
+
else if (params.opusDtx === 'false') {
|
|
57
|
+
sdp = removeCodecParam(sdp, 'opus/48000', 'usedtx');
|
|
58
|
+
}
|
|
59
|
+
// Set Opus maxplaybackrate, if requested.
|
|
60
|
+
if (params.opusMaxPbr) {
|
|
61
|
+
sdp = setCodecParam(sdp, 'opus/48000', 'maxplaybackrate', params.opusMaxPbr);
|
|
62
|
+
}
|
|
63
|
+
return sdp;
|
|
64
|
+
}
|
|
65
|
+
function maybeSetAudioSendBitRate(sdp, params) {
|
|
66
|
+
if (!params.audioSendBitrate) {
|
|
67
|
+
return sdp;
|
|
68
|
+
}
|
|
69
|
+
trace(`Prefer audio send bitrate: ${params.audioSendBitrate}`);
|
|
70
|
+
return preferBitRate(sdp, params.audioSendBitrate, 'audio');
|
|
71
|
+
}
|
|
72
|
+
function maybeSetAudioReceiveBitRate(sdp, params) {
|
|
73
|
+
if (!params.audioRecvBitrate) {
|
|
74
|
+
return sdp;
|
|
75
|
+
}
|
|
76
|
+
trace(`Prefer audio receive bitrate: ${params.audioRecvBitrate}`);
|
|
77
|
+
return preferBitRate(sdp, params.audioRecvBitrate, 'audio');
|
|
78
|
+
}
|
|
79
|
+
function maybeSetVideoSendBitRate(sdp, params) {
|
|
80
|
+
if (!params.videoSendBitrate) {
|
|
81
|
+
return sdp;
|
|
82
|
+
}
|
|
83
|
+
trace(`Prefer video send bitrate: ${params.videoSendBitrate}`);
|
|
84
|
+
return preferBitRate(sdp, params.videoSendBitrate, 'video');
|
|
85
|
+
}
|
|
86
|
+
function maybeSetVideoReceiveBitRate(sdp, params) {
|
|
87
|
+
if (!params.videoRecvBitrate) {
|
|
88
|
+
return sdp;
|
|
89
|
+
}
|
|
90
|
+
trace(`Prefer video receive bitrate: ${params.videoRecvBitrate}`);
|
|
91
|
+
return preferBitRate(sdp, params.videoRecvBitrate, 'video');
|
|
92
|
+
}
|
|
93
|
+
// Add a b=AS:bitrate line to the m=mediaType section.
|
|
94
|
+
function preferBitRate(sdp, bitrate, mediaType) {
|
|
95
|
+
const sdpLines = sdp.split('\r\n');
|
|
96
|
+
// Find m line for the given mediaType.
|
|
97
|
+
const mLineIndex = findLine(sdpLines, 'm=', mediaType);
|
|
98
|
+
if (mLineIndex === -1) {
|
|
99
|
+
trace('Failed to add bandwidth line to sdp, as no m-line found');
|
|
100
|
+
return sdp;
|
|
101
|
+
}
|
|
102
|
+
// Find next m-line if any.
|
|
103
|
+
let nextMLineIndex = findLineInRange(sdpLines, mLineIndex + 1, -1, 'm=');
|
|
104
|
+
if (nextMLineIndex === -1) {
|
|
105
|
+
nextMLineIndex = sdpLines.length;
|
|
106
|
+
}
|
|
107
|
+
// Find c-line corresponding to the m-line.
|
|
108
|
+
const cLineIndex = findLineInRange(sdpLines, mLineIndex + 1, nextMLineIndex, 'c=');
|
|
109
|
+
if (cLineIndex === -1) {
|
|
110
|
+
trace('Failed to add bandwidth line to sdp, as no c-line found');
|
|
111
|
+
return sdp;
|
|
112
|
+
}
|
|
113
|
+
// Check if bandwidth line already exists between c-line and next m-line.
|
|
114
|
+
const bLineIndex = findLineInRange(sdpLines, cLineIndex + 1, nextMLineIndex, 'b=AS');
|
|
115
|
+
if (bLineIndex >= 0) {
|
|
116
|
+
sdpLines.splice(bLineIndex, 1);
|
|
117
|
+
}
|
|
118
|
+
// Create the b (bandwidth) sdp line.
|
|
119
|
+
const bwLine = `b=AS:${bitrate}`;
|
|
120
|
+
// As per RFC 4566, the b line should follow after c-line.
|
|
121
|
+
sdpLines.splice(cLineIndex + 1, 0, bwLine);
|
|
122
|
+
sdp = sdpLines.join('\r\n');
|
|
123
|
+
return sdp;
|
|
124
|
+
}
|
|
125
|
+
// Add an a=fmtp: x-google-min-bitrate=kbps line, if videoSendInitialBitrate
|
|
126
|
+
// is specified. We'll also add a x-google-min-bitrate value, since the max
|
|
127
|
+
// must be >= the min.
|
|
128
|
+
function maybeSetVideoSendInitialBitRate(sdp, params) {
|
|
129
|
+
let initialBitrate = params.videoSendInitialBitrate;
|
|
130
|
+
if (!initialBitrate) {
|
|
131
|
+
return sdp;
|
|
132
|
+
}
|
|
133
|
+
// Validate the initial bitrate value.
|
|
134
|
+
let maxBitrate = initialBitrate;
|
|
135
|
+
const bitrate = params.videoSendBitrate;
|
|
136
|
+
if (bitrate) {
|
|
137
|
+
if (initialBitrate > bitrate) {
|
|
138
|
+
trace(`Clamping initial bitrate to max bitrate of ${bitrate} kbps.`);
|
|
139
|
+
initialBitrate = bitrate;
|
|
140
|
+
params.videoSendInitialBitrate = initialBitrate;
|
|
141
|
+
}
|
|
142
|
+
maxBitrate = bitrate;
|
|
143
|
+
}
|
|
144
|
+
const sdpLines = sdp.split('\r\n');
|
|
145
|
+
// Search for m line.
|
|
146
|
+
const mLineIndex = findLine(sdpLines, 'm=', 'video');
|
|
147
|
+
if (mLineIndex === -1) {
|
|
148
|
+
trace('Failed to find video m-line');
|
|
149
|
+
return sdp;
|
|
150
|
+
}
|
|
151
|
+
const codec = params.videoRecvCodec || '';
|
|
152
|
+
sdp = setCodecParam(sdp, codec, 'x-google-min-bitrate', params.videoSendInitialBitrate || '');
|
|
153
|
+
sdp = setCodecParam(sdp, codec, 'x-google-max-bitrate', maxBitrate.toString());
|
|
154
|
+
return sdp;
|
|
155
|
+
}
|
|
156
|
+
function removePayloadTypeFromMline(mLine, payloadType) {
|
|
157
|
+
const lines = mLine.split(' ');
|
|
158
|
+
const { length } = lines;
|
|
159
|
+
for (let i = 0; i < length; ++i) {
|
|
160
|
+
if (lines[i] === payloadType.toString()) {
|
|
161
|
+
lines.splice(i, 1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return lines.join(' ');
|
|
165
|
+
}
|
|
166
|
+
function removeCodecByName(sdpLines, codec) {
|
|
167
|
+
const index = findLine(sdpLines, 'a=rtpmap', codec);
|
|
168
|
+
if (index === -1) {
|
|
169
|
+
return sdpLines;
|
|
170
|
+
}
|
|
171
|
+
const payloadType = getCodecPayloadTypeFromLine(sdpLines[index]);
|
|
172
|
+
sdpLines.splice(index, 1);
|
|
173
|
+
// Search for the video m= line and remove the codec.
|
|
174
|
+
const mLineIndex = findLine(sdpLines, 'm=', 'video');
|
|
175
|
+
if (mLineIndex === -1) {
|
|
176
|
+
return sdpLines;
|
|
177
|
+
}
|
|
178
|
+
sdpLines[mLineIndex] = removePayloadTypeFromMline(sdpLines[mLineIndex], payloadType);
|
|
179
|
+
return sdpLines;
|
|
180
|
+
}
|
|
181
|
+
function removeCodecByPayloadType(sdpLines, payloadType) {
|
|
182
|
+
const index = findLine(sdpLines, 'a=rtpmap', payloadType.toString());
|
|
183
|
+
if (index === -1) {
|
|
184
|
+
return sdpLines;
|
|
185
|
+
}
|
|
186
|
+
sdpLines.splice(index, 1);
|
|
187
|
+
// Search for the video m= line and remove the codec.
|
|
188
|
+
const mLineIndex = findLine(sdpLines, 'm=', 'video');
|
|
189
|
+
if (mLineIndex === -1) {
|
|
190
|
+
return sdpLines;
|
|
191
|
+
}
|
|
192
|
+
sdpLines[mLineIndex] = removePayloadTypeFromMline(sdpLines[mLineIndex], payloadType);
|
|
193
|
+
return sdpLines;
|
|
194
|
+
}
|
|
195
|
+
function maybeRemoveVideoFec(sdp, params) {
|
|
196
|
+
if (params.videoFec !== 'false') {
|
|
197
|
+
return sdp;
|
|
198
|
+
}
|
|
199
|
+
let sdpLines = sdp.split('\r\n');
|
|
200
|
+
let index = findLine(sdpLines, 'a=rtpmap', 'red');
|
|
201
|
+
if (index === -1) {
|
|
202
|
+
return sdp;
|
|
203
|
+
}
|
|
204
|
+
const redPayloadType = getCodecPayloadTypeFromLine(sdpLines[index]);
|
|
205
|
+
sdpLines = removeCodecByPayloadType(sdpLines, redPayloadType);
|
|
206
|
+
sdpLines = removeCodecByName(sdpLines, 'ulpfec');
|
|
207
|
+
// Remove fmtp lines associated with red codec.
|
|
208
|
+
index = findLine(sdpLines, 'a=fmtp', redPayloadType.toString());
|
|
209
|
+
if (index === -1) {
|
|
210
|
+
return sdp;
|
|
211
|
+
}
|
|
212
|
+
const fmtpLine = parseFmtpLine(sdpLines[index]);
|
|
213
|
+
const rtxPayloadType = fmtpLine.pt;
|
|
214
|
+
if (rtxPayloadType == null) {
|
|
215
|
+
return sdp;
|
|
216
|
+
}
|
|
217
|
+
sdpLines.splice(index, 1);
|
|
218
|
+
sdpLines = removeCodecByPayloadType(sdpLines, rtxPayloadType);
|
|
219
|
+
return sdpLines.join('\r\n');
|
|
220
|
+
}
|
|
221
|
+
// Promotes |audioSendCodec| to be the first in the m=audio line, if set.
|
|
222
|
+
function maybePreferAudioSendCodec(sdp, params) {
|
|
223
|
+
return maybePreferCodec(sdp, 'audio', 'send', params.audioSendCodec);
|
|
224
|
+
}
|
|
225
|
+
// Promotes |audioRecvCodec| to be the first in the m=audio line, if set.
|
|
226
|
+
function maybePreferAudioReceiveCodec(sdp, params) {
|
|
227
|
+
return maybePreferCodec(sdp, 'audio', 'receive', params.audioRecvCodec);
|
|
228
|
+
}
|
|
229
|
+
// Promotes |videoSendCodec| to be the first in the m=audio line, if set.
|
|
230
|
+
function maybePreferVideoSendCodec(sdp, params) {
|
|
231
|
+
return maybePreferCodec(sdp, 'video', 'send', params.videoSendCodec);
|
|
232
|
+
}
|
|
233
|
+
// Promotes |videoRecvCodec| to be the first in the m=audio line, if set.
|
|
234
|
+
function maybePreferVideoReceiveCodec(sdp, params) {
|
|
235
|
+
return maybePreferCodec(sdp, 'video', 'receive', params.videoRecvCodec);
|
|
236
|
+
}
|
|
237
|
+
// Sets |codec| as the default |type| codec if it's present.
|
|
238
|
+
// The format of |codec| is 'NAME/RATE', e.g. 'opus/48000'.
|
|
239
|
+
function maybePreferCodec(sdp, type, _dir, codec) {
|
|
240
|
+
// const str = `${type} ${dir} codec`;
|
|
241
|
+
if (!codec) {
|
|
242
|
+
// trace(`No preference on ${str}.`);
|
|
243
|
+
return sdp;
|
|
244
|
+
}
|
|
245
|
+
// trace(`Prefer ${str}: ${codec}`);
|
|
246
|
+
const sdpLines = sdp.split('\r\n');
|
|
247
|
+
// Search for m line.
|
|
248
|
+
const mLineIndex = findLine(sdpLines, 'm=', type);
|
|
249
|
+
if (mLineIndex === -1) {
|
|
250
|
+
return sdp;
|
|
251
|
+
}
|
|
252
|
+
// If the codec is available, set it as the default in m line.
|
|
253
|
+
const payload = getCodecPayloadType(sdpLines, codec);
|
|
254
|
+
if (payload) {
|
|
255
|
+
sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], payload);
|
|
256
|
+
}
|
|
257
|
+
sdp = sdpLines.join('\r\n');
|
|
258
|
+
return sdp;
|
|
259
|
+
}
|
|
260
|
+
// Set fmtp param to specific codec in SDP. If param does not exists, add it.
|
|
261
|
+
function setCodecParam(sdp, codec, param, value) {
|
|
262
|
+
const sdpLines = sdp.split('\r\n');
|
|
263
|
+
const fmtpLineIndex = findFmtpLine(sdpLines, codec);
|
|
264
|
+
if (fmtpLineIndex === null) {
|
|
265
|
+
const index = findLine(sdpLines, 'a=rtpmap', codec);
|
|
266
|
+
if (index === -1) {
|
|
267
|
+
return sdp;
|
|
268
|
+
}
|
|
269
|
+
const payload = getCodecPayloadTypeFromLine(sdpLines[index]);
|
|
270
|
+
const fmtpObj = {
|
|
271
|
+
pt: payload.toString(),
|
|
272
|
+
params: { [param]: value },
|
|
273
|
+
};
|
|
274
|
+
sdpLines.splice(index + 1, 0, writeFmtpLine(fmtpObj));
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
const fmtpObj = parseFmtpLine(sdpLines[fmtpLineIndex]);
|
|
278
|
+
fmtpObj.params[param] = value;
|
|
279
|
+
sdpLines[fmtpLineIndex] = writeFmtpLine(fmtpObj);
|
|
280
|
+
}
|
|
281
|
+
sdp = sdpLines.join('\r\n');
|
|
282
|
+
return sdp;
|
|
283
|
+
}
|
|
284
|
+
// Remove fmtp param if it exists.
|
|
285
|
+
function removeCodecParam(sdp, codec, param) {
|
|
286
|
+
const sdpLines = sdp.split('\r\n');
|
|
287
|
+
const fmtpLineIndex = findFmtpLine(sdpLines, codec);
|
|
288
|
+
if (fmtpLineIndex === null) {
|
|
289
|
+
return sdp;
|
|
290
|
+
}
|
|
291
|
+
const map = parseFmtpLine(sdpLines[fmtpLineIndex]);
|
|
292
|
+
// if (map) {
|
|
293
|
+
delete map.params[param];
|
|
294
|
+
const newLine = writeFmtpLine(map);
|
|
295
|
+
if (newLine === null) {
|
|
296
|
+
sdpLines.splice(fmtpLineIndex, 1);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
sdpLines[fmtpLineIndex] = newLine;
|
|
300
|
+
}
|
|
301
|
+
// }
|
|
302
|
+
sdp = sdpLines.join('\r\n');
|
|
303
|
+
return sdp;
|
|
304
|
+
}
|
|
305
|
+
// Split an fmtp line into an object including 'pt' and 'params'.
|
|
306
|
+
function parseFmtpLine(fmtpLine) {
|
|
307
|
+
const fmtpObj = {};
|
|
308
|
+
const spacePos = fmtpLine.indexOf(' ');
|
|
309
|
+
const keyValues = fmtpLine.substring(spacePos + 1).split('; ');
|
|
310
|
+
const pattern = /a=fmtp:(\d+)/;
|
|
311
|
+
const result = pattern.exec(fmtpLine);
|
|
312
|
+
if (result?.length === 2) {
|
|
313
|
+
fmtpObj.pt = result[1];
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
// return null;
|
|
317
|
+
}
|
|
318
|
+
const params = {};
|
|
319
|
+
keyValues.forEach((keyValue) => {
|
|
320
|
+
const pair = keyValue.split('=');
|
|
321
|
+
if (pair.length === 2) {
|
|
322
|
+
params[pair[0]] = pair[1];
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
fmtpObj.params = params;
|
|
326
|
+
return fmtpObj;
|
|
327
|
+
}
|
|
328
|
+
// Generate an fmtp line from an object including 'pt' and 'params'.
|
|
329
|
+
function writeFmtpLine(fmtpObj) {
|
|
330
|
+
if (!fmtpObj.hasOwnProperty('pt') || !fmtpObj.hasOwnProperty('params')) {
|
|
331
|
+
// return null;
|
|
332
|
+
return '';
|
|
333
|
+
}
|
|
334
|
+
const { pt = '', params = {} } = fmtpObj;
|
|
335
|
+
const keyValues = Object.entries(params).map(([key, value]) => `${key}=${value}`);
|
|
336
|
+
if (keyValues.length === 0) {
|
|
337
|
+
return '';
|
|
338
|
+
}
|
|
339
|
+
return `a=fmtp:${pt.toString()} ${keyValues.join('; ')}`;
|
|
340
|
+
}
|
|
341
|
+
// Find fmtp attribute for |codec| in |sdpLines|.
|
|
342
|
+
function findFmtpLine(sdpLines, codec) {
|
|
343
|
+
// Find payload of codec.
|
|
344
|
+
const payload = getCodecPayloadType(sdpLines, codec);
|
|
345
|
+
// Find the payload in fmtp line.
|
|
346
|
+
return payload ? findLine(sdpLines, `a=fmtp:${payload.toString()}`) : null;
|
|
347
|
+
}
|
|
348
|
+
// Find the line in sdpLines that starts with |prefix|, and, if specified,
|
|
349
|
+
// contains |substr| (case-insensitive search).
|
|
350
|
+
function findLine(sdpLines, prefix, substr) {
|
|
351
|
+
return findLineInRange(sdpLines, 0, -1, prefix, substr);
|
|
352
|
+
}
|
|
353
|
+
// Find the line in sdpLines[startLine...endLine - 1] that starts with |prefix|
|
|
354
|
+
// and, if specified, contains |substr| (case-insensitive search).
|
|
355
|
+
function findLineInRange(sdpLines, startLine, endLine, prefix, substr) {
|
|
356
|
+
const realEndLine = endLine !== -1 ? endLine : sdpLines.length;
|
|
357
|
+
for (let i = startLine; i < realEndLine; ++i) {
|
|
358
|
+
if (sdpLines[i].startsWith(prefix)) {
|
|
359
|
+
if (!substr || sdpLines[i].toLowerCase().includes(substr.toLowerCase())) {
|
|
360
|
+
return i;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return -1;
|
|
365
|
+
}
|
|
366
|
+
// Gets the codec payload type from sdp lines.
|
|
367
|
+
function getCodecPayloadType(sdpLines, codec) {
|
|
368
|
+
const index = findLine(sdpLines, 'a=rtpmap', codec);
|
|
369
|
+
return index >= 0 ? getCodecPayloadTypeFromLine(sdpLines[index]) : null;
|
|
370
|
+
}
|
|
371
|
+
// Gets the codec payload type from an a=rtpmap:X line.
|
|
372
|
+
function getCodecPayloadTypeFromLine(sdpLine) {
|
|
373
|
+
const pattern = /a=rtpmap:(\d+) [a-zA-Z0-9-]+\/\d+/;
|
|
374
|
+
const result = pattern.exec(sdpLine);
|
|
375
|
+
return result?.length === 2 ? result[1] : '';
|
|
376
|
+
}
|
|
377
|
+
// Returns a new m= line with the specified codec as the first one.
|
|
378
|
+
function setDefaultCodec(mLine, payload) {
|
|
379
|
+
const elements = mLine.split(' ');
|
|
380
|
+
// Just copy the first three parameters; codec order starts on fourth.
|
|
381
|
+
const newLine = elements.slice(0, 3);
|
|
382
|
+
// Put target payload first and copy in the rest.
|
|
383
|
+
newLine.push(payload);
|
|
384
|
+
const { length } = elements;
|
|
385
|
+
for (let i = 3; i < length; i++) {
|
|
386
|
+
if (elements[i] !== payload) {
|
|
387
|
+
newLine.push(elements[i]);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return newLine.join(' ');
|
|
391
|
+
}
|
|
392
|
+
export function prepareLocalDescription(desc, preferCodecs = {}) {
|
|
393
|
+
let sdp = maybePreferAudioReceiveCodec(desc.sdp || '', preferCodecs);
|
|
394
|
+
sdp = maybePreferVideoReceiveCodec(sdp, preferCodecs);
|
|
395
|
+
sdp = maybeSetAudioReceiveBitRate(sdp, preferCodecs);
|
|
396
|
+
sdp = maybeSetVideoReceiveBitRate(sdp, preferCodecs);
|
|
397
|
+
sdp = maybeRemoveVideoFec(sdp, preferCodecs);
|
|
398
|
+
const newDesc = {
|
|
399
|
+
sdp,
|
|
400
|
+
type: desc.type,
|
|
401
|
+
};
|
|
402
|
+
return newDesc;
|
|
403
|
+
}
|
|
404
|
+
export function prepareRemoteDescription(desc, preferCodecs = {}) {
|
|
405
|
+
let sdp = maybeSetOpusOptions(desc.sdp || '', preferCodecs);
|
|
406
|
+
sdp = maybePreferAudioSendCodec(sdp, preferCodecs);
|
|
407
|
+
sdp = maybePreferVideoSendCodec(sdp, preferCodecs);
|
|
408
|
+
sdp = maybeSetAudioSendBitRate(sdp, preferCodecs);
|
|
409
|
+
sdp = maybeSetVideoSendBitRate(sdp, preferCodecs);
|
|
410
|
+
sdp = maybeSetVideoSendInitialBitRate(sdp, preferCodecs);
|
|
411
|
+
sdp = maybeRemoveVideoFec(sdp, preferCodecs);
|
|
412
|
+
const newDesc = {
|
|
413
|
+
sdp,
|
|
414
|
+
type: desc.type,
|
|
415
|
+
};
|
|
416
|
+
return newDesc;
|
|
417
|
+
}
|
package/ws/WSController.js
CHANGED
|
@@ -1 +1,148 @@
|
|
|
1
|
-
import ReconnectingWebSocket,
|
|
1
|
+
import ReconnectingWebSocket, {} from 'reconnecting-websocket';
|
|
2
|
+
import { EventEmitter } from '@js-toolkit/utils/EventEmitter';
|
|
3
|
+
import { delayed } from '@js-toolkit/utils/delayed';
|
|
4
|
+
import { EventEmitterListener } from '../EventEmitterListener';
|
|
5
|
+
// function getNotConnectedError(): Error {
|
|
6
|
+
// return new Error('The object is not connected yet.');
|
|
7
|
+
// }
|
|
8
|
+
export class WSController extends EventEmitter {
|
|
9
|
+
// eslint-disable-next-line class-methods-use-this
|
|
10
|
+
get Events() {
|
|
11
|
+
return WSController.Events;
|
|
12
|
+
}
|
|
13
|
+
logger;
|
|
14
|
+
ws;
|
|
15
|
+
listener;
|
|
16
|
+
reconnectOnIdle;
|
|
17
|
+
halfOpen;
|
|
18
|
+
// private closeInvoked = false;
|
|
19
|
+
constructor(url, options) {
|
|
20
|
+
super();
|
|
21
|
+
const { logger, protocols, binaryType, idleTimeout, halfOpenDetection, startClosed, ...rest } = options ?? {};
|
|
22
|
+
this.logger = logger ?? console;
|
|
23
|
+
this.ws = new ReconnectingWebSocket(url, protocols, {
|
|
24
|
+
...rest,
|
|
25
|
+
startClosed: true,
|
|
26
|
+
});
|
|
27
|
+
if (binaryType != null) {
|
|
28
|
+
this.ws.binaryType = binaryType;
|
|
29
|
+
}
|
|
30
|
+
this.listener = new EventEmitterListener(this.ws);
|
|
31
|
+
// reconnect on idle
|
|
32
|
+
if (idleTimeout != null && idleTimeout > 0) {
|
|
33
|
+
this.reconnectOnIdle = delayed(() => {
|
|
34
|
+
this.logger.info(`WS connection reconnecting after ${idleTimeout}ms due to inactivity.`);
|
|
35
|
+
this.ws.reconnect();
|
|
36
|
+
}, idleTimeout);
|
|
37
|
+
}
|
|
38
|
+
// reconnect on half-open
|
|
39
|
+
this.halfOpen = (() => {
|
|
40
|
+
if (!halfOpenDetection)
|
|
41
|
+
return undefined;
|
|
42
|
+
const { pingTimeout = 12_000, outMessage = () => JSON.stringify({ type: 'pong' }), inMessageFilter = (message) => message != null && typeof message === 'object' && message.type === 'ping', } = halfOpenDetection;
|
|
43
|
+
const reconnectOnHalfOpen = delayed(() => {
|
|
44
|
+
this.logger.info(`WS connection reconnecting after ${pingTimeout}ms due to the probability of a half-open connection.`);
|
|
45
|
+
this.ws.reconnect();
|
|
46
|
+
}, pingTimeout);
|
|
47
|
+
const heartbeat = () => {
|
|
48
|
+
// console.log('heartbeat');
|
|
49
|
+
reconnectOnHalfOpen();
|
|
50
|
+
this.ws.send(typeof outMessage === 'function' && !(outMessage instanceof Blob)
|
|
51
|
+
? outMessage()
|
|
52
|
+
: outMessage);
|
|
53
|
+
};
|
|
54
|
+
const onMessage = ({ data }) => {
|
|
55
|
+
if (inMessageFilter(data))
|
|
56
|
+
heartbeat();
|
|
57
|
+
else
|
|
58
|
+
reconnectOnHalfOpen(); // Any message signals that the connection still alive.
|
|
59
|
+
};
|
|
60
|
+
return { heartbeat, cancel: () => reconnectOnHalfOpen.cancel(), onMessage };
|
|
61
|
+
})();
|
|
62
|
+
this.listener
|
|
63
|
+
.on('open', () => {
|
|
64
|
+
if (this.reconnectOnIdle)
|
|
65
|
+
this.reconnectOnIdle();
|
|
66
|
+
this.halfOpen?.heartbeat();
|
|
67
|
+
this.emit(this.Events.Connected);
|
|
68
|
+
})
|
|
69
|
+
.on('close', ({ code }) => {
|
|
70
|
+
this.reconnectOnIdle?.cancel();
|
|
71
|
+
this.halfOpen?.cancel();
|
|
72
|
+
if (options?.maxRetries != null &&
|
|
73
|
+
this.ws.readyState === this.ws.CLOSED &&
|
|
74
|
+
this.ws.retryCount === options.maxRetries) {
|
|
75
|
+
// Without this call ws will constantly try to reconnect
|
|
76
|
+
this.close(code, `Failed to connect after ${options.maxRetries} attempts.`);
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
.on('message', (event) => {
|
|
80
|
+
if (this.reconnectOnIdle)
|
|
81
|
+
this.reconnectOnIdle();
|
|
82
|
+
this.halfOpen?.onMessage(event);
|
|
83
|
+
this.emit(this.Events.Message, event);
|
|
84
|
+
})
|
|
85
|
+
.on('error', ({ error, message }) => {
|
|
86
|
+
this.emit(this.Events.Error, { error, message });
|
|
87
|
+
});
|
|
88
|
+
if (this.halfOpen && (this.isConnected() || this.ws.readyState === this.ws.CONNECTING)) {
|
|
89
|
+
this.halfOpen.heartbeat();
|
|
90
|
+
}
|
|
91
|
+
if (!startClosed) {
|
|
92
|
+
this.connect();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
isConnected() {
|
|
96
|
+
return this.ws.readyState === this.ws.OPEN;
|
|
97
|
+
}
|
|
98
|
+
getUrl() {
|
|
99
|
+
return this.ws.url;
|
|
100
|
+
}
|
|
101
|
+
connect() {
|
|
102
|
+
// this.close()
|
|
103
|
+
// this.closeInvoked = false;
|
|
104
|
+
this.ws.reconnect();
|
|
105
|
+
}
|
|
106
|
+
send(data) {
|
|
107
|
+
// if (!this.ws) throw getNotConnectedError();
|
|
108
|
+
this.ws.send(data);
|
|
109
|
+
}
|
|
110
|
+
close(code, reason) {
|
|
111
|
+
// if (this.closeInvoked) return;
|
|
112
|
+
// this.closeInvoked = true;
|
|
113
|
+
// const hasWS = !!this.ws;
|
|
114
|
+
try {
|
|
115
|
+
if (this.reconnectOnIdle) {
|
|
116
|
+
this.reconnectOnIdle.cancel();
|
|
117
|
+
}
|
|
118
|
+
if (this.halfOpen) {
|
|
119
|
+
this.halfOpen.cancel();
|
|
120
|
+
}
|
|
121
|
+
this.ws.close(code, reason);
|
|
122
|
+
}
|
|
123
|
+
catch (ex) {
|
|
124
|
+
this.logger.warn(ex);
|
|
125
|
+
}
|
|
126
|
+
// if (hasWS) this.emit(this.Events.Closed, reason ? { reason } : undefined);
|
|
127
|
+
this.emit(this.Events.Closed, reason ? { reason } : undefined);
|
|
128
|
+
}
|
|
129
|
+
destroy() {
|
|
130
|
+
this.close();
|
|
131
|
+
this.listener.removeAllListeners();
|
|
132
|
+
this.removeAllListeners();
|
|
133
|
+
}
|
|
134
|
+
[Symbol.dispose]() {
|
|
135
|
+
this.destroy();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
139
|
+
(function (WSController) {
|
|
140
|
+
let Events;
|
|
141
|
+
(function (Events) {
|
|
142
|
+
Events["Connected"] = "Connected";
|
|
143
|
+
Events["Message"] = "Message";
|
|
144
|
+
Events["Error"] = "Error";
|
|
145
|
+
/** If connection was closed and the object will not to try to reconnect */
|
|
146
|
+
Events["Closed"] = "Closed";
|
|
147
|
+
})(Events = WSController.Events || (WSController.Events = {}));
|
|
148
|
+
})(WSController || (WSController = {}));
|