Implement event handlers for web

This commit is contained in:
2025-10-03 23:07:28 +02:00
parent a092578cab
commit 937d34d73e
4 changed files with 214 additions and 10 deletions

View File

@@ -12,14 +12,16 @@ import {
import type { VideoPlayerBase } from "./types/VideoPlayerBase"; import type { VideoPlayerBase } from "./types/VideoPlayerBase";
import type { VideoPlayerStatus } from "./types/VideoPlayerStatus"; import type { VideoPlayerStatus } from "./types/VideoPlayerStatus";
import { VideoPlayerEvents } from "./VideoPlayerEvents"; import { VideoPlayerEvents } from "./VideoPlayerEvents";
import { WebEventEmiter } from "./WebEventEmiter";
class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
protected player = new shaka.Player(); protected player = new shaka.Player();
protected video = document.createElement("video"); protected video: HTMLVideoElement;
constructor(source: VideoSource | VideoConfig | VideoPlayerSource) { constructor(source: VideoSource | VideoConfig | VideoPlayerSource) {
// Initialize events const video = document.createElement("video");
super(player.eventEmitter); super(new WebEventEmiter(video));
this.video = video;
this.player.attach(this.video); this.player.attach(this.video);
} }

View File

@@ -13,9 +13,9 @@ export class VideoPlayerEvents {
protected readonly supportedEvents: (keyof PlayerEvents)[] = protected readonly supportedEvents: (keyof PlayerEvents)[] =
ALL_PLAYER_EVENTS; ALL_PLAYER_EVENTS;
constructor(eventEmitter: VideoPlayerEventEmitter) { constructor(eventEmitter: PlayerEvents) {
this.eventEmitter = eventEmitter; this.eventEmitter = eventEmitter;
for (let event of this.supportedEvents) { for (const event of this.supportedEvents) {
// @ts-expect-error we narrow the type of the event // @ts-expect-error we narrow the type of the event
this.eventEmitter[event] = this.triggerEvent.bind(this, event); this.eventEmitter[event] = this.triggerEvent.bind(this, event);
} }
@@ -26,7 +26,7 @@ export class VideoPlayerEvents {
...params: Parameters<PlayerEvents[Event]> ...params: Parameters<PlayerEvents[Event]>
): boolean { ): boolean {
if (!this.eventListeners[event]?.size) return false; if (!this.eventListeners[event]?.size) return false;
for (let fn of this.eventListeners[event]) { for (const fn of this.eventListeners[event]) {
fn(...params); fn(...params);
} }
return true; return true;
@@ -34,7 +34,7 @@ export class VideoPlayerEvents {
addEventListener<Event extends keyof PlayerEvents>( addEventListener<Event extends keyof PlayerEvents>(
event: Event, event: Event,
callback: PlayerEvents[Event] callback: PlayerEvents[Event],
) { ) {
this.eventListeners[event] ??= new Set<PlayerEvents[Event]>(); this.eventListeners[event] ??= new Set<PlayerEvents[Event]>();
this.eventListeners[event].add(callback); this.eventListeners[event].add(callback);
@@ -42,7 +42,7 @@ export class VideoPlayerEvents {
removeEventListener<Event extends keyof PlayerEvents>( removeEventListener<Event extends keyof PlayerEvents>(
event: Event, event: Event,
callback: PlayerEvents[Event] callback: PlayerEvents[Event],
) { ) {
this.eventListeners[event]?.delete(callback); this.eventListeners[event]?.delete(callback);
} }

View File

@@ -0,0 +1,198 @@
import type {
BandwidthData,
onLoadData,
onLoadStartData,
onPlaybackStateChangeData,
onProgressData,
onVolumeChangeData,
AllPlayerEvents as PlayerEvents,
TimedMetadata,
} from "./types/Events";
import type { TextTrack } from "./types/TextTrack";
import type { VideoRuntimeError } from "./types/VideoError";
import type { VideoPlayerStatus } from "./types/VideoPlayerStatus";
export class WebEventEmiter implements PlayerEvents {
private _isBuferring = false;
constructor(private video: HTMLVideoElement) {
// TODO: add `onBandwithUpdate`
// on buffer
this.video.addEventListener("canplay", this._onCanPlay);
this.video.addEventListener("waiting", this._onWaiting);
// on end
this.video.addEventListener("ended", this._onEnded);
// on load
this.video.addEventListener("durationchange", this._onDurationChange);
// on load start
this.video.addEventListener("loadstart", this._onLoadStart);
// on playback state change
this.video.addEventListener("play", this._onPlay);
this.video.addEventListener("pause", this._onPause);
// on playback rate change
this.video.addEventListener("ratechange", this._onRateChange);
// on progress
this.video.addEventListener("timeupdate", this._onTimeUpdate);
// on ready to play
this.video.addEventListener("loadeddata", this._onLoadedData);
// on seek
this.video.addEventListener("seeked", this._onSeeked);
// on volume change
this.video.addEventListener("volumechange", this._onVolumeChange);
// on status change
this.video.addEventListener("error", this._onError);
}
destroy() {
this.video.removeEventListener("canplay", this._onCanPlay);
this.video.removeEventListener("waiting", this._onWaiting);
this.video.removeEventListener("ended", this._onEnded);
this.video.removeEventListener("durationchange", this._onDurationChange);
this.video.removeEventListener("play", this._onPlay);
this.video.removeEventListener("pause", this._onPause);
this.video.removeEventListener("ratechange", this._onRateChange);
this.video.removeEventListener("timeupdate", this._onTimeUpdate);
this.video.removeEventListener("loadeddata", this._onLoadedData);
this.video.removeEventListener("seeked", this._onSeeked);
this.video.removeEventListener("volumechange", this._onVolumeChange);
this.video.removeEventListener("error", this._onError);
}
_onTimeUpdate() {
this.onProgress({
currentTime: this.video.currentTime,
bufferDuration: this.video.buffered.length
? this.video.buffered.end(this.video.buffered.length - 1)
: 0,
});
}
_onCanPlay() {
this._isBuferring = false;
this.onBuffer(false);
this.onStatusChange("readyToPlay");
}
_onWaiting() {
this._isBuferring = true;
this.onBuffer(true);
this.onStatusChange("loading");
}
_onDurationChange() {
this.onLoad({
currentTime: this.video.currentTime,
duration: this.video.duration,
width: this.video.width,
height: this.video.height,
orientation: "unknown",
});
}
_onEnded() {
this.onEnd();
this.onStatusChange("idle");
}
_onLoadStart() {
this.onLoadStart({
sourceType: "network",
source: {
uri: this.video.currentSrc,
config: {
uri: this.video.currentSrc,
externalSubtitles: [],
},
getAssetInformationAsync: async () => {
return {
duration: BigInt(this.video.duration),
height: this.video.height,
width: this.video.width,
orientation: "unknown",
bitrate: NaN,
fileSize: BigInt(NaN),
isHDR: false,
isLive: false,
};
},
},
});
}
_onPlay() {
this.onPlaybackStateChange({
isPlaying: true,
isBuffering: this._isBuferring,
});
}
_onPause() {
this.onPlaybackStateChange({
isPlaying: false,
isBuffering: this._isBuferring,
});
}
_onRateChange() {
this.onPlaybackRateChange(this.video.playbackRate);
}
_onLoadedData() {
this.onReadyToDisplay();
}
_onSeeked() {
this.onSeek(this.video.currentTime);
}
_onVolumeChange() {
this.onVolumeChange({ muted: this.video.muted, volume: this.video.volume });
}
_onError() {
this.onStatusChange("error");
}
NOOP = () => {};
onError: (error: VideoRuntimeError) => void = this.NOOP;
onAudioBecomingNoisy: () => void = this.NOOP;
onAudioFocusChange: (hasAudioFocus: boolean) => void = this.NOOP;
onBandwidthUpdate: (data: BandwidthData) => void = this.NOOP;
onBuffer: (buffering: boolean) => void = this.NOOP;
onControlsVisibleChange: (visible: boolean) => void = this.NOOP;
onEnd: () => void = this.NOOP;
onExternalPlaybackChange: (externalPlaybackActive: boolean) => void =
this.NOOP;
onLoad: (data: onLoadData) => void = this.NOOP;
onLoadStart: (data: onLoadStartData) => void = this.NOOP;
onPlaybackStateChange: (data: onPlaybackStateChangeData) => void = this.NOOP;
onPlaybackRateChange: (rate: number) => void = this.NOOP;
onProgress: (data: onProgressData) => void = this.NOOP;
onReadyToDisplay: () => void = this.NOOP;
onSeek: (seekTime: number) => void = this.NOOP;
onTimedMetadata: (metadata: TimedMetadata) => void = this.NOOP;
onTextTrackDataChanged: (texts: string[]) => void = this.NOOP;
onTrackChange: (track: TextTrack | null) => void = this.NOOP;
onVolumeChange: (data: onVolumeChangeData) => void = this.NOOP;
onStatusChange: (status: VideoPlayerStatus) => void = this.NOOP;
}

View File

@@ -1,7 +1,7 @@
import type { VideoPlayerSource } from '../../spec/nitro/VideoPlayerSource.nitro';
import type { TextTrack } from './TextTrack'; import type { TextTrack } from './TextTrack';
import type { VideoRuntimeError } from './VideoError'; import type { VideoRuntimeError } from './VideoError';
import type { VideoOrientation } from './VideoOrientation'; import type { VideoOrientation } from './VideoOrientation';
import type { VideoPlayerSourceBase } from './VideoPlayerSourceBase';
import type { VideoPlayerStatus } from './VideoPlayerStatus'; import type { VideoPlayerStatus } from './VideoPlayerStatus';
export interface VideoPlayerEvents { export interface VideoPlayerEvents {
@@ -28,6 +28,7 @@ export interface VideoPlayerEvents {
/** /**
* Called when the video view's controls visibility changes. * Called when the video view's controls visibility changes.
* @param visible Whether the video view's controls are visible. * @param visible Whether the video view's controls are visible.
* @platform Android, Ios
*/ */
onControlsVisibleChange: (visible: boolean) => void; onControlsVisibleChange: (visible: boolean) => void;
/** /**
@@ -72,15 +73,18 @@ export interface VideoPlayerEvents {
onSeek: (seekTime: number) => void; onSeek: (seekTime: number) => void;
/** /**
* Called when player receives timed metadata. * Called when player receives timed metadata.
* @platform Android, Ios
*/ */
onTimedMetadata: (metadata: TimedMetadata) => void; onTimedMetadata: (metadata: TimedMetadata) => void;
/** /**
* Called when the text track (currently displayed subtitle) data changes. * Called when the text track (currently displayed subtitle) data changes.
* @platform Android, Ios
*/ */
onTextTrackDataChanged: (texts: string[]) => void; onTextTrackDataChanged: (texts: string[]) => void;
/** /**
* Called when the selected text track changes. * Called when the selected text track changes.
* @param track - The newly selected text track, or null if no track is selected * @param track - The newly selected text track, or null if no track is selected
* @platform Android, Ios
*/ */
onTrackChange: (track: TextTrack | null) => void; onTrackChange: (track: TextTrack | null) => void;
/** /**
@@ -178,7 +182,7 @@ export interface onLoadStartData {
/** /**
* The source of the video. * The source of the video.
*/ */
source: VideoPlayerSource; source: VideoPlayerSourceBase;
} }
export interface onPlaybackStateChangeData { export interface onPlaybackStateChangeData {