@js-toolkit/web-utils 1.66.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 +19 -16
- 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.d.ts +18 -0
- package/responsive/MediaQuery.js +40 -0
- package/responsive/MediaQueryListener.d.ts +19 -0
- package/responsive/MediaQueryListener.js +55 -0
- package/responsive/ViewSize.d.ts +45 -0
- package/responsive/ViewSize.js +82 -0
- package/responsive/getViewSizeQueryMap.d.ts +2 -0
- package/responsive/getViewSizeQueryMap.js +7 -0
- 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
|
@@ -1 +1,112 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-misused-promises */
|
|
2
|
+
/* eslint-disable max-classes-per-file */
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-namespace */
|
|
4
|
+
import { EventEmitter } from '@js-toolkit/utils/EventEmitter';
|
|
5
|
+
import { ErrorCompat } from '@js-toolkit/utils/ErrorCompat';
|
|
6
|
+
import { onPageReady } from '../onPageReady';
|
|
7
|
+
import { isLocalhost } from './utils';
|
|
8
|
+
/*
|
|
9
|
+
* https://web.dev/articles/service-workers-registration?hl=ru
|
|
10
|
+
*/
|
|
11
|
+
export class ServiceWorkerUnavailableError extends ErrorCompat {
|
|
12
|
+
constructor() {
|
|
13
|
+
super(ServiceWorkerUnavailableError, 'ServiceWorker is not available', {
|
|
14
|
+
name: 'ServiceWorkerUnavailableError',
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class ServiceWorkerInstaller extends EventEmitter {
|
|
19
|
+
static isAvailable() {
|
|
20
|
+
return 'serviceWorker' in navigator;
|
|
21
|
+
}
|
|
22
|
+
// private readonly logPrefix = 'SW:';
|
|
23
|
+
// private readonly options;
|
|
24
|
+
registration;
|
|
25
|
+
cancelDefferedRegister;
|
|
26
|
+
// constructor(/* options: ServiceWorkerInstaller.Options */) {
|
|
27
|
+
// super();
|
|
28
|
+
// // this.options = { ...options, logger: options.logger ?? console };
|
|
29
|
+
// }
|
|
30
|
+
register(swUrl, options) {
|
|
31
|
+
if (!ServiceWorkerInstaller.isAvailable()) {
|
|
32
|
+
throw new ServiceWorkerUnavailableError();
|
|
33
|
+
}
|
|
34
|
+
const { deffered, ...swOptions } = options ?? {};
|
|
35
|
+
const register = async () => {
|
|
36
|
+
try {
|
|
37
|
+
if (isLocalhost()) {
|
|
38
|
+
// This is running on localhost. Let's check if a service worker still exists or not.
|
|
39
|
+
const response = await fetch(swUrl);
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
throw new Error(`No service worker found at '${response.url}'.`, {
|
|
42
|
+
cause: `Response: ${response.status} ${response.statusText}`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const registration = await navigator.serviceWorker.register(swUrl, swOptions);
|
|
47
|
+
this.registration = registration;
|
|
48
|
+
this.emit('registered', { registration });
|
|
49
|
+
// https://whatwebcando.today/articles/handling-service-worker-updates/
|
|
50
|
+
registration.onupdatefound = () => {
|
|
51
|
+
// At this point we only know the browser detected the Service Worker file change.
|
|
52
|
+
//
|
|
53
|
+
// Wait until the new instance is ready for activation (its state is installed).
|
|
54
|
+
const sw = registration.installing;
|
|
55
|
+
if (sw == null) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
sw.onstatechange = () => {
|
|
59
|
+
if (sw.state === 'installed') {
|
|
60
|
+
if (navigator.serviceWorker.controller) {
|
|
61
|
+
// At this point, the updated precached content has been fetched,
|
|
62
|
+
// but the previous service worker will still serve the older content (until all client tabs are closed).
|
|
63
|
+
this.emit('updatePending', { registration });
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// It's the first install.
|
|
67
|
+
// At this point, everything has been precached.
|
|
68
|
+
// It's the perfect time to display a "Content is cached for offline use." message.
|
|
69
|
+
this.emit('updated', { registration });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
sw.onerror = (error) => {
|
|
74
|
+
// this.options.logger.error(this.logPrefix, 'Installing worker:', error);
|
|
75
|
+
const nextError = new Error('Error during service worker installation', {
|
|
76
|
+
cause: error,
|
|
77
|
+
});
|
|
78
|
+
this.emit('error', { error: nextError });
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const nextError = new Error('Error during service worker registration', { cause: error });
|
|
84
|
+
// this.options.logger.error(this.logPrefix, getErrorMessage(nextError));
|
|
85
|
+
this.emit('error', { error: nextError });
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
if (typeof deffered === 'number' ? deffered > 0 : deffered) {
|
|
89
|
+
this.cancelDefferedRegister = onPageReady(register, typeof deffered === 'number' ? { timeout: deffered } : undefined);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
void register();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
unregister() {
|
|
96
|
+
if (this.cancelDefferedRegister)
|
|
97
|
+
this.cancelDefferedRegister();
|
|
98
|
+
this.registration?.unregister().catch((error) => {
|
|
99
|
+
const nextError = new Error('Error during service worker unregister', { cause: error });
|
|
100
|
+
// this.options.logger.error(this.logPrefix, getErrorMessage(nextError));
|
|
101
|
+
this.emit('error', { error: nextError });
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
destroy() {
|
|
105
|
+
if (this.cancelDefferedRegister)
|
|
106
|
+
this.cancelDefferedRegister();
|
|
107
|
+
this.removeAllListeners();
|
|
108
|
+
}
|
|
109
|
+
[Symbol.dispose]() {
|
|
110
|
+
this.destroy();
|
|
111
|
+
}
|
|
112
|
+
}
|
package/serviceWorker/utils.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference lib="webworker" preserve="true" />
|
|
2
2
|
export declare function isLocalhost(hostname?: string): boolean;
|
|
3
3
|
/** Delete all caches that aren't named in `caches`. */
|
|
4
|
-
export declare function removeUnknownCaches
|
|
4
|
+
export declare function removeUnknownCaches(expectedCaches: Record<string, string>): Promise<unknown>;
|
|
5
5
|
export declare function addResourcesToCache(cacheName: string, resources: readonly RequestInfo[]): Promise<void>;
|
|
6
6
|
interface CacheFirstOptions extends Pick<FetchEvent, 'request'> {
|
|
7
7
|
readonly fallbackUrl?: string | undefined;
|
package/serviceWorker/utils.js
CHANGED
|
@@ -1 +1,86 @@
|
|
|
1
|
-
|
|
1
|
+
/// <reference lib="webworker" preserve="true" />
|
|
2
|
+
import { getErrorMessage } from '@js-toolkit/utils/getErrorMessage';
|
|
3
|
+
export function isLocalhost(hostname = window.location.hostname) {
|
|
4
|
+
return !!(hostname === 'localhost' ||
|
|
5
|
+
// [::1] is the IPv6 localhost address.
|
|
6
|
+
hostname === '[::1]' ||
|
|
7
|
+
// 127.0.0.1/8 is considered localhost for IPv4.
|
|
8
|
+
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/.exec(hostname));
|
|
9
|
+
}
|
|
10
|
+
/** Delete all caches that aren't named in `caches`. */
|
|
11
|
+
export function removeUnknownCaches(expectedCaches) {
|
|
12
|
+
return caches.keys().then((cacheNames) => {
|
|
13
|
+
const expectedCacheNamesSet = new Set(Object.values(expectedCaches));
|
|
14
|
+
return Promise.all(
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/await-thenable
|
|
16
|
+
cacheNames.map((cacheName) => {
|
|
17
|
+
if (!expectedCacheNamesSet.has(cacheName)) {
|
|
18
|
+
// If this cache name isn't present in the set of "expected" cache names, then delete it.
|
|
19
|
+
// logger.debug('Deleting out of date cache:', cacheName);
|
|
20
|
+
return caches.delete(cacheName);
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export async function addResourcesToCache(cacheName, resources) {
|
|
27
|
+
const cache = await caches.open(cacheName);
|
|
28
|
+
await cache.addAll(resources);
|
|
29
|
+
}
|
|
30
|
+
export async function cacheFirst(cacheName, { request, fallbackUrl, saveToCache, logger = console }) {
|
|
31
|
+
// First try to get the resource from the cache
|
|
32
|
+
const cache = await caches.open(cacheName);
|
|
33
|
+
const responseFromCache = await cache.match(request);
|
|
34
|
+
if (responseFromCache) {
|
|
35
|
+
logger.debug('Found response in cache:', responseFromCache);
|
|
36
|
+
return responseFromCache;
|
|
37
|
+
}
|
|
38
|
+
// Otherwise, if there is no entry in the cache for `request`, response will be undefined,
|
|
39
|
+
// and we need to fetch() the resource.
|
|
40
|
+
logger.debug('No response for %s found in cache. About to fetch from network...', request.url);
|
|
41
|
+
// Next try to use the preloaded response, if it's there
|
|
42
|
+
// NOTE: Chrome throws errors regarding preloadResponse, see:
|
|
43
|
+
// https://bugs.chromium.org/p/chromium/issues/detail?id=1420515
|
|
44
|
+
// https://github.com/mdn/dom-examples/issues/145
|
|
45
|
+
// To avoid those errors, remove or comment out this block of preloadResponse
|
|
46
|
+
// code along with enableNavigationPreload() and the "activate" listener.
|
|
47
|
+
// const preloadResponseValue = await preloadResponse;
|
|
48
|
+
// if (preloadResponseValue) {
|
|
49
|
+
// logger.info('using preload response', preloadResponseValue);
|
|
50
|
+
// putInCache(request, preloadResponseValue.clone());
|
|
51
|
+
// return preloadResponseValue;
|
|
52
|
+
// }
|
|
53
|
+
// Next try to get the resource from the network
|
|
54
|
+
// We call .clone() on the request since we might use it in a call to cache.put() later on.
|
|
55
|
+
// Both fetch() and cache.put() "consume" the request, so we need to make a copy.
|
|
56
|
+
// (see https://fetch.spec.whatwg.org/#dom-request-clone)
|
|
57
|
+
try {
|
|
58
|
+
const response = await fetch(request.clone());
|
|
59
|
+
logger.debug('Response for %s from network is: %O', request.url, response);
|
|
60
|
+
// This avoids caching responses that we know are errors (i.e. HTTP status code of 4xx or 5xx).
|
|
61
|
+
if (response.status < 400 && (!saveToCache || saveToCache({ request, response }))) {
|
|
62
|
+
// We call .clone() on the response to save a copy of it to the cache. By doing so, we get to keep
|
|
63
|
+
// the original response object which we will return back to the controlled page.
|
|
64
|
+
// (see https://fetch.spec.whatwg.org/#dom-response-clone)
|
|
65
|
+
logger.debug('Caching the response to', request.url);
|
|
66
|
+
cache.put(request, response.clone()).catch((error) => {
|
|
67
|
+
logger.error(getErrorMessage(new Error(`Caching error of ${request.url}`, { cause: error })));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
logger.debug('Not caching the response to', request.url);
|
|
72
|
+
}
|
|
73
|
+
return response;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
const fallbackResponse = fallbackUrl ? await cache.match(fallbackUrl) : undefined;
|
|
77
|
+
if (fallbackResponse) {
|
|
78
|
+
return fallbackResponse;
|
|
79
|
+
}
|
|
80
|
+
// This catch() will handle exceptions that arise from the match() or fetch() operations.
|
|
81
|
+
// Note that a HTTP error response (e.g. 404) will NOT trigger an exception.
|
|
82
|
+
// It will return a normal response object that has the appropriate error code set.
|
|
83
|
+
logger.error('Error in fetch handler:', error);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
package/stopPropagation.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export declare function stopPropagation
|
|
1
|
+
export declare function stopPropagation(event: {
|
|
2
2
|
stopPropagation: VoidFunction;
|
|
3
|
-
}
|
|
3
|
+
}): void;
|
package/stopPropagation.js
CHANGED
package/takeScreenshot.js
CHANGED
|
@@ -1 +1,51 @@
|
|
|
1
|
-
import{hasIn}from
|
|
1
|
+
import { hasIn } from '@js-toolkit/utils/hasIn';
|
|
2
|
+
export function get2dContextError() {
|
|
3
|
+
return new Error('Failed to get canvas 2d context.');
|
|
4
|
+
}
|
|
5
|
+
const getDefaultWidth = (element) => {
|
|
6
|
+
if (element instanceof HTMLVideoElement) {
|
|
7
|
+
return element.videoWidth;
|
|
8
|
+
}
|
|
9
|
+
if (hasIn(element, 'displayWidth')) {
|
|
10
|
+
return element.displayWidth;
|
|
11
|
+
}
|
|
12
|
+
return element.width instanceof SVGAnimatedLength ? element.width.animVal.value : element.width;
|
|
13
|
+
};
|
|
14
|
+
const getDefaultHeight = (element) => {
|
|
15
|
+
if (element instanceof HTMLVideoElement) {
|
|
16
|
+
return element.videoHeight;
|
|
17
|
+
}
|
|
18
|
+
if (hasIn(element, 'displayHeight')) {
|
|
19
|
+
return element.displayHeight;
|
|
20
|
+
}
|
|
21
|
+
return element.height instanceof SVGAnimatedLength
|
|
22
|
+
? element.height.animVal.value
|
|
23
|
+
: element.height;
|
|
24
|
+
};
|
|
25
|
+
export function takeScreenshot(element, { width = getDefaultWidth(element), height = getDefaultHeight(element), type = 'image/jpeg', quality = 1, } = {}) {
|
|
26
|
+
const canvas = document.createElement('canvas');
|
|
27
|
+
canvas.width = width;
|
|
28
|
+
canvas.height = height;
|
|
29
|
+
const ctx = canvas.getContext('2d');
|
|
30
|
+
if (!ctx)
|
|
31
|
+
throw get2dContextError();
|
|
32
|
+
ctx.drawImage(element, 0, 0);
|
|
33
|
+
return canvas.toDataURL(type, quality);
|
|
34
|
+
}
|
|
35
|
+
export function takeScreenshotAsync(element, { width = getDefaultWidth(element), height = getDefaultHeight(element), type = 'image/jpeg', quality = 1, } = {}) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const canvas = document.createElement('canvas');
|
|
38
|
+
canvas.width = width;
|
|
39
|
+
canvas.height = height;
|
|
40
|
+
const ctx = canvas.getContext('2d');
|
|
41
|
+
if (!ctx)
|
|
42
|
+
throw get2dContextError();
|
|
43
|
+
ctx.drawImage(element, 0, 0);
|
|
44
|
+
canvas.toBlob((blob) => {
|
|
45
|
+
if (blob == null)
|
|
46
|
+
reject(new Error('Unable get blob'));
|
|
47
|
+
else
|
|
48
|
+
resolve(blob);
|
|
49
|
+
}, type, quality);
|
|
50
|
+
});
|
|
51
|
+
}
|
package/toBase64.js
CHANGED
|
@@ -1 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-deprecated */
|
|
2
|
+
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
|
|
3
|
+
/** Safe for unicode string. */
|
|
4
|
+
export function toBase64(str) {
|
|
5
|
+
const binString = window.TextEncoder
|
|
6
|
+
? Array.from(new TextEncoder().encode(str), (byte) => String.fromCodePoint(byte)).join('')
|
|
7
|
+
: unescape(encodeURIComponent(str));
|
|
8
|
+
return window.btoa(binString);
|
|
9
|
+
}
|
package/toLocalPoint.js
CHANGED
|
@@ -1 +1,9 @@
|
|
|
1
|
-
export function toLocalPoint(
|
|
1
|
+
export function toLocalPoint(coord, targetOrClientRect) {
|
|
2
|
+
const rect = 'tagName' in targetOrClientRect
|
|
3
|
+
? targetOrClientRect.getBoundingClientRect()
|
|
4
|
+
: targetOrClientRect;
|
|
5
|
+
return {
|
|
6
|
+
x: coord.clientX - rect.left,
|
|
7
|
+
y: coord.clientY - rect.top,
|
|
8
|
+
};
|
|
9
|
+
}
|
package/types/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
"use strict";
|
|
1
|
+
"use strict";
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
package/types/refs.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import '@js-toolkit/utils/types';
|
package/viewableTracker.js
CHANGED
|
@@ -1 +1,69 @@
|
|
|
1
|
-
import throttleFn from
|
|
1
|
+
import throttleFn from 'lodash.throttle';
|
|
2
|
+
export function getViewableTracker(element, { visiblePart: visiblePartOption = 0.8, scrollThrottle = 200, documentVisibility = true, onChange, }) {
|
|
3
|
+
const visiblePart = Math.min(+visiblePartOption, 1);
|
|
4
|
+
// const visiblePart = +visiblePartOption;
|
|
5
|
+
let raf = 0;
|
|
6
|
+
let lastViewable;
|
|
7
|
+
let lastDocumentViewable = document.visibilityState === 'visible';
|
|
8
|
+
const checkVisibility = () => {
|
|
9
|
+
if (documentVisibility && document.visibilityState !== 'visible')
|
|
10
|
+
return;
|
|
11
|
+
const { top, bottom, height } = element.getBoundingClientRect();
|
|
12
|
+
const visibleHeight = height * visiblePart;
|
|
13
|
+
const bottomPos = top + visibleHeight;
|
|
14
|
+
const viewable = window.innerHeight >= bottomPos && bottom > visibleHeight;
|
|
15
|
+
// console.log(bottomPos, top, bottom, visibleHeight, viewable);
|
|
16
|
+
if (lastViewable !== viewable) {
|
|
17
|
+
lastViewable = viewable;
|
|
18
|
+
onChange(viewable);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const checkVisibilityRaf = () => {
|
|
22
|
+
if (documentVisibility && document.visibilityState !== 'visible')
|
|
23
|
+
return;
|
|
24
|
+
raf = window.requestAnimationFrame(checkVisibility);
|
|
25
|
+
};
|
|
26
|
+
const checkDocumentVisibility = () => {
|
|
27
|
+
const viewable = document.visibilityState === 'visible';
|
|
28
|
+
if (lastDocumentViewable === viewable)
|
|
29
|
+
return;
|
|
30
|
+
lastDocumentViewable = viewable;
|
|
31
|
+
// Check if visible on page
|
|
32
|
+
if (viewable /* && visiblePart > 0 */) {
|
|
33
|
+
// Always re-check, the page size may be changed or something else.
|
|
34
|
+
lastViewable = undefined;
|
|
35
|
+
checkVisibility();
|
|
36
|
+
}
|
|
37
|
+
// Hidden tab
|
|
38
|
+
else if ( /* visiblePart <= 0 || */lastViewable !== viewable) {
|
|
39
|
+
onChange(viewable);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const handler = scrollThrottle > 0 ? throttleFn(checkVisibility, scrollThrottle) : checkVisibilityRaf;
|
|
43
|
+
// if (visiblePart > 0) {
|
|
44
|
+
window.addEventListener('scroll', handler, { capture: false, passive: true });
|
|
45
|
+
// }
|
|
46
|
+
if (documentVisibility) {
|
|
47
|
+
document.addEventListener('visibilitychange', checkDocumentVisibility);
|
|
48
|
+
}
|
|
49
|
+
const check = () => {
|
|
50
|
+
// if (visiblePart > 0) {
|
|
51
|
+
checkVisibility();
|
|
52
|
+
// }
|
|
53
|
+
if (documentVisibility) {
|
|
54
|
+
checkDocumentVisibility();
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const destroy = () => {
|
|
58
|
+
cancelAnimationFrame(raf);
|
|
59
|
+
window.removeEventListener('scroll', handler, { capture: false });
|
|
60
|
+
document.removeEventListener('visibilitychange', checkDocumentVisibility);
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
check,
|
|
64
|
+
destroy,
|
|
65
|
+
[Symbol.dispose]() {
|
|
66
|
+
this.destroy();
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
package/webrtc/PeerConnection.js
CHANGED
|
@@ -1 +1,212 @@
|
|
|
1
|
-
import{EventEmitter}from
|
|
1
|
+
import { EventEmitter } from '@js-toolkit/utils/EventEmitter';
|
|
2
|
+
import { getErrorMessage } from '@js-toolkit/utils/getErrorMessage';
|
|
3
|
+
import { hasIn } from '@js-toolkit/utils/hasIn';
|
|
4
|
+
import log from '@js-toolkit/utils/log';
|
|
5
|
+
import * as sdpUtils from './sdputils';
|
|
6
|
+
export class PeerConnection extends EventEmitter {
|
|
7
|
+
options;
|
|
8
|
+
// eslint-disable-next-line class-methods-use-this
|
|
9
|
+
get Events() {
|
|
10
|
+
return PeerConnection.Events;
|
|
11
|
+
}
|
|
12
|
+
logger;
|
|
13
|
+
pc;
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
super();
|
|
16
|
+
this.options = options;
|
|
17
|
+
this.logger = options.logger ?? log.getLogger('PeerConnection');
|
|
18
|
+
this.pc = this.createPC();
|
|
19
|
+
}
|
|
20
|
+
clear() {
|
|
21
|
+
this.pc.onsignalingstatechange = null;
|
|
22
|
+
this.pc.onconnectionstatechange = null;
|
|
23
|
+
this.pc.oniceconnectionstatechange = null;
|
|
24
|
+
this.pc.ontrack = null;
|
|
25
|
+
this.pc.onnegotiationneeded = null;
|
|
26
|
+
this.pc.onicecandidate = null;
|
|
27
|
+
this.pc.onicecandidateerror = null;
|
|
28
|
+
}
|
|
29
|
+
createPC() {
|
|
30
|
+
const pc = new RTCPeerConnection(this.options.rtc);
|
|
31
|
+
pc.onsignalingstatechange = () => {
|
|
32
|
+
this.logger.debug(`Signaling state changed to: ${pc.signalingState}`);
|
|
33
|
+
if (pc.signalingState === 'closed') {
|
|
34
|
+
this.emit(this.Events.Closed);
|
|
35
|
+
this.clear();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const handleConnectionStateChange = (state) => {
|
|
39
|
+
this.logger.debug(`Connection state changed to: ${state}`);
|
|
40
|
+
if (state === 'connected')
|
|
41
|
+
this.emit(this.Events.Connected);
|
|
42
|
+
else if (state === 'disconnected')
|
|
43
|
+
this.emit(this.Events.Disconnected);
|
|
44
|
+
};
|
|
45
|
+
if (hasIn(RTCPeerConnection.prototype, 'onconnectionstatechange')) {
|
|
46
|
+
pc.onconnectionstatechange = () => handleConnectionStateChange(pc.connectionState);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
pc.oniceconnectionstatechange = () => handleConnectionStateChange(pc.iceConnectionState);
|
|
50
|
+
}
|
|
51
|
+
// All tracks must be in one stream for controlling adding/removing tracks.
|
|
52
|
+
pc.ontrack = ({ streams, track }) => {
|
|
53
|
+
this.logger.debug('Remote stream received.', streams.length, track.kind);
|
|
54
|
+
if (streams.length === 0)
|
|
55
|
+
return;
|
|
56
|
+
const [stream] = streams;
|
|
57
|
+
stream.onremovetrack = () => {
|
|
58
|
+
this.logger.debug('onremovetrack');
|
|
59
|
+
this.emit(this.Events.RemoteStreamChanged, stream);
|
|
60
|
+
};
|
|
61
|
+
stream.onaddtrack = () => {
|
|
62
|
+
this.logger.debug('onaddtrack');
|
|
63
|
+
this.emit(this.Events.RemoteStreamChanged, stream);
|
|
64
|
+
};
|
|
65
|
+
// stream.oninactive = () => {
|
|
66
|
+
// this.logger.debug('oninactive');
|
|
67
|
+
// };
|
|
68
|
+
this.emit(this.Events.RemoteStreamChanged, stream);
|
|
69
|
+
};
|
|
70
|
+
pc.onnegotiationneeded = () => {
|
|
71
|
+
// When media stream is changed we should re-communicate.
|
|
72
|
+
const { iceConnectionState: state } = pc;
|
|
73
|
+
if (state === 'connected' || state === 'completed') {
|
|
74
|
+
this.logger.debug('Reinitializing connection...');
|
|
75
|
+
this.emit(this.Events.ReinitializingConnectionRequired);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
pc.onicecandidate = ({ candidate }) => {
|
|
79
|
+
if (!candidate) {
|
|
80
|
+
// End of candidates.
|
|
81
|
+
this.emit(this.Events.EndOfIceCandidates);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!sdpUtils.isValidIceCandidate(candidate))
|
|
85
|
+
return;
|
|
86
|
+
this.emit(this.Events.LocalIceCandidate, candidate);
|
|
87
|
+
// this.logger.debug(`New ICE candidate: ${JSON.stringify(candidate)}`);
|
|
88
|
+
};
|
|
89
|
+
pc.onicecandidateerror = (event) => {
|
|
90
|
+
if (event instanceof RTCPeerConnectionIceErrorEvent) {
|
|
91
|
+
const { errorCode, errorText, hostCandidate, url } = event;
|
|
92
|
+
this.logger.warn(`ICE candidate error: errorCode=${errorCode}, errorText=${errorText}, hostCandidate=${hostCandidate}, url=${url}`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
this.logger.warn(`ICE candidate error: ${getErrorMessage(event)}`);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
return pc;
|
|
99
|
+
}
|
|
100
|
+
async setLocalDescription(pc, desc) {
|
|
101
|
+
const newDesc = sdpUtils.prepareLocalDescription(desc, this.options.codecs);
|
|
102
|
+
await pc.setLocalDescription(newDesc);
|
|
103
|
+
return newDesc;
|
|
104
|
+
}
|
|
105
|
+
async setRemoteDescription(pc, desc) {
|
|
106
|
+
const newDesc = sdpUtils.prepareRemoteDescription(desc, this.options.codecs);
|
|
107
|
+
await pc.setRemoteDescription(newDesc);
|
|
108
|
+
return newDesc;
|
|
109
|
+
}
|
|
110
|
+
isConnected() {
|
|
111
|
+
return this.pc.iceConnectionState === 'connected' || this.pc.iceConnectionState === 'completed';
|
|
112
|
+
}
|
|
113
|
+
isClosed() {
|
|
114
|
+
return this.pc.signalingState === 'closed';
|
|
115
|
+
}
|
|
116
|
+
/** Add icecandidate when the clients is exchanging icecandidates. */
|
|
117
|
+
addIceCandidate(candidate) {
|
|
118
|
+
return this.pc.addIceCandidate(candidate);
|
|
119
|
+
}
|
|
120
|
+
/** Add stream to the remote peer if needed. */
|
|
121
|
+
attachStream(stream) {
|
|
122
|
+
const senders = this.pc.getSenders();
|
|
123
|
+
// Get tracks which not existed in peer connection.
|
|
124
|
+
const newTracks = senders.length > 0
|
|
125
|
+
? stream
|
|
126
|
+
.getTracks()
|
|
127
|
+
.filter((t) => !senders.find(({ track }) => !!track && track.id === t.id))
|
|
128
|
+
: stream.getTracks();
|
|
129
|
+
// If connection has already attached tracks then not possible attach tracks again.
|
|
130
|
+
if (newTracks.length === 0) {
|
|
131
|
+
this.logger.debug('No tracks to attach to a peer connection.');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
newTracks.forEach((track) => this.pc.addTrack(track, stream));
|
|
135
|
+
this.logger.debug(`Attached ${newTracks.length} track(s) to a peer connection.`);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Remove ended tracks and unknown tracks and invoke `attachStream`.
|
|
139
|
+
* If stream is not passed then all tracks will be removed.
|
|
140
|
+
*/
|
|
141
|
+
reattachStream(stream) {
|
|
142
|
+
// Reattach only if connected or just created
|
|
143
|
+
const state = this.pc.iceConnectionState;
|
|
144
|
+
if (state !== 'new' && state !== 'connected' && state !== 'completed')
|
|
145
|
+
return;
|
|
146
|
+
// Remove diff tracks from connection
|
|
147
|
+
// If do not remove tracks then they are piling up on the remote client (in received stream).
|
|
148
|
+
// The removing track must by in `ended` readyState.
|
|
149
|
+
const tracks = stream ? stream.getTracks() : [];
|
|
150
|
+
this.pc.getSenders().forEach((s) => {
|
|
151
|
+
const { track } = s;
|
|
152
|
+
if (track && (track.readyState === 'ended' || !tracks.find((_) => _.id === track.id))) {
|
|
153
|
+
this.pc.removeTrack(s);
|
|
154
|
+
this.logger.debug(`'${track.kind}' track is removed from a peer connection.`);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
if (stream)
|
|
158
|
+
this.attachStream(stream);
|
|
159
|
+
}
|
|
160
|
+
/** A call to establish a connection. */
|
|
161
|
+
async createOffer() {
|
|
162
|
+
const desc = await this.pc.createOffer(this.options.offerOptions);
|
|
163
|
+
// this.logger.debug(`Offer created with: ${JSON.stringify(options)}`);
|
|
164
|
+
return this.setLocalDescription(this.pc, desc);
|
|
165
|
+
}
|
|
166
|
+
/** A call from another client which received an offer. */
|
|
167
|
+
async createAnswer(offer) {
|
|
168
|
+
await this.setRemoteDescription(this.pc, offer);
|
|
169
|
+
const desc = await this.pc.createAnswer(this.options.offerOptions);
|
|
170
|
+
return this.setLocalDescription(this.pc, desc);
|
|
171
|
+
}
|
|
172
|
+
/** Apply answer from another client after calling `createOffer`. */
|
|
173
|
+
applyAnswer(remoteDesc) {
|
|
174
|
+
return this.setRemoteDescription(this.pc, remoteDesc);
|
|
175
|
+
}
|
|
176
|
+
reconnect() {
|
|
177
|
+
this.close();
|
|
178
|
+
this.pc = this.createPC();
|
|
179
|
+
}
|
|
180
|
+
/** After calling this method the connection can no longer be used unless `reconnect` will be called. */
|
|
181
|
+
close() {
|
|
182
|
+
this.pc.close();
|
|
183
|
+
// The event is not fired if close manually
|
|
184
|
+
if (this.pc.onsignalingstatechange) {
|
|
185
|
+
this.emit(this.Events.Closed);
|
|
186
|
+
this.clear();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
destroy() {
|
|
190
|
+
this.close();
|
|
191
|
+
this.removeAllListeners();
|
|
192
|
+
}
|
|
193
|
+
[Symbol.dispose]() {
|
|
194
|
+
this.destroy();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
198
|
+
(function (PeerConnection) {
|
|
199
|
+
let Events;
|
|
200
|
+
(function (Events) {
|
|
201
|
+
/** IceCandidate for exchanging. */
|
|
202
|
+
Events["LocalIceCandidate"] = "LocalIceCandidate";
|
|
203
|
+
Events["EndOfIceCandidates"] = "EndOfIceCandidates";
|
|
204
|
+
/** The connection is established. */
|
|
205
|
+
Events["Connected"] = "Connected";
|
|
206
|
+
/** The remote client closed a connection or connection is failed. */
|
|
207
|
+
Events["Disconnected"] = "Disconnected";
|
|
208
|
+
Events["RemoteStreamChanged"] = "RemoteStreamChanged";
|
|
209
|
+
Events["ReinitializingConnectionRequired"] = "ReinitializingConnectionRequired";
|
|
210
|
+
Events["Closed"] = "Closed";
|
|
211
|
+
})(Events = PeerConnection.Events || (PeerConnection.Events = {}));
|
|
212
|
+
})(PeerConnection || (PeerConnection = {}));
|