mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat: add media delegate mixin (#598)
This commit is contained in:
@@ -161,7 +161,7 @@ interface MediaApi extends MediaBaseApi,
|
||||
export interface MediaApiProxyTarget extends EventTarget {}
|
||||
|
||||
// Proxy mixin that creates a MediaApi from the passed classes and proxies the methods and properties to the attached target.
|
||||
export const MediaApiProxyMixin = <T extends EventTarget>(
|
||||
export const MediaProxyMixin = <T extends EventTarget>(
|
||||
...MediaApiTargetClasses: AnyConstructor<T extends [unknown, ...unknown[]] ? T : any>[]
|
||||
) => class MediaApiProxy {
|
||||
// Logic that proxies media API to the media target
|
||||
@@ -179,9 +179,9 @@ export const MediaApiProxyMixin = <T extends EventTarget>(
|
||||
}
|
||||
|
||||
// Size optimized for the most common use case of proxying HTMLMediaElement.
|
||||
export class MediaApiProxy extends MediaApiProxyMixin<HTMLMediaElement>(HTMLMediaElement, EventTarget) {}
|
||||
export class VideoApiProxy extends MediaApiProxyMixin<HTMLVideoElement>(HTMLVideoElement, HTMLMediaElement, EventTarget) {}
|
||||
export class AudioApiProxy extends MediaApiProxyMixin<HTMLAudioElement>(HTMLAudioElement, HTMLMediaElement, EventTarget) {}
|
||||
export class MediaApiProxy extends MediaProxyMixin<HTMLMediaElement>(HTMLMediaElement, EventTarget) {}
|
||||
export class VideoApiProxy extends MediaProxyMixin<HTMLVideoElement>(HTMLVideoElement, HTMLMediaElement, EventTarget) {}
|
||||
export class AudioApiProxy extends MediaProxyMixin<HTMLAudioElement>(HTMLAudioElement, HTMLMediaElement, EventTarget) {}
|
||||
```
|
||||
|
||||
### Example of using the media API Proxy
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { Constructor } from '@videojs/utils/types';
|
||||
|
||||
export interface MediaDelegate {
|
||||
attach?(target: EventTarget): void;
|
||||
detach?(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixin that intercepts `get`, `set`, and `call` to delegate property access
|
||||
* and method calls to an instance of `DelegateClass` before falling through
|
||||
* to the base class implementation.
|
||||
*
|
||||
* Works with both `CustomMediaMixin` and `MediaProxyMixin`.
|
||||
*/
|
||||
export function MediaDelegateMixin<Base extends Constructor<any>, Delegate extends Constructor<MediaDelegate>>(
|
||||
BaseClass: Base,
|
||||
DelegateClass: Delegate
|
||||
) {
|
||||
class DelegateMedia extends (BaseClass as Constructor<any>) {
|
||||
#delegate = new DelegateClass();
|
||||
|
||||
get(prop: string): any {
|
||||
if (prop in this.#delegate) {
|
||||
return (this.#delegate as any)[prop];
|
||||
}
|
||||
return super.get?.(prop);
|
||||
}
|
||||
|
||||
set(prop: string, val: any): void {
|
||||
if (prop in this.#delegate) {
|
||||
(this.#delegate as any)[prop] = val;
|
||||
return;
|
||||
}
|
||||
super.set?.(prop, val);
|
||||
}
|
||||
|
||||
call(prop: string, ...args: any[]): any {
|
||||
if (prop in this.#delegate) {
|
||||
return (this.#delegate as any)[prop](...args);
|
||||
}
|
||||
return super.call?.(prop, ...args);
|
||||
}
|
||||
|
||||
attach(target: EventTarget): void {
|
||||
super.attach?.(target);
|
||||
this.#delegate.attach?.(target);
|
||||
}
|
||||
|
||||
detach(): void {
|
||||
this.#delegate.detach?.();
|
||||
super.detach?.();
|
||||
}
|
||||
}
|
||||
|
||||
return DelegateMedia as unknown as Constructor<
|
||||
InstanceType<Base> & {
|
||||
attach(target: EventTarget): void;
|
||||
detach(): void;
|
||||
}
|
||||
> &
|
||||
Omit<Base, 'prototype'>;
|
||||
}
|
||||
@@ -19,12 +19,13 @@ const API_GET_SET: API_TYPE = 2;
|
||||
*
|
||||
* The `get`, `set`, and `call` methods can be overridden to provide catch-all custom behavior.
|
||||
*/
|
||||
export const MediaApiProxyMixin = <T extends EventTarget>(
|
||||
...MediaApiTargetClasses: AnyConstructor<T extends [unknown, ...unknown[]] ? T : any>[]
|
||||
export const MediaProxyMixin = <T extends EventTarget>(
|
||||
PrimaryClass: AnyConstructor<T>,
|
||||
...AdditionalClasses: AnyConstructor<EventTarget>[]
|
||||
) => {
|
||||
class MediaApiProxy {
|
||||
static extends(...MediaApiTargetClasses: AnyConstructor<T>[]) {
|
||||
const props = getClassProps<T>(...MediaApiTargetClasses);
|
||||
static extends(...MediaApiTargetClasses: AnyConstructor<any>[]) {
|
||||
const props = getClassProps(...MediaApiTargetClasses);
|
||||
|
||||
for (const [prop, type] of props.entries()) {
|
||||
if (prop in MediaApiProxy.prototype) continue;
|
||||
@@ -81,9 +82,9 @@ export const MediaApiProxyMixin = <T extends EventTarget>(
|
||||
}
|
||||
}
|
||||
|
||||
MediaApiProxy.extends(...MediaApiTargetClasses);
|
||||
MediaApiProxy.extends(PrimaryClass, ...AdditionalClasses);
|
||||
|
||||
return MediaApiProxy as unknown as Constructor<T> & typeof MediaApiProxy;
|
||||
return MediaApiProxy as unknown as Constructor<T>;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+40
-60
@@ -1,5 +1,3 @@
|
||||
// TODO: This should be adapted to use the MediaApiMixin as a base class.
|
||||
|
||||
/**
|
||||
* Custom Media Element
|
||||
* Based on https://github.com/muxinc/custom-video-element - Mux - MIT License
|
||||
@@ -187,6 +185,7 @@ export function CustomMediaMixin<T extends Constructor<HTMLElement>>(
|
||||
static shadowRootOptions: ShadowRootInit = { mode: 'open' };
|
||||
static Events = Events;
|
||||
static #isDefined = false;
|
||||
static #propsToAttrs: Set<string>;
|
||||
|
||||
static get observedAttributes() {
|
||||
CustomMedia.#define();
|
||||
@@ -202,9 +201,9 @@ export function CustomMediaMixin<T extends Constructor<HTMLElement>>(
|
||||
if (CustomMedia.#isDefined) return;
|
||||
CustomMedia.#isDefined = true;
|
||||
|
||||
const propsToAttrs = new Set(CustomMedia.observedAttributes);
|
||||
CustomMedia.#propsToAttrs = new Set(CustomMedia.observedAttributes);
|
||||
// defaultMuted maps to the muted attribute, handled manually below.
|
||||
propsToAttrs.delete('muted');
|
||||
CustomMedia.#propsToAttrs.delete('muted');
|
||||
|
||||
// Passthrough native element functions from the custom element to the native element
|
||||
for (const prop of nativeElProps) {
|
||||
@@ -215,53 +214,21 @@ export function CustomMediaMixin<T extends Constructor<HTMLElement>>(
|
||||
// @ts-expect-error
|
||||
CustomMedia.prototype[prop] = function (...args: any[]) {
|
||||
this.#init();
|
||||
|
||||
const fn = () => {
|
||||
if (this.call) return this.call(prop, ...args);
|
||||
const nativeFn = this.nativeEl?.[prop] as ((...args: any[]) => any) | undefined;
|
||||
return nativeFn?.apply(this.nativeEl, args);
|
||||
};
|
||||
|
||||
return fn();
|
||||
return this.call(prop, ...args);
|
||||
};
|
||||
} else {
|
||||
// Getter and setter configuration
|
||||
const config: PropertyDescriptor = {
|
||||
get(this: CustomMedia) {
|
||||
this.#init();
|
||||
|
||||
const attr = prop.toLowerCase();
|
||||
if (propsToAttrs.has(attr)) {
|
||||
const val = this.getAttribute(attr);
|
||||
return val === null ? false : val === '' ? true : val;
|
||||
}
|
||||
return this.get?.(prop) ?? this.nativeEl?.[prop];
|
||||
return this.get(prop);
|
||||
},
|
||||
};
|
||||
|
||||
if (prop !== prop.toUpperCase()) {
|
||||
config.set = function (this: CustomMedia, val: any) {
|
||||
this.#init();
|
||||
|
||||
const attr = prop.toLowerCase();
|
||||
if (propsToAttrs.has(attr)) {
|
||||
if (val === true || val === false || val == null) {
|
||||
this.toggleAttribute(attr, Boolean(val));
|
||||
} else {
|
||||
this.setAttribute(attr, val);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.set) {
|
||||
this.set(prop, val);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.nativeEl) {
|
||||
// @ts-expect-error
|
||||
this.nativeEl[prop] = val;
|
||||
}
|
||||
this.set(prop, val);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -276,9 +243,36 @@ export function CustomMediaMixin<T extends Constructor<HTMLElement>>(
|
||||
#childMap = new Map<MediaChild, MediaChild>();
|
||||
#childObserver?: MutationObserver;
|
||||
|
||||
get: ((prop: string) => any) | undefined;
|
||||
set: ((prop: string, val: any) => void) | undefined;
|
||||
call: ((prop: string, ...args: any[]) => any) | undefined;
|
||||
get(prop: string): any {
|
||||
const attr = prop.toLowerCase();
|
||||
if (CustomMedia.#propsToAttrs.has(attr)) {
|
||||
const val = this.getAttribute(attr);
|
||||
return val === null ? false : val === '' ? true : val;
|
||||
}
|
||||
return this.nativeEl?.[prop as keyof typeof this.nativeEl];
|
||||
}
|
||||
|
||||
set(prop: string, val: any): void {
|
||||
const attr = prop.toLowerCase();
|
||||
if (CustomMedia.#propsToAttrs.has(attr)) {
|
||||
if (val === true || val === false || val == null) {
|
||||
this.toggleAttribute(attr, Boolean(val));
|
||||
} else {
|
||||
this.setAttribute(attr, val);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.nativeEl) {
|
||||
// @ts-expect-error
|
||||
this.nativeEl[prop as keyof typeof this.nativeEl] = val;
|
||||
}
|
||||
}
|
||||
|
||||
call(prop: string, ...args: any[]): any {
|
||||
const nativeFn = this.nativeEl?.[prop as keyof typeof this.nativeEl] as ((...args: any[]) => any) | undefined;
|
||||
return nativeFn?.apply(this.nativeEl, args);
|
||||
}
|
||||
|
||||
// If the custom element is defined before the custom element's HTML is parsed
|
||||
// no attributes will be available in the constructor (construction process).
|
||||
@@ -301,27 +295,13 @@ export function CustomMediaMixin<T extends Constructor<HTMLElement>>(
|
||||
}
|
||||
|
||||
get defaultMuted() {
|
||||
return this.hasAttribute('muted');
|
||||
this.#init();
|
||||
return this.get('muted');
|
||||
}
|
||||
|
||||
set defaultMuted(val) {
|
||||
this.toggleAttribute('muted', val);
|
||||
}
|
||||
|
||||
get src() {
|
||||
return this.getAttribute('src');
|
||||
}
|
||||
|
||||
set src(val) {
|
||||
this.setAttribute('src', `${val}`);
|
||||
}
|
||||
|
||||
get preload() {
|
||||
return this.getAttribute('preload') ?? this.nativeEl?.preload;
|
||||
}
|
||||
|
||||
set preload(val) {
|
||||
this.setAttribute('preload', `${val}`);
|
||||
this.#init();
|
||||
this.set('muted', val);
|
||||
}
|
||||
|
||||
#init(): void {
|
||||
@@ -1,34 +1,37 @@
|
||||
import type { AnyConstructor } from '@videojs/utils/types';
|
||||
import Hls from 'hls.js';
|
||||
|
||||
import type { MediaApiProxyTarget } from '../../core/media/proxy';
|
||||
import { VideoApiProxy } from './proxy';
|
||||
import { type MediaDelegate, MediaDelegateMixin } from '../../core/media/delegate';
|
||||
import { MediaProxyMixin } from '../../core/media/proxy';
|
||||
import { CustomMediaMixin } from './custom-media-element';
|
||||
|
||||
export class HlsMediaDelegate implements MediaDelegate {
|
||||
#engine = new Hls();
|
||||
|
||||
attach(target: EventTarget): void {
|
||||
this.#engine.attachMedia(target as HTMLMediaElement);
|
||||
}
|
||||
|
||||
detach(): void {
|
||||
this.#engine.detachMedia();
|
||||
}
|
||||
|
||||
set src(src: string) {
|
||||
this.#engine.loadSource(src);
|
||||
}
|
||||
|
||||
get src(): string {
|
||||
return this.#engine.url ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
// This is used by the web component because it needs to extend HTMLElement!
|
||||
export const HlsMediaMixin = <T extends AnyConstructor<EventTarget>>(Super: T) => {
|
||||
class HlsMedia extends Super {
|
||||
engine = new Hls();
|
||||
|
||||
attach(target: MediaApiProxyTarget): void {
|
||||
super.attach?.(target);
|
||||
this.engine.attachMedia(target as HTMLMediaElement);
|
||||
}
|
||||
|
||||
detach(): void {
|
||||
super.detach?.();
|
||||
this.engine.detachMedia();
|
||||
}
|
||||
|
||||
set src(value: string) {
|
||||
this.engine.loadSource(value);
|
||||
}
|
||||
|
||||
get src(): string {
|
||||
return this.engine.url ?? '';
|
||||
}
|
||||
}
|
||||
return HlsMedia as T & typeof HlsMedia;
|
||||
};
|
||||
export class HlsCustomMedia extends MediaDelegateMixin(
|
||||
CustomMediaMixin(HTMLElement, { tag: 'video' }),
|
||||
HlsMediaDelegate
|
||||
) {}
|
||||
|
||||
// This is used by the React component.
|
||||
export class HlsMedia extends HlsMediaMixin(VideoApiProxy) {}
|
||||
export class HlsMedia extends MediaDelegateMixin(
|
||||
MediaProxyMixin(HTMLVideoElement, HTMLMediaElement, EventTarget),
|
||||
HlsMediaDelegate
|
||||
) {}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { MediaApiProxyMixin } from '../../core/media/proxy';
|
||||
|
||||
export type { MediaApiProxyTarget } from '../../core/media/proxy';
|
||||
|
||||
export class MediaApiProxy extends MediaApiProxyMixin<HTMLMediaElement>(HTMLMediaElement, EventTarget) {}
|
||||
export class VideoApiProxy extends MediaApiProxyMixin<HTMLVideoElement>(
|
||||
HTMLVideoElement,
|
||||
HTMLMediaElement,
|
||||
EventTarget
|
||||
) {}
|
||||
export class AudioApiProxy extends MediaApiProxyMixin<HTMLAudioElement>(
|
||||
HTMLAudioElement,
|
||||
HTMLMediaElement,
|
||||
EventTarget
|
||||
) {}
|
||||
@@ -2,7 +2,7 @@
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"declarationDir": "../../types/dom",
|
||||
"useDefineForClassFields": false
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ const createConfig = (mode: BuildMode): UserConfig => ({
|
||||
index: './src/core/index.ts',
|
||||
dom: './src/dom/index.ts',
|
||||
'dom/media/hls': './src/dom/media/hls.ts',
|
||||
'dom/media/custom-media-element': './src/dom/media/custom-media-element.ts',
|
||||
},
|
||||
platform: 'neutral',
|
||||
format: 'es',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CustomMediaMixin } from '../custom-media-element';
|
||||
import { CustomMediaMixin } from '@videojs/core/dom/media/custom-media-element';
|
||||
|
||||
function getTemplateHTML(attrs: Record<string, string>) {
|
||||
return /*html*/ `
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { HlsMediaMixin } from '@videojs/core/dom/media/hls';
|
||||
import { CustomMediaMixin } from '../custom-media-element';
|
||||
import { HlsCustomMedia } from '@videojs/core/dom/media/hls';
|
||||
|
||||
export class HlsVideo extends HlsMediaMixin(CustomMediaMixin(HTMLElement, { tag: 'video' })) {
|
||||
export class HlsVideo extends HlsCustomMedia {
|
||||
static getTemplateHTML(attrs: Record<string, string>): string {
|
||||
const { src, ...rest } = attrs;
|
||||
// biome-ignore lint/complexity/noThisInStatic: intentional use of super
|
||||
|
||||
Reference in New Issue
Block a user