mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat: add media API + HLS video components (#507)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
export * from './media/proxy';
|
||||
export * from './media/state';
|
||||
export * from './ui/buffering-indicator/buffering-indicator-core';
|
||||
export * from './ui/buffering-indicator/buffering-indicator-data-attrs';
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import type { AnyConstructor, Constructor } from '@videojs/utils/types';
|
||||
|
||||
export interface MediaApiProxyTarget extends EventTarget {}
|
||||
|
||||
type API_TYPE = 0 | 1 | 2;
|
||||
|
||||
const API_METHOD: API_TYPE = 0;
|
||||
const API_GET: API_TYPE = 1;
|
||||
const API_GET_SET: API_TYPE = 2;
|
||||
|
||||
/**
|
||||
* This mixin creates an API from the passed classes and proxies the methods and properties to the attached target.
|
||||
*
|
||||
* Many methods and properties will need no translation and are proxied directly to the attached target.
|
||||
* For example, the `play` and `pause` methods are proxied directly to the attached target.
|
||||
*
|
||||
* Child classes can override the proxied methods and properties to provide custom behavior.
|
||||
* For example, the `src` property for HLS media is proxied to the HLS engine, not the target itself.
|
||||
*
|
||||
* 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>[]
|
||||
) => {
|
||||
class MediaApiProxy {
|
||||
static extends(...MediaApiTargetClasses: AnyConstructor<T>[]) {
|
||||
const props = getClassProps<T>(...MediaApiTargetClasses);
|
||||
|
||||
for (const [prop, type] of props.entries()) {
|
||||
if (prop in MediaApiProxy.prototype) continue;
|
||||
|
||||
const config: PropertyDescriptor = {};
|
||||
if (type === API_METHOD) {
|
||||
config.value = function (this: MediaApiProxy, ...args: any[]) {
|
||||
return this.call(prop as keyof MediaApiProxyTarget, ...args);
|
||||
};
|
||||
} else if (type === API_GET || type === API_GET_SET) {
|
||||
config.get = function (this: MediaApiProxy) {
|
||||
return this.get(prop as keyof MediaApiProxyTarget);
|
||||
};
|
||||
if (type === API_GET_SET) {
|
||||
config.set = function (this: MediaApiProxy, val: any) {
|
||||
this.set(prop as keyof MediaApiProxyTarget, val);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(MediaApiProxy.prototype, prop, config);
|
||||
}
|
||||
}
|
||||
|
||||
#target: MediaApiProxyTarget | null = null;
|
||||
|
||||
get target() {
|
||||
return this.#target;
|
||||
}
|
||||
|
||||
get(prop: keyof MediaApiProxyTarget): any {
|
||||
return this.target?.[prop];
|
||||
}
|
||||
|
||||
set(prop: keyof MediaApiProxyTarget, val: any): void {
|
||||
if (this.target) {
|
||||
this.target[prop] = val;
|
||||
}
|
||||
}
|
||||
|
||||
call(prop: keyof MediaApiProxyTarget, ...args: any[]): any {
|
||||
const nativeFn = this.target?.[prop] as ((...args: any[]) => any) | undefined;
|
||||
return nativeFn?.apply(this.target, args);
|
||||
}
|
||||
|
||||
attach(target: MediaApiProxyTarget): void {
|
||||
if (!target || this.#target === target) return;
|
||||
this.#target = target;
|
||||
}
|
||||
|
||||
detach(): void {
|
||||
if (!this.#target) return;
|
||||
this.#target = null;
|
||||
}
|
||||
}
|
||||
|
||||
MediaApiProxy.extends(...MediaApiTargetClasses);
|
||||
|
||||
return MediaApiProxy as unknown as Constructor<T> & typeof MediaApiProxy;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to get the methods, getters, and setters from a class prototype.
|
||||
*/
|
||||
export function getClassProps<T extends EventTarget>(...Classes: AnyConstructor<T>[]) {
|
||||
const props = new Map<keyof T, API_TYPE>();
|
||||
for (const Class of Classes) {
|
||||
const names = Object.getOwnPropertyNames(Class.prototype) as (keyof T)[];
|
||||
for (const name of names) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(Class.prototype, name);
|
||||
if (typeof descriptor?.value === 'function') {
|
||||
props.set(name, API_METHOD);
|
||||
} else if (typeof descriptor?.get === 'function') {
|
||||
if (typeof descriptor?.set === 'function') {
|
||||
props.set(name, API_GET_SET);
|
||||
} else {
|
||||
props.set(name, API_GET);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return props;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './feature';
|
||||
export * from './media/proxy';
|
||||
export * from './media/types';
|
||||
export * from './store/features';
|
||||
export * from './store/selectors';
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { MediaApiProxyTarget } from '@videojs/core';
|
||||
import type { AnyConstructor } from '@videojs/utils/types';
|
||||
import Hls from 'hls.js';
|
||||
import { VideoApiProxy } from './proxy';
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
// This is used by the React component.
|
||||
export class HlsMedia extends HlsMediaMixin(VideoApiProxy) {}
|
||||
@@ -0,0 +1,15 @@
|
||||
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
|
||||
) {}
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { AnySlice, Slice, Store, UnionSliceState } from '@videojs/store';
|
||||
|
||||
import type {
|
||||
MediaBufferState,
|
||||
MediaControlsState,
|
||||
@@ -11,7 +10,16 @@ import type {
|
||||
MediaVolumeState,
|
||||
} from '../../core/media/state';
|
||||
|
||||
export interface Media extends HTMLMediaElement {}
|
||||
type WithOptional<Required, Full> = Required & Partial<Omit<Full, keyof Required>>;
|
||||
|
||||
export type MediaBaseApi = {
|
||||
play: () => Promise<void>;
|
||||
paused: boolean;
|
||||
};
|
||||
|
||||
export type MediaApi = WithOptional<MediaBaseApi, HTMLVideoElement>;
|
||||
|
||||
export type Media = MediaApi | HTMLMediaElement | HTMLAudioElement | HTMLVideoElement | null;
|
||||
|
||||
export interface MediaContainer extends HTMLElement {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user