mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(media)!: add Google Cast by default to HLS, DASH media (#1661)
This commit is contained in:
@@ -16,6 +16,7 @@ export { isMacOS } from './platform';
|
||||
export { tryHidePopover, tryShowPopover } from './popover';
|
||||
export { isHTMLAudioElement, isHTMLMediaElement, isHTMLVideoElement } from './predicates';
|
||||
export { type RafThrottled, rafThrottle } from './raf-throttle';
|
||||
export { loadScript } from './script';
|
||||
export {
|
||||
applyShadowStyles,
|
||||
createShadowStyle,
|
||||
@@ -33,7 +34,13 @@ export {
|
||||
isCaptionOrSubtitleTrack,
|
||||
} from './text-track';
|
||||
export { serializeTimeRanges } from './time-ranges';
|
||||
export type { CustomElement, CustomElementCallbacks } from './types';
|
||||
export type {
|
||||
CustomElement,
|
||||
CustomElementCallbacks,
|
||||
EventListenerFor,
|
||||
EventType,
|
||||
QueriedElement,
|
||||
} from './types';
|
||||
export {
|
||||
isWebKitAirPlayCapable,
|
||||
supportsWebKitAirPlay,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
const cache = new Map<string, Promise<void>>();
|
||||
|
||||
export function hasScript(src: string): boolean {
|
||||
for (const script of document.scripts) {
|
||||
if (script.getAttribute('src') === src) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a script once. Concurrent and repeat calls for the same `src` share a
|
||||
* single promise; failed loads are evicted (and the tag removed) so they can
|
||||
* be retried.
|
||||
*/
|
||||
export function loadScript(src: string): Promise<void> {
|
||||
let promise = cache.get(src);
|
||||
if (promise) return promise;
|
||||
|
||||
// Assume a tag we didn't create (e.g. added directly in HTML) has loaded or will load.
|
||||
if (hasScript(src)) return Promise.resolve();
|
||||
|
||||
promise = new Promise<void>((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.onload = () => resolve();
|
||||
script.onerror = () => {
|
||||
script.remove();
|
||||
reject(new Error(`Failed to load script: ${src}`));
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
cache.set(src, promise);
|
||||
// Evict on failure so callers can retry; must not propagate the rejection.
|
||||
promise.catch(() => cache.delete(src));
|
||||
|
||||
return promise;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { hasScript, loadScript } from '../script';
|
||||
|
||||
function getScript(src: string) {
|
||||
return document.head.querySelector(`script[src="${src}"]`);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
document.head.innerHTML = '';
|
||||
});
|
||||
|
||||
describe('loadScript', () => {
|
||||
it('appends a script tag and resolves on load', async () => {
|
||||
const src = 'https://example.com/load.js';
|
||||
const promise = loadScript(src);
|
||||
|
||||
const script = getScript(src);
|
||||
expect(script).not.toBeNull();
|
||||
|
||||
script!.dispatchEvent(new Event('load'));
|
||||
await expect(promise).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('shares a single promise for concurrent calls', () => {
|
||||
const src = 'https://example.com/concurrent.js';
|
||||
const first = loadScript(src);
|
||||
const second = loadScript(src);
|
||||
|
||||
expect(first).toBe(second);
|
||||
expect(document.querySelectorAll(`script[src="${src}"]`)).toHaveLength(1);
|
||||
|
||||
getScript(src)!.dispatchEvent(new Event('load'));
|
||||
});
|
||||
|
||||
it('reuses the cached promise after a successful load', async () => {
|
||||
const src = 'https://example.com/cached.js';
|
||||
const first = loadScript(src);
|
||||
getScript(src)!.dispatchEvent(new Event('load'));
|
||||
await first;
|
||||
|
||||
expect(loadScript(src)).toBe(first);
|
||||
expect(document.querySelectorAll(`script[src="${src}"]`)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('rejects with an error, removes the tag, and allows a retry', async () => {
|
||||
const src = 'https://example.com/fail.js';
|
||||
const first = loadScript(src);
|
||||
|
||||
getScript(src)!.dispatchEvent(new Event('error'));
|
||||
|
||||
await expect(first).rejects.toThrowError(`Failed to load script: ${src}`);
|
||||
expect(getScript(src)).toBeNull();
|
||||
|
||||
const retry = loadScript(src);
|
||||
expect(retry).not.toBe(first);
|
||||
expect(getScript(src)).not.toBeNull();
|
||||
|
||||
getScript(src)!.dispatchEvent(new Event('load'));
|
||||
await expect(retry).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('resolves without adding a tag when the script already exists', async () => {
|
||||
const src = 'https://example.com/existing.js';
|
||||
const script = document.createElement('script');
|
||||
script.setAttribute('src', src);
|
||||
document.head.appendChild(script);
|
||||
|
||||
await expect(loadScript(src)).resolves.toBeUndefined();
|
||||
expect(document.querySelectorAll(`script[src="${src}"]`)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasScript', () => {
|
||||
it('matches scripts by their src attribute', () => {
|
||||
const script = document.createElement('script');
|
||||
script.setAttribute('src', 'https://example.com/has.js');
|
||||
document.head.appendChild(script);
|
||||
|
||||
expect(hasScript('https://example.com/has.js')).toBe(true);
|
||||
expect(hasScript('https://example.com/other.js')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -13,3 +13,14 @@ export interface CustomElement extends HTMLElement, CustomElementCallbacks {}
|
||||
export interface CustomElementConstructor {
|
||||
new (): CustomElement;
|
||||
}
|
||||
|
||||
export type QueriedElement<S extends string, E extends Element> = S extends keyof HTMLElementTagNameMap
|
||||
? HTMLElementTagNameMap[S]
|
||||
: E;
|
||||
|
||||
export type EventType<Events> = (keyof Events & string) | (string & {});
|
||||
|
||||
export type EventListenerFor<Events, K> =
|
||||
| ((event: K extends keyof Events ? Events[K] : Event) => void)
|
||||
| EventListenerOrEventListenerObject
|
||||
| null;
|
||||
|
||||
Reference in New Issue
Block a user