mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(monorepo): migrate prototype code to organized package structure
Migrate all prototype code from vjs10-prototype repository into the monorepo structure following established dependency hierarchy and package organization patterns. Core packages migrated: - @vjs-10/media-store: Complete media store with nanostores integration - @vjs-10/playback-engine: HLS.js playback engine implementation - @vjs-10/media: Core media state owner and interfaces HTML packages migrated: - @vjs-10/html-icons: Web component icons with proper base classes - @vjs-10/html-media-elements: Context provider integration - @vjs-10/html: Main HTML UI components, skins, and utilities React packages migrated: - @vjs-10/react-icons: React icon components - @vjs-10/react-media-elements: Video component with state integration - @vjs-10/react-media-store: MediaProvider and React hooks - @vjs-10/react: Main React UI components and skins Changes made: - Updated all imports to use monorepo package names (@vjs-10/*) - Distributed dependencies to specific packages that use them - Preserved all prototype functionality and component structure - Created proper index files for all packages - Fixed workspace protocol usage for npm compatibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
fc5a0e46fd
commit
4b472ec49c
@@ -20,6 +20,9 @@
|
||||
"player"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"nanostores": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { map, subscribeKeys } from 'nanostores';
|
||||
|
||||
export type StateOwners = {
|
||||
media?: any;
|
||||
};
|
||||
|
||||
export type EventOrAction<D = undefined> = {
|
||||
type: string;
|
||||
detail?: D;
|
||||
target?: EventTarget;
|
||||
};
|
||||
|
||||
export type FacadeGetter<T, D = T> = (
|
||||
stateOwners: StateOwners,
|
||||
event?: EventOrAction<D>,
|
||||
) => T;
|
||||
|
||||
export type FacadeSetter<T> = (value: T, stateOwners: StateOwners) => void;
|
||||
|
||||
export type StateOwnerUpdateHandler<T> = (
|
||||
handler: (value: T) => void,
|
||||
stateOwners: StateOwners,
|
||||
) => void;
|
||||
|
||||
export type ReadonlyFacadeProp<T, D = T> = {
|
||||
get: FacadeGetter<T, D>;
|
||||
mediaEvents?: string[];
|
||||
};
|
||||
|
||||
export type FacadeProp<T, S = T, D = T> = ReadonlyFacadeProp<T, D> & {
|
||||
set: FacadeSetter<S>;
|
||||
/** @TODO We probably need to refactor this for more complex cases where we can't simply translate to a setter */
|
||||
actions: { [k: string]: (val: CustomEvent<any>) => ReturnType<FacadeGetter<T, D>> };
|
||||
};
|
||||
|
||||
export type StateMediator = {
|
||||
mediaPaused: FacadeProp<HTMLMediaElement['paused']>;
|
||||
mediaMuted: FacadeProp<HTMLMediaElement['muted']>;
|
||||
mediaVolume: FacadeProp<HTMLMediaElement['volume']>;
|
||||
mediaVolumeLevel: ReadonlyFacadeProp<'high' | 'medium' | 'low' | 'off'>;
|
||||
};
|
||||
|
||||
export function createMediaStore({
|
||||
// media,
|
||||
stateMediator,
|
||||
}: {
|
||||
media?: any;
|
||||
stateMediator: Partial<StateMediator> & Pick<StateMediator, 'mediaPaused'>;
|
||||
}) {
|
||||
const stateOwners: any = {};
|
||||
const store = map<any>({});
|
||||
const stateUpdateHandlers: Record<string, () => void> = {};
|
||||
const keys = Object.keys(stateMediator);
|
||||
|
||||
function updateStateOwners(nextStateOwners: any) {
|
||||
if (nextStateOwners.media === stateOwners.media) {
|
||||
return;
|
||||
}
|
||||
|
||||
let media = stateOwners.media;
|
||||
if (media) {
|
||||
for (const { mediaEvents = [] } of Object.values(stateMediator)) {
|
||||
for (const mediaEvent of mediaEvents) {
|
||||
media.removeEventListener(
|
||||
mediaEvent,
|
||||
stateUpdateHandlers[mediaEvent],
|
||||
);
|
||||
delete stateUpdateHandlers[mediaEvent];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(stateOwners, nextStateOwners);
|
||||
|
||||
media = stateOwners.media;
|
||||
store.set(getInitialState(stateMediator, stateOwners));
|
||||
|
||||
if (media) {
|
||||
for (const [stateName, stateObject] of Object.entries(stateMediator)) {
|
||||
const { get, mediaEvents = [] } = stateObject;
|
||||
for (const mediaEvent of mediaEvents) {
|
||||
stateUpdateHandlers[mediaEvent] = () =>
|
||||
store.setKey(stateName, get(stateOwners));
|
||||
media.addEventListener(mediaEvent, stateUpdateHandlers[mediaEvent]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
dispatch(action: any) {
|
||||
const { type, detail } = action;
|
||||
|
||||
if (type === 'mediaelementchangerequest') {
|
||||
updateStateOwners({ media: detail });
|
||||
} else {
|
||||
for (const stateObject of Object.values(stateMediator).filter(
|
||||
(stateMediator) => 'set' in stateMediator,
|
||||
)) {
|
||||
const { set, actions } = stateObject;
|
||||
if (type in actions) {
|
||||
/** @TODO FIXME ASAP!!! (CJP) */
|
||||
// @ts-ignore
|
||||
set(actions[type as keyof typeof actions](), stateOwners);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getState() {
|
||||
return store.get();
|
||||
},
|
||||
|
||||
subscribeKeys(keys: string[], callback: (state: any) => void) {
|
||||
subscribeKeys(store, keys, callback);
|
||||
},
|
||||
|
||||
// NOTE: In the POC architecture using nano-stores, subscribe is simply subscribeKeys across all keys. (CJP)
|
||||
subscribe(callback: (state: any) => void) {
|
||||
subscribeKeys(store, keys, callback);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getInitialState(
|
||||
stateMediator: Partial<StateMediator> & Pick<StateMediator, 'mediaPaused'>,
|
||||
stateOwners: any,
|
||||
) {
|
||||
let initialState: any = {};
|
||||
for (const [stateName, { get }] of Object.entries(stateMediator)) {
|
||||
initialState[stateName] = get(stateOwners);
|
||||
}
|
||||
return initialState;
|
||||
}
|
||||
@@ -1,46 +1,4 @@
|
||||
export interface MediaState {
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
paused: boolean;
|
||||
volume: number;
|
||||
muted: boolean;
|
||||
}
|
||||
|
||||
export interface MediaStateOwner {
|
||||
getState(): MediaState;
|
||||
setState(state: Partial<MediaState>): void;
|
||||
}
|
||||
|
||||
export class MediaStore {
|
||||
private owners: Set<MediaStateOwner> = new Set();
|
||||
private state: MediaState = {
|
||||
currentTime: 0,
|
||||
duration: 0,
|
||||
paused: true,
|
||||
volume: 1,
|
||||
muted: false,
|
||||
};
|
||||
|
||||
addOwner(owner: MediaStateOwner): void {
|
||||
this.owners.add(owner);
|
||||
}
|
||||
|
||||
removeOwner(owner: MediaStateOwner): void {
|
||||
this.owners.delete(owner);
|
||||
}
|
||||
|
||||
getState(): MediaState {
|
||||
return { ...this.state };
|
||||
}
|
||||
|
||||
updateState(updates: Partial<MediaState>): void {
|
||||
this.state = { ...this.state, ...updates };
|
||||
this.notifyOwners();
|
||||
}
|
||||
|
||||
private notifyOwners(): void {
|
||||
this.owners.forEach(owner => {
|
||||
owner.setState(this.state);
|
||||
});
|
||||
}
|
||||
}
|
||||
export * from './factory';
|
||||
export * from './media-store';
|
||||
export * from './state-mediators/playable';
|
||||
export * from './state-mediators/audible';
|
||||
@@ -0,0 +1,9 @@
|
||||
import { playable } from './state-mediators/playable';
|
||||
import { audible } from './state-mediators/audible';
|
||||
import { createMediaStore as factory } from './factory';
|
||||
// Example of default media store with default state mediator definitions. (CJP)
|
||||
// NOTE: We can also change the API to take an array of stateMediators (or either/both) (CJP)
|
||||
const stateMediator = { ...playable, ...audible };
|
||||
type Params = Partial<Parameters<typeof factory>[0]>;
|
||||
export const createMediaStore = (params: Params = {}) =>
|
||||
factory({ stateMediator, ...params });
|
||||
@@ -0,0 +1,48 @@
|
||||
export const audible = {
|
||||
mediaMuted: {
|
||||
get(stateOwners: any) {
|
||||
const { media } = stateOwners;
|
||||
return media?.muted ?? false;
|
||||
},
|
||||
set(value: boolean, stateOwners: any) {
|
||||
const { media } = stateOwners;
|
||||
if (!media) return;
|
||||
media.muted = value;
|
||||
},
|
||||
mediaEvents: ['volumechange'],
|
||||
actions: {
|
||||
/** @TODO Refactor me to play more nicely with side effects that don't/can't correlate with set() API or aren't simple 1:1 with getter vs. setter (CJP) */
|
||||
mediamuterequest: () => true,
|
||||
mediaunmuterequest: () => false,
|
||||
},
|
||||
},
|
||||
mediaVolume: {
|
||||
get(stateOwners: any) {
|
||||
const { media } = stateOwners;
|
||||
return media?.volume ?? 1.0;
|
||||
},
|
||||
set(value: number, stateOwners: any) {
|
||||
const { media } = stateOwners;
|
||||
if (!media) return;
|
||||
if (!Number.isFinite(+value)) return;
|
||||
media.volume = +value;
|
||||
},
|
||||
mediaEvents: ['volumechange'],
|
||||
actions: {
|
||||
/** @TODO Refactor me to play more nicely with side effects that don't/can't correlate with set() API (CJP) */
|
||||
mediavolumerequest: ({ detail }: Pick<CustomEvent<any>, 'detail'> = { detail: 0 }) => +detail,
|
||||
},
|
||||
},
|
||||
// NOTE: This could be (re)implemented as "derived state" in some manner (e.g. selectors but also other patterns/conventions) if preferred. (CJP)
|
||||
mediaVolumeLevel: {
|
||||
get(stateOwners: any) {
|
||||
const { media } = stateOwners;
|
||||
if (typeof media?.volume == 'undefined') return 'high';
|
||||
if (media.muted || media.volume === 0) return 'off';
|
||||
if (media.volume < 0.5) return 'low';
|
||||
if (media.volume < 0.75) return 'medium';
|
||||
return 'high';
|
||||
},
|
||||
mediaEvents: ['volumechange'],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
export const playable = {
|
||||
mediaPaused: {
|
||||
get(stateOwners: any) {
|
||||
const { media } = stateOwners;
|
||||
return media?.paused ?? true;
|
||||
},
|
||||
set(value: boolean, stateOwners: any) {
|
||||
const { media } = stateOwners;
|
||||
media?.[value ? 'pause' : 'play']();
|
||||
},
|
||||
mediaEvents: ['play', 'playing', 'pause', 'emptied'],
|
||||
actions: {
|
||||
/** @TODO Refactor me to play more nicely with side effects that don't/can't correlate with set() API (CJP) */
|
||||
mediaplayrequest: () => false,
|
||||
mediapauserequest: () => true,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,69 +1 @@
|
||||
export type MediaReadyState = 0 | 1 | 2 | 3 | 4;
|
||||
|
||||
export const READY_STATE = {
|
||||
HAVE_NOTHING: 0 as MediaReadyState,
|
||||
HAVE_METADATA: 1 as MediaReadyState,
|
||||
HAVE_CURRENT_DATA: 2 as MediaReadyState,
|
||||
HAVE_FUTURE_DATA: 3 as MediaReadyState,
|
||||
HAVE_ENOUGH_DATA: 4 as MediaReadyState,
|
||||
} as const;
|
||||
|
||||
export type MediaNetworkState = 0 | 1 | 2 | 3;
|
||||
|
||||
export const NETWORK_STATE = {
|
||||
EMPTY: 0 as MediaNetworkState,
|
||||
IDLE: 1 as MediaNetworkState,
|
||||
LOADING: 2 as MediaNetworkState,
|
||||
NO_SOURCE: 3 as MediaNetworkState,
|
||||
} as const;
|
||||
|
||||
export interface MediaElementLike {
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
paused: boolean;
|
||||
ended: boolean;
|
||||
volume: number;
|
||||
muted: boolean;
|
||||
playbackRate: number;
|
||||
readyState: MediaReadyState;
|
||||
networkState: MediaNetworkState;
|
||||
|
||||
play(): Promise<void>;
|
||||
pause(): void;
|
||||
load(): void;
|
||||
}
|
||||
|
||||
export function isMediaElement(element: any): element is HTMLMediaElement {
|
||||
return element &&
|
||||
typeof element.play === 'function' &&
|
||||
typeof element.pause === 'function' &&
|
||||
typeof element.load === 'function' &&
|
||||
typeof element.currentTime === 'number';
|
||||
}
|
||||
|
||||
export function createMediaElementAdapter(element: HTMLMediaElement): MediaElementLike {
|
||||
return {
|
||||
get currentTime() { return element.currentTime; },
|
||||
set currentTime(value: number) { element.currentTime = value; },
|
||||
|
||||
get duration() { return element.duration; },
|
||||
get paused() { return element.paused; },
|
||||
get ended() { return element.ended; },
|
||||
|
||||
get volume() { return element.volume; },
|
||||
set volume(value: number) { element.volume = value; },
|
||||
|
||||
get muted() { return element.muted; },
|
||||
set muted(value: boolean) { element.muted = value; },
|
||||
|
||||
get playbackRate() { return element.playbackRate; },
|
||||
set playbackRate(value: number) { element.playbackRate = value; },
|
||||
|
||||
get readyState() { return element.readyState as MediaReadyState; },
|
||||
get networkState() { return element.networkState as MediaNetworkState; },
|
||||
|
||||
play: () => element.play(),
|
||||
pause: () => element.pause(),
|
||||
load: () => element.load(),
|
||||
};
|
||||
}
|
||||
export * from './playable';
|
||||
@@ -0,0 +1,112 @@
|
||||
import type { IBasePlaybackEngine } from '@vjs-10/playback-engine';
|
||||
import { createPlaybackEngine } from '@vjs-10/playback-engine';
|
||||
/** @TODO Split out "playable" vs. "audible" and compose via factory (current mixin pattern or spreadable mixin pattern) (CJP) */
|
||||
export const Events = ['volumechange', 'pause', 'play', 'playing'] as const;
|
||||
|
||||
export interface IBaseMediaStateOwner<
|
||||
T extends Pick<HTMLMediaElement, 'src'> = Pick<HTMLMediaElement, 'src'>,
|
||||
> extends EventTarget,
|
||||
Pick<HTMLMediaElement, 'src'> {
|
||||
mediaElement?: T;
|
||||
}
|
||||
|
||||
export interface IPlayableMediaStateOwner
|
||||
extends EventTarget,
|
||||
IBaseMediaStateOwner,
|
||||
Pick<HTMLMediaElement, 'play' | 'pause' | 'paused'> {}
|
||||
|
||||
export interface IAudibleMediaStateOwner
|
||||
extends EventTarget,
|
||||
IBaseMediaStateOwner,
|
||||
Pick<HTMLMediaElement, 'muted' | 'volume'> {}
|
||||
|
||||
export class PlayableMediaStateOwner
|
||||
extends EventTarget
|
||||
implements IPlayableMediaStateOwner, IAudibleMediaStateOwner
|
||||
{
|
||||
protected _playbackEngine: IBasePlaybackEngine;
|
||||
constructor() {
|
||||
super();
|
||||
this._playbackEngine = createPlaybackEngine();
|
||||
}
|
||||
|
||||
get mediaElement() {
|
||||
return this._playbackEngine.mediaElement;
|
||||
}
|
||||
|
||||
set mediaElement(value) {
|
||||
if (this.mediaElement === value) return;
|
||||
if (this.mediaElement != null) {
|
||||
Events.forEach((eventType) => {
|
||||
this.mediaElement?.removeEventListener(eventType, this);
|
||||
});
|
||||
}
|
||||
this._playbackEngine.mediaElement = value;
|
||||
if (this.mediaElement) {
|
||||
Events.forEach((eventType) => {
|
||||
this.mediaElement?.addEventListener(eventType, this);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get paused() {
|
||||
return this.mediaElement?.paused ?? false;
|
||||
}
|
||||
|
||||
pause() {
|
||||
/** @TODO implement deferred state etc. for cases where media has yet to be set */
|
||||
if (!this.mediaElement) return;
|
||||
return this.mediaElement.pause();
|
||||
}
|
||||
|
||||
play() {
|
||||
/** @TODO implement deferred state etc. for cases where media has yet to be set */
|
||||
if (!this.mediaElement) return Promise.reject();
|
||||
return this.mediaElement.play();
|
||||
}
|
||||
|
||||
get muted() {
|
||||
return this.mediaElement?.muted ?? false;
|
||||
}
|
||||
|
||||
set muted(value) {
|
||||
if (value === this.muted) return;
|
||||
/** @TODO implement deferred state etc. for cases where media has yet to be set */
|
||||
if (!this.mediaElement) return;
|
||||
this.mediaElement.muted = value;
|
||||
}
|
||||
|
||||
get volume() {
|
||||
return this.mediaElement?.volume ?? 0;
|
||||
}
|
||||
|
||||
set volume(value) {
|
||||
if (value === this.volume) return;
|
||||
/** @TODO implement deferred state etc. for cases where media has yet to be set */
|
||||
if (!this.mediaElement) return;
|
||||
this.mediaElement.volume = value;
|
||||
}
|
||||
|
||||
get src() {
|
||||
return this._playbackEngine.src ?? '';
|
||||
}
|
||||
|
||||
set src(value) {
|
||||
this._playbackEngine.src = value;
|
||||
}
|
||||
|
||||
handleEvent(event: Event): void {
|
||||
if (event.target === this.mediaElement) {
|
||||
// @ts-ignore
|
||||
const clonedEvent = new event.constructor(event.type, event);
|
||||
this.dispatchEvent(clonedEvent);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.mediaElement = undefined;
|
||||
this._playbackEngine.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export const createMediaStateOwner = () => new PlayableMediaStateOwner();
|
||||
@@ -21,6 +21,9 @@
|
||||
"streaming"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"hls.js": "^1.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import Hls from 'hls.js';
|
||||
|
||||
/** @TODO Move interface/type defs (CJP) */
|
||||
/** @TODO Narrow and/or parameterize EventTarget via generics/inference (CJP) */
|
||||
export interface IBasePlaybackEngine extends EventTarget {
|
||||
src: HTMLMediaElement['src'] | undefined;
|
||||
// NOTE: Unlike other APIs, this *must* be an HTMLMediaElement based on internal and/or external API assumptions (CJP)
|
||||
mediaElement: HTMLMediaElement | undefined;
|
||||
/** @TODO Widen/extend and/or parameterize EventTarget via generics/inference (CJP) */
|
||||
element: HTMLElement | undefined;
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
export class HlsJSPlaybackEngine extends EventTarget {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
protected _hlsInstance: Hls | undefined;
|
||||
|
||||
get src() {
|
||||
return this._hlsInstance?.url ?? undefined;
|
||||
}
|
||||
|
||||
set src(val) {
|
||||
if (this.src === val) return;
|
||||
if (!this._hlsInstance) {
|
||||
this._createHlsInstance();
|
||||
}
|
||||
if (!this.src && val) {
|
||||
this._hlsInstance?.loadSource(val);
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Some playback engines may not rely on an underlying HTMLMediaElement but still have
|
||||
// an HTMLElement (e.g. HTMLCanvasElement, HTMLImageElement, etc.). This API provides a more
|
||||
// generic interface for cases where we want more minimal assumptions (e.g. fullscreen behaviors for most playtforms but not iOS)
|
||||
get element(): HTMLElement | undefined {
|
||||
return this.mediaElement;
|
||||
}
|
||||
|
||||
set element(val) {
|
||||
if (this.element === val) return;
|
||||
/** @TODO programmatic type checking? (CJP) */
|
||||
this.mediaElement = val as HTMLMediaElement;
|
||||
}
|
||||
|
||||
get mediaElement() {
|
||||
return this._hlsInstance?.media ?? undefined;
|
||||
}
|
||||
|
||||
set mediaElement(val) {
|
||||
if (this.mediaElement === val) return;
|
||||
if (!this._hlsInstance) {
|
||||
this._createHlsInstance();
|
||||
}
|
||||
if (!this.mediaElement && val) {
|
||||
this._hlsInstance?.attachMedia(val);
|
||||
}
|
||||
}
|
||||
|
||||
protected _createHlsInstance() {
|
||||
this._hlsInstance = new Hls();
|
||||
}
|
||||
|
||||
protected _destroyHlsInstance() {
|
||||
this._hlsInstance?.destroy();
|
||||
this._hlsInstance = undefined;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._destroyHlsInstance();
|
||||
}
|
||||
}
|
||||
|
||||
export const createPlaybackEngine = () => new HlsJSPlaybackEngine();
|
||||
@@ -1,83 +1 @@
|
||||
export interface MediaSource {
|
||||
src: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface PlaybackEngineOptions {
|
||||
autoplay?: boolean;
|
||||
preload?: 'none' | 'metadata' | 'auto';
|
||||
}
|
||||
|
||||
export abstract class PlaybackEngine {
|
||||
protected element: HTMLMediaElement | null = null;
|
||||
protected options: PlaybackEngineOptions;
|
||||
|
||||
constructor(options: PlaybackEngineOptions = {}) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
abstract attach(element: HTMLMediaElement): void;
|
||||
abstract detach(): void;
|
||||
abstract load(source: MediaSource): Promise<void>;
|
||||
abstract play(): Promise<void>;
|
||||
abstract pause(): void;
|
||||
abstract seekTo(time: number): void;
|
||||
|
||||
getElement(): HTMLMediaElement | null {
|
||||
return this.element;
|
||||
}
|
||||
}
|
||||
|
||||
export class NativePlaybackEngine extends PlaybackEngine {
|
||||
attach(element: HTMLMediaElement): void {
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
detach(): void {
|
||||
this.element = null;
|
||||
}
|
||||
|
||||
async load(source: MediaSource): Promise<void> {
|
||||
if (!this.element) {
|
||||
throw new Error('No media element attached');
|
||||
}
|
||||
|
||||
this.element.src = source.src;
|
||||
await new Promise((resolve, reject) => {
|
||||
const onLoad = () => {
|
||||
this.element!.removeEventListener('loadedmetadata', onLoad);
|
||||
this.element!.removeEventListener('error', onError);
|
||||
resolve(void 0);
|
||||
};
|
||||
const onError = () => {
|
||||
this.element!.removeEventListener('loadedmetadata', onLoad);
|
||||
this.element!.removeEventListener('error', onError);
|
||||
reject(new Error('Failed to load media'));
|
||||
};
|
||||
|
||||
this.element.addEventListener('loadedmetadata', onLoad);
|
||||
this.element.addEventListener('error', onError);
|
||||
});
|
||||
}
|
||||
|
||||
async play(): Promise<void> {
|
||||
if (!this.element) {
|
||||
throw new Error('No media element attached');
|
||||
}
|
||||
await this.element.play();
|
||||
}
|
||||
|
||||
pause(): void {
|
||||
if (!this.element) {
|
||||
throw new Error('No media element attached');
|
||||
}
|
||||
this.element.pause();
|
||||
}
|
||||
|
||||
seekTo(time: number): void {
|
||||
if (!this.element) {
|
||||
throw new Error('No media element attached');
|
||||
}
|
||||
this.element.currentTime = time;
|
||||
}
|
||||
}
|
||||
export * from './HlsJSPlaybackEngine';
|
||||
Reference in New Issue
Block a user