@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
package/iframe/isIframeLoaded.js
CHANGED
|
@@ -1 +1,9 @@
|
|
|
1
|
-
export function isIframeLoaded(
|
|
1
|
+
export function isIframeLoaded(iframe, { blank, complete } = {}) {
|
|
2
|
+
if (!iframe.contentDocument)
|
|
3
|
+
return false;
|
|
4
|
+
const { documentURI, readyState } = iframe.contentDocument;
|
|
5
|
+
return ((blank || (!!documentURI && documentURI !== 'about:blank')) &&
|
|
6
|
+
(complete
|
|
7
|
+
? readyState === 'complete'
|
|
8
|
+
: readyState === 'interactive' || readyState === 'complete'));
|
|
9
|
+
}
|
package/iframe/messages.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
export declare const IFRAME_PING = "@_IFRAME_PING";
|
|
2
2
|
export declare const IFRAME_HOST_READY = "@_IFRAME_HOST_READY";
|
|
3
3
|
export declare const IFRAME_CLIENT_READY = "@_IFRAME_CLIENT_READY";
|
|
4
|
-
export
|
|
4
|
+
export interface MessagesTypes {
|
|
5
5
|
readonly Ping: string;
|
|
6
6
|
readonly TargetReady: string;
|
|
7
7
|
readonly SelfReady: string;
|
|
8
|
-
}
|
|
8
|
+
}
|
|
9
9
|
export declare function getHostMessages(): MessagesTypes;
|
|
10
10
|
export declare function getClientMessages(): MessagesTypes;
|
|
11
11
|
export interface IframeMessage<T extends keyof MessagesTypes = keyof MessagesTypes> {
|
package/iframe/messages.js
CHANGED
|
@@ -1 +1,50 @@
|
|
|
1
|
-
export const IFRAME_PING
|
|
1
|
+
export const IFRAME_PING = '@_IFRAME_PING';
|
|
2
|
+
export const IFRAME_HOST_READY = '@_IFRAME_HOST_READY';
|
|
3
|
+
export const IFRAME_CLIENT_READY = '@_IFRAME_CLIENT_READY';
|
|
4
|
+
export function getHostMessages() {
|
|
5
|
+
return {
|
|
6
|
+
Ping: IFRAME_PING,
|
|
7
|
+
SelfReady: IFRAME_HOST_READY,
|
|
8
|
+
TargetReady: IFRAME_CLIENT_READY,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function getClientMessages() {
|
|
12
|
+
return {
|
|
13
|
+
Ping: IFRAME_PING,
|
|
14
|
+
SelfReady: IFRAME_CLIENT_READY,
|
|
15
|
+
TargetReady: IFRAME_HOST_READY,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
// export type IframePingMessage = IframeMessage<'Ping'>;
|
|
19
|
+
// export interface IframeHostReadyMessage<T> extends IframeDataMessage<'HostReady', T> {}
|
|
20
|
+
// export interface IframeClientReadyMessage<T> extends IframeDataMessage<'ClientReady', T> {}
|
|
21
|
+
// export function isIframeMessage<T extends keyof MessagesTypes>(
|
|
22
|
+
// data: unknown,
|
|
23
|
+
// validType: T
|
|
24
|
+
// ): data is IframeMessage<T> {
|
|
25
|
+
// return !!data && (data as IframeMessage<T>).type === validType;
|
|
26
|
+
// }
|
|
27
|
+
// export function isIframeDataMessage<T extends string, D>(
|
|
28
|
+
// data: unknown,
|
|
29
|
+
// validType: T
|
|
30
|
+
// ): data is IframeDataMessage<T, D> {
|
|
31
|
+
// return !!data && (data as IframeDataMessage<T, D>).type === validType;
|
|
32
|
+
// }
|
|
33
|
+
export function isPingMessage(data, messagesTypes) {
|
|
34
|
+
return data != null && data.type === messagesTypes.Ping;
|
|
35
|
+
}
|
|
36
|
+
export function isTargetReadyMessage(data, messagesTypes) {
|
|
37
|
+
return data != null && data.type === messagesTypes.TargetReady;
|
|
38
|
+
}
|
|
39
|
+
// export function isHostReadyMessage<T>(
|
|
40
|
+
// data: unknown,
|
|
41
|
+
// messagesTypes: MessagesTypes
|
|
42
|
+
// ): data is IframeDataMessage<'HostReady', T> {
|
|
43
|
+
// return !!data && (data as IframeDataMessage).type === messagesTypes.HostReady;
|
|
44
|
+
// }
|
|
45
|
+
// export function isClientReadyMessage<T>(
|
|
46
|
+
// data: unknown,
|
|
47
|
+
// messagesTypes: MessagesTypes
|
|
48
|
+
// ): data is IframeDataMessage<'ClientReady', T> {
|
|
49
|
+
// return !!data && (data as IframeDataMessage).type === messagesTypes.ClientReady;
|
|
50
|
+
// }
|
package/iframe/utils.js
CHANGED
|
@@ -1 +1,33 @@
|
|
|
1
|
-
export function findTarget(
|
|
1
|
+
export function findTarget(source, targets
|
|
2
|
+
// logger: Pick<Console, 'warn'> | undefined = console
|
|
3
|
+
) {
|
|
4
|
+
const { length } = targets;
|
|
5
|
+
for (let i = 0; i < length; i += 1) {
|
|
6
|
+
const frame = targets[i];
|
|
7
|
+
const window = frame instanceof HTMLIFrameElement ? frame.contentWindow : frame;
|
|
8
|
+
if (window === source)
|
|
9
|
+
return frame;
|
|
10
|
+
// if (window == null) {
|
|
11
|
+
// logger.warn(`Search target: <iframe>(#${frame.id}) contentWindow is undefined.`);
|
|
12
|
+
// }
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
export function isWindowProxy(target) {
|
|
17
|
+
return !((window.MessagePort !== undefined && target instanceof MessagePort) ||
|
|
18
|
+
(window.ServiceWorker !== undefined && target instanceof ServiceWorker));
|
|
19
|
+
}
|
|
20
|
+
export function readTargets(list) {
|
|
21
|
+
// const result = new Array<Window>(list.length);
|
|
22
|
+
const result = new Array();
|
|
23
|
+
const { length } = list;
|
|
24
|
+
for (let i = 0; i < length; i += 1) {
|
|
25
|
+
const frame = list[i];
|
|
26
|
+
const window = frame instanceof HTMLIFrameElement ? frame.contentWindow : frame;
|
|
27
|
+
if (window) {
|
|
28
|
+
result.push(window);
|
|
29
|
+
// result[i] = window;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
package/imageToBlob.js
CHANGED
|
@@ -1 +1,20 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { get2dContextError } from './takeScreenshot';
|
|
2
|
+
export function imageToBlob(image) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
const canvas = document.createElement('canvas');
|
|
5
|
+
canvas.width = image.width;
|
|
6
|
+
canvas.height = image.height;
|
|
7
|
+
const ctx = canvas.getContext('2d');
|
|
8
|
+
if (!ctx)
|
|
9
|
+
throw get2dContextError();
|
|
10
|
+
ctx.drawImage(image, 0, 0);
|
|
11
|
+
canvas.toBlob((blob) => {
|
|
12
|
+
if (blob) {
|
|
13
|
+
resolve(blob);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
reject(new Error('Unable to get blob from image.'));
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
package/isImageTypeSupported.js
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
|
-
export function isImageTypeSupported(
|
|
1
|
+
export function isImageTypeSupported(mimeType) {
|
|
2
|
+
try {
|
|
3
|
+
return document.createElement('canvas').toDataURL(mimeType).startsWith(`data:${mimeType}`);
|
|
4
|
+
}
|
|
5
|
+
catch {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
}
|
package/isWebPSupported.js
CHANGED
|
@@ -1 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
// https://developers.google.com/speed/webp/faq#how_can_i_detect_browser_support_for_webp
|
|
2
|
+
export async function isWebPSupported(feature = 'lossy') {
|
|
3
|
+
const testImages = {
|
|
4
|
+
lossy: 'UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA',
|
|
5
|
+
lossless: 'UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==',
|
|
6
|
+
alpha: 'UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAwAAAARBxAR/Q9ERP8DAABWUDggGAAAABQBAJ0BKgEAAQAAAP4AAA3AAP7mtQAAAA==',
|
|
7
|
+
animation: 'UklGRlIAAABXRUJQVlA4WAoAAAASAAAAAAAAAAAAQU5JTQYAAAD/////AABBTk1GJgAAAAAAAAAAAAAAAAAAAGQAAABWUDhMDQAAAC8AAAAQBxAREYiI/gcA',
|
|
8
|
+
};
|
|
9
|
+
const img = new Image();
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
img.onload = () => resolve(img.width > 0 && img.height > 0);
|
|
12
|
+
img.onerror = () => resolve(false);
|
|
13
|
+
img.src = `data:image/webp;base64,${testImages[feature]}`;
|
|
14
|
+
});
|
|
15
|
+
}
|
package/loadImage.js
CHANGED
|
@@ -1 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
/** Don't worry, it uses cached by browser image if url already loaded previously otherwise cache the image. */
|
|
2
|
+
export function loadImage(src
|
|
3
|
+
// crossOrigin?: HTMLImageElement['crossOrigin'] | undefined
|
|
4
|
+
) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
const img = new Image();
|
|
7
|
+
img.onload = () => {
|
|
8
|
+
img.onload = null;
|
|
9
|
+
img.onerror = null;
|
|
10
|
+
resolve(img);
|
|
11
|
+
};
|
|
12
|
+
img.onerror = (event) => {
|
|
13
|
+
img.onload = null;
|
|
14
|
+
img.onerror = null;
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
|
|
16
|
+
reject(event);
|
|
17
|
+
};
|
|
18
|
+
if (typeof src === 'string') {
|
|
19
|
+
img.src = src;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
const { crossOrigin, ...rest } = src;
|
|
23
|
+
if (crossOrigin !== undefined) {
|
|
24
|
+
img.crossOrigin = crossOrigin;
|
|
25
|
+
}
|
|
26
|
+
Object.assign(img, rest);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
package/loadScript.d.ts
CHANGED
|
@@ -8,4 +8,4 @@ export interface LoadScriptOptions extends Partial<Pick<HTMLScriptElement, 'id'>
|
|
|
8
8
|
/** Defaults to `true`. */
|
|
9
9
|
waitDomReady?: boolean | undefined;
|
|
10
10
|
}
|
|
11
|
-
export declare function loadScript(url: string, { id, keepScript, async, defer, waitDomReady, }?: LoadScriptOptions, isExecuted?: (
|
|
11
|
+
export declare function loadScript(url: string, { id, keepScript, async, defer, waitDomReady, }?: LoadScriptOptions, isExecuted?: () => boolean): Promise<void>;
|
package/loadScript.js
CHANGED
|
@@ -1 +1,67 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-use-before-define */
|
|
2
|
+
import { onDOMReady } from './onDOMReady';
|
|
3
|
+
function findScript(src) {
|
|
4
|
+
const url = src.startsWith('//') ? window.location.protocol + src : src;
|
|
5
|
+
const { length } = document.scripts;
|
|
6
|
+
for (let i = 0; i < length; i += 1) {
|
|
7
|
+
if (document.scripts[i].src === url) {
|
|
8
|
+
return document.scripts[i];
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
function load(url, { id, keepScript, async, defer }, isExecuted, resolve, reject) {
|
|
14
|
+
try {
|
|
15
|
+
const addedScript = id ? document.scripts.namedItem(id) : findScript(url);
|
|
16
|
+
if (addedScript && (!isExecuted || isExecuted())) {
|
|
17
|
+
resolve();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const scriptElement = addedScript ?? document.createElement('script');
|
|
21
|
+
const done = () => {
|
|
22
|
+
scriptElement.removeEventListener('load', onLoad);
|
|
23
|
+
scriptElement.removeEventListener('error', onError);
|
|
24
|
+
if (!keepScript && !addedScript) {
|
|
25
|
+
scriptElement.remove();
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const onLoad = () => {
|
|
29
|
+
done();
|
|
30
|
+
resolve();
|
|
31
|
+
};
|
|
32
|
+
const onError = (error) => {
|
|
33
|
+
done();
|
|
34
|
+
const ex = error instanceof ErrorEvent
|
|
35
|
+
? error
|
|
36
|
+
: new Error(`Unable to load script by url ${url}.`, { cause: error });
|
|
37
|
+
reject(ex);
|
|
38
|
+
};
|
|
39
|
+
// Subscribe in any way because the script may be already added but not executed/loaded yet.
|
|
40
|
+
scriptElement.addEventListener('load', onLoad, { once: true });
|
|
41
|
+
scriptElement.addEventListener('error', onError, { once: true });
|
|
42
|
+
if (!addedScript) {
|
|
43
|
+
if (id) {
|
|
44
|
+
scriptElement.id = id;
|
|
45
|
+
}
|
|
46
|
+
scriptElement.async = async;
|
|
47
|
+
scriptElement.defer = defer;
|
|
48
|
+
scriptElement.src = url;
|
|
49
|
+
document.head.appendChild(scriptElement);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
reject(error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function loadScript(url, { id, keepScript = false, async = true, defer = false, waitDomReady = true, } = {}, isExecuted) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
if (waitDomReady) {
|
|
59
|
+
onDOMReady(() => {
|
|
60
|
+
load(url, { id: id ?? '', keepScript, async, defer }, isExecuted, resolve, reject);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
load(url, { id: id ?? '', keepScript, async, defer }, isExecuted, resolve, reject);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
package/media/Capabilities.js
CHANGED
|
@@ -1 +1,44 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-extraneous-class */
|
|
2
|
+
import { getMediaSource } from './getMediaSource';
|
|
3
|
+
export class Capabilities {
|
|
4
|
+
static supportMap = new Map();
|
|
5
|
+
static canPlayMap = new Map();
|
|
6
|
+
static tmpVideo;
|
|
7
|
+
static cacheTimer;
|
|
8
|
+
/** Check video element. Mime type and optional codecs. */
|
|
9
|
+
static isCanPlayType(type) {
|
|
10
|
+
if (this.canPlayMap.has(type)) {
|
|
11
|
+
return !!this.canPlayMap.get(type);
|
|
12
|
+
}
|
|
13
|
+
if (!this.tmpVideo) {
|
|
14
|
+
this.tmpVideo = document.getElementsByTagName('video')[0] ?? document.createElement('video');
|
|
15
|
+
// Release handle for GC work.
|
|
16
|
+
window.clearTimeout(this.cacheTimer);
|
|
17
|
+
this.cacheTimer = window.setTimeout(() => {
|
|
18
|
+
this.tmpVideo = undefined;
|
|
19
|
+
}, 1000);
|
|
20
|
+
}
|
|
21
|
+
const support = !!this.tmpVideo.canPlayType(type);
|
|
22
|
+
this.supportMap.set(type, support);
|
|
23
|
+
return support;
|
|
24
|
+
}
|
|
25
|
+
/** Check MediaSource. */
|
|
26
|
+
static isTypeSupported(type, managedMediaSource) {
|
|
27
|
+
if (this.supportMap.has(type)) {
|
|
28
|
+
return !!this.supportMap.get(type);
|
|
29
|
+
}
|
|
30
|
+
const mediaSource = getMediaSource(managedMediaSource);
|
|
31
|
+
if (mediaSource) {
|
|
32
|
+
const support = mediaSource.isTypeSupported(type);
|
|
33
|
+
this.supportMap.set(type, support);
|
|
34
|
+
return support;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
static reset() {
|
|
39
|
+
window.clearTimeout(this.cacheTimer);
|
|
40
|
+
this.tmpVideo = undefined;
|
|
41
|
+
this.supportMap.clear();
|
|
42
|
+
this.canPlayMap.clear();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1 +1,6 @@
|
|
|
1
|
-
import{ErrorCompat}from
|
|
1
|
+
import { ErrorCompat } from '@js-toolkit/utils/ErrorCompat';
|
|
2
|
+
export class MediaNotAttachedError extends ErrorCompat {
|
|
3
|
+
constructor(message = 'Media element is not attached yet.', options) {
|
|
4
|
+
super(MediaNotAttachedError, message, { ...options, name: 'MediaNotAttachedError ' });
|
|
5
|
+
}
|
|
6
|
+
}
|
|
@@ -1 +1,84 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { resetMedia } from './resetMedia';
|
|
2
|
+
export function attachMediaStream(media, stream) {
|
|
3
|
+
// Reassign stream will trigger events onEmptied, onLoadStart and so on.
|
|
4
|
+
// If stream is empty or has only audio and muted then will fire onLoadStart and stop on it,
|
|
5
|
+
// so we need assign null instead of empty stream in order for fire onEmptied and stop on it.
|
|
6
|
+
if (stream &&
|
|
7
|
+
stream.active &&
|
|
8
|
+
media.muted &&
|
|
9
|
+
// Assign null if screen sharing (especially for local stream)
|
|
10
|
+
stream.getVideoTracks().filter((t) => !t.getSettings().displaySurface).length === 0) {
|
|
11
|
+
media.srcObject = null;
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
media.srcObject = stream?.active ? stream : null;
|
|
15
|
+
}
|
|
16
|
+
export function removeTrack(mediaStream, track) {
|
|
17
|
+
mediaStream.removeTrack(track);
|
|
18
|
+
// Dispatch event manually because on local stream it not fired
|
|
19
|
+
mediaStream.dispatchEvent(new MediaStreamTrackEvent('removetrack', { track }));
|
|
20
|
+
}
|
|
21
|
+
export function addTrack(mediaStream, track, onEnded) {
|
|
22
|
+
mediaStream.addTrack(track);
|
|
23
|
+
// Dispatch event manually because on local stream it not fired
|
|
24
|
+
mediaStream.dispatchEvent(new MediaStreamTrackEvent('addtrack', { track }));
|
|
25
|
+
// When track is uncontrolled ended then remove it from stream
|
|
26
|
+
track.addEventListener('ended', () => {
|
|
27
|
+
removeTrack(mediaStream, track);
|
|
28
|
+
if (onEnded)
|
|
29
|
+
onEnded();
|
|
30
|
+
}, { once: true });
|
|
31
|
+
}
|
|
32
|
+
export class MediaStreamController {
|
|
33
|
+
mediaStream;
|
|
34
|
+
media;
|
|
35
|
+
constructor(mediaStream) {
|
|
36
|
+
this.mediaStream = mediaStream;
|
|
37
|
+
}
|
|
38
|
+
attach(media) {
|
|
39
|
+
// if (this.media !== media) {
|
|
40
|
+
// this.detachMedia();
|
|
41
|
+
// }
|
|
42
|
+
this.media = media;
|
|
43
|
+
attachMediaStream(media, this.mediaStream);
|
|
44
|
+
}
|
|
45
|
+
detach() {
|
|
46
|
+
if (this.media) {
|
|
47
|
+
const { media } = this;
|
|
48
|
+
this.media = undefined;
|
|
49
|
+
resetMedia(media);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
updateStream(stream) {
|
|
53
|
+
if (this.mediaStream === stream)
|
|
54
|
+
return;
|
|
55
|
+
const { media } = this;
|
|
56
|
+
// this.detachMedia();
|
|
57
|
+
this.mediaStream = stream;
|
|
58
|
+
if (media)
|
|
59
|
+
this.attach(media);
|
|
60
|
+
}
|
|
61
|
+
removeTrack(track) {
|
|
62
|
+
removeTrack(this.mediaStream, track);
|
|
63
|
+
}
|
|
64
|
+
addTrack(track, onEnded) {
|
|
65
|
+
addTrack(this.mediaStream, track, () => {
|
|
66
|
+
this.removeTrack(track);
|
|
67
|
+
if (onEnded)
|
|
68
|
+
onEnded();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
reset() {
|
|
72
|
+
this.mediaStream.getTracks().forEach((track) => {
|
|
73
|
+
track.stop();
|
|
74
|
+
this.removeTrack(track);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
destroy() {
|
|
78
|
+
this.detach();
|
|
79
|
+
this.reset();
|
|
80
|
+
}
|
|
81
|
+
[Symbol.dispose]() {
|
|
82
|
+
this.destroy();
|
|
83
|
+
}
|
|
84
|
+
}
|
package/media/PipController.d.ts
CHANGED
package/media/PipController.js
CHANGED
|
@@ -1 +1,140 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unnecessary-type-conversion */
|
|
2
|
+
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
|
|
3
|
+
import { EventEmitter } from '@js-toolkit/utils/EventEmitter';
|
|
4
|
+
import { EventEmitterListener } from '../EventEmitterListener';
|
|
5
|
+
import { toggleNativeSubtitles } from './toggleNativeSubtitles';
|
|
6
|
+
const getPipUnavailableError = () => new Error('PiP is not available');
|
|
7
|
+
export class PipController extends EventEmitter {
|
|
8
|
+
options;
|
|
9
|
+
static isApiEnabled() {
|
|
10
|
+
return (!!HTMLVideoElement.prototype.requestPictureInPicture &&
|
|
11
|
+
!!document.exitPictureInPicture &&
|
|
12
|
+
!!document.pictureInPictureEnabled);
|
|
13
|
+
}
|
|
14
|
+
// Only able to call on instance
|
|
15
|
+
static isWebkitApiEnabled(media) {
|
|
16
|
+
return (!!media.webkitSupportsPresentationMode &&
|
|
17
|
+
media.webkitSupportsPresentationMode('picture-in-picture'));
|
|
18
|
+
}
|
|
19
|
+
static isAvailable(video) {
|
|
20
|
+
return ((this.isApiEnabled() && !video.disablePictureInPicture) || this.isWebkitApiEnabled(video));
|
|
21
|
+
}
|
|
22
|
+
// eslint-disable-next-line class-methods-use-this
|
|
23
|
+
get Events() {
|
|
24
|
+
return PipController.Events;
|
|
25
|
+
}
|
|
26
|
+
listener;
|
|
27
|
+
constructor(video, options = {}) {
|
|
28
|
+
super();
|
|
29
|
+
this.options = options;
|
|
30
|
+
this.listener = new EventEmitterListener(video);
|
|
31
|
+
if (PipController.isAvailable(video)) {
|
|
32
|
+
const enterPipHandler = (() => {
|
|
33
|
+
const handler = () => {
|
|
34
|
+
handler.nativeSubtitles =
|
|
35
|
+
this.options.toggleNativeSubtitles && this.listener.target.textTracks.length > 0;
|
|
36
|
+
if (handler.nativeSubtitles) {
|
|
37
|
+
toggleNativeSubtitles(true, this.listener.target.textTracks);
|
|
38
|
+
}
|
|
39
|
+
this.emit(this.Events.Change, { pip: true });
|
|
40
|
+
};
|
|
41
|
+
handler.nativeSubtitles = undefined;
|
|
42
|
+
return handler;
|
|
43
|
+
})();
|
|
44
|
+
const exitPipHandler = () => {
|
|
45
|
+
if (enterPipHandler.nativeSubtitles) {
|
|
46
|
+
toggleNativeSubtitles(false, this.listener.target.textTracks);
|
|
47
|
+
}
|
|
48
|
+
this.emit(this.Events.Change, { pip: false });
|
|
49
|
+
};
|
|
50
|
+
if (PipController.isApiEnabled()) {
|
|
51
|
+
this.listener.on('enterpictureinpicture', enterPipHandler);
|
|
52
|
+
this.listener.on('leavepictureinpicture', exitPipHandler);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
this.listener.on('webkitpresentationmodechanged', (() => {
|
|
56
|
+
let lastMode = this.listener.target.webkitPresentationMode;
|
|
57
|
+
return () => {
|
|
58
|
+
if (this.listener.target.webkitPresentationMode === 'picture-in-picture') {
|
|
59
|
+
enterPipHandler();
|
|
60
|
+
}
|
|
61
|
+
else if (lastMode === 'picture-in-picture') {
|
|
62
|
+
exitPipHandler();
|
|
63
|
+
}
|
|
64
|
+
lastMode = this.listener.target.webkitPresentationMode;
|
|
65
|
+
};
|
|
66
|
+
})(), true);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
isPip() {
|
|
71
|
+
return PipController.isApiEnabled()
|
|
72
|
+
? document.pictureInPictureElement === this.listener.target
|
|
73
|
+
: this.listener.target.webkitPresentationMode === 'picture-in-picture';
|
|
74
|
+
}
|
|
75
|
+
getCurrentElement() {
|
|
76
|
+
return this.isPip() ? this.listener.target : null;
|
|
77
|
+
}
|
|
78
|
+
request() {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
if (this.isPip()) {
|
|
81
|
+
resolve();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!PipController.isAvailable(this.listener.target)) {
|
|
85
|
+
throw getPipUnavailableError();
|
|
86
|
+
}
|
|
87
|
+
if (PipController.isApiEnabled()) {
|
|
88
|
+
void this.listener.target.requestPictureInPicture().then(() => resolve(), reject);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
this.listener.once('webkitpresentationmodechanged', () => {
|
|
92
|
+
if (this.listener.target.webkitPresentationMode === 'picture-in-picture')
|
|
93
|
+
resolve();
|
|
94
|
+
else
|
|
95
|
+
reject(new Error('Something went wrong.'));
|
|
96
|
+
}, true);
|
|
97
|
+
this.listener.target.webkitSetPresentationMode('picture-in-picture');
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
exit() {
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
if (!this.isPip()) {
|
|
104
|
+
resolve();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!PipController.isAvailable(this.listener.target)) {
|
|
108
|
+
throw getPipUnavailableError();
|
|
109
|
+
}
|
|
110
|
+
if (PipController.isApiEnabled()) {
|
|
111
|
+
void document.exitPictureInPicture().then(resolve, reject);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
this.listener.once('webkitpresentationmodechanged', () => {
|
|
115
|
+
if (this.listener.target.webkitPresentationMode !== 'picture-in-picture')
|
|
116
|
+
resolve();
|
|
117
|
+
else
|
|
118
|
+
reject(new Error('Something went wrong.'));
|
|
119
|
+
}, true);
|
|
120
|
+
this.listener.target.webkitSetPresentationMode('inline');
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
destroy() {
|
|
125
|
+
return this.exit().finally(() => {
|
|
126
|
+
this.removeAllListeners();
|
|
127
|
+
this.listener.removeAllListeners();
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
[Symbol.asyncDispose]() {
|
|
131
|
+
return this.destroy();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
135
|
+
(function (PipController) {
|
|
136
|
+
let Events;
|
|
137
|
+
(function (Events) {
|
|
138
|
+
Events["Change"] = "change";
|
|
139
|
+
})(Events = PipController.Events || (PipController.Events = {}));
|
|
140
|
+
})(PipController || (PipController = {}));
|
|
@@ -6,9 +6,6 @@ declare global {
|
|
|
6
6
|
interface TextTrack {
|
|
7
7
|
native?: boolean | undefined;
|
|
8
8
|
}
|
|
9
|
-
interface TextTrackList {
|
|
10
|
-
[index: number]: TextTrack | undefined;
|
|
11
|
-
}
|
|
12
9
|
}
|
|
13
10
|
interface TextTracksEventMap {
|
|
14
11
|
texttracklistchange: CustomEvent<TextTracksController.EventMap[TextTracksController.Events.TextTrackListChanged][0]>;
|