mirror of
https://github.com/zoriya/react-native-video.git
synced 2026-05-28 08:58:50 +00:00
refactor(web): extract WebTrackHandler and shared video.js types
This commit is contained in:
@@ -17,47 +17,13 @@ import { MediaSessionHandler } from "./web/MediaSession";
|
||||
import { WebEventEmitter } from "./web/WebEventEmitter";
|
||||
import type { VideoTrack } from "./types/VideoTrack";
|
||||
import type { QualityLevel } from "./types/QualityLevel";
|
||||
|
||||
type VideoJsPlayer = ReturnType<typeof videojs>;
|
||||
|
||||
// declared https://github.com/videojs/video.js/blob/main/src/js/tracks/track-list.js#L58
|
||||
export type VideoJsTextTracks = {
|
||||
length: number;
|
||||
[i: number]: {
|
||||
// declared: https://github.com/videojs/video.js/blob/main/src/js/tracks/track.js
|
||||
id: string;
|
||||
label: string;
|
||||
language: string;
|
||||
// declared https://github.com/videojs/video.js/blob/20f8d76cd24325a97ccedf0b013cd1a90ad0bcd7/src/js/tracks/text-track.js
|
||||
default: boolean;
|
||||
mode: "showing" | "disabled" | "hidden";
|
||||
};
|
||||
};
|
||||
|
||||
export type VideoJsTracks = {
|
||||
length: number;
|
||||
[i: number]: {
|
||||
id: string;
|
||||
label: string;
|
||||
language: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
// declared https://github.com/videojs/videojs-contrib-quality-levels/blob/main/src/quality-level.js#L32
|
||||
export type VideoJsQualityArray = {
|
||||
length: number;
|
||||
selectedIndex: number;
|
||||
[i: number]: {
|
||||
id: string;
|
||||
label: string;
|
||||
width: number;
|
||||
height: number;
|
||||
bitrate: number;
|
||||
frameRate: number;
|
||||
enabled: boolean;
|
||||
};
|
||||
};
|
||||
import {
|
||||
mapVideoJsTracks,
|
||||
type VideoJsPlayer,
|
||||
type VideoJsTextTracks,
|
||||
type VideoJsTracks,
|
||||
type VideoJsQualityArray,
|
||||
} from "./web/WebVideoJsTypes";
|
||||
|
||||
class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
||||
protected video: HTMLVideoElement;
|
||||
@@ -318,11 +284,11 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
||||
// @ts-expect-error they define length & index properties via prototype
|
||||
const tracks: VideoJsTextTracks = this.player.textTracks();
|
||||
|
||||
return [...Array(tracks.length)].map((_, i) => ({
|
||||
id: tracks[i]!.id,
|
||||
label: tracks[i]!.label,
|
||||
language: tracks[i]!.language,
|
||||
selected: tracks[i]!.mode === "showing",
|
||||
return mapVideoJsTracks(tracks, (track) => ({
|
||||
id: track.id,
|
||||
label: track.label,
|
||||
language: track.language,
|
||||
selected: track.mode === "showing",
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -347,11 +313,11 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
||||
// @ts-expect-error they define length & index properties via prototype
|
||||
const tracks: VideoJsTracks = this.player.audioTracks();
|
||||
|
||||
return [...Array(tracks.length)].map((_, i) => ({
|
||||
id: tracks[i]!.id,
|
||||
label: tracks[i]!.label,
|
||||
language: tracks[i]!.language,
|
||||
selected: tracks[i]!.enabled,
|
||||
return mapVideoJsTracks(tracks, (track) => ({
|
||||
id: track.id,
|
||||
label: track.label,
|
||||
language: track.language,
|
||||
selected: track.enabled,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -374,11 +340,11 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
||||
// @ts-expect-error they define length & index properties via prototype
|
||||
const tracks: VideoJsTracks = this.player.videoTracks();
|
||||
|
||||
return [...Array(tracks.length)].map((_, i) => ({
|
||||
id: tracks[i]!.id,
|
||||
label: tracks[i]!.label,
|
||||
language: tracks[i]!.language,
|
||||
selected: tracks[i]!.enabled,
|
||||
return mapVideoJsTracks(tracks, (track) => ({
|
||||
id: track.id,
|
||||
label: track.label,
|
||||
language: track.language,
|
||||
selected: track.enabled,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -400,11 +366,11 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
||||
getAvailableQualities(): QualityLevel[] {
|
||||
// @ts-expect-error this isn't typed
|
||||
const levels: VideoJsQualityArray = this.player.qualityLevels();
|
||||
return [...Array(levels.length)].map((_, i) => ({
|
||||
id: levels[i]!.id,
|
||||
width: levels[i]!.width,
|
||||
height: levels[i]!.height,
|
||||
bitrate: levels[i]!.bitrate,
|
||||
return mapVideoJsTracks(levels, (level, i) => ({
|
||||
id: level.id,
|
||||
width: level.width,
|
||||
height: level.height,
|
||||
bitrate: level.bitrate,
|
||||
selected: levels.selectedIndex === i,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type videojs from "video.js";
|
||||
import type { CustomVideoMetadata } from "../types/VideoConfig";
|
||||
|
||||
type VideoJsPlayer = ReturnType<typeof videojs>;
|
||||
import type { VideoJsPlayer } from "./WebVideoJsTypes";
|
||||
|
||||
function getMediaSession(): MediaSession | undefined {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type videojs from "video.js";
|
||||
import type {
|
||||
BandwidthData,
|
||||
onLoadData,
|
||||
@@ -19,19 +18,19 @@ import {
|
||||
} from "../types/VideoError";
|
||||
import type { VideoPlayerStatus } from "../types/VideoPlayerStatus";
|
||||
import type { AudioTrack } from "../types/AudioTrack";
|
||||
import type { VideoJsTracks, VideoJsQualityArray } from "../VideoPlayer.web";
|
||||
import type { VideoTrack } from "../types/VideoTrack";
|
||||
import type { QualityLevel } from "../types/QualityLevel";
|
||||
import type {
|
||||
ListenerSubscription,
|
||||
VideoPlayerEventEmitterBase,
|
||||
} from "../types/EventEmitter";
|
||||
|
||||
type VideoJsPlayer = ReturnType<typeof videojs>;
|
||||
import type { VideoJsPlayer } from "./WebVideoJsTypes";
|
||||
import { attachTrackHandlers } from "./WebTrackHandler";
|
||||
|
||||
export class WebEventEmitter implements VideoPlayerEventEmitterBase {
|
||||
private _isBuffering = false;
|
||||
private _listeners: Map<string, Set<(...args: any[]) => void>> = new Map();
|
||||
private detachTracks: () => void;
|
||||
|
||||
constructor(private player: VideoJsPlayer) {
|
||||
// TODO: add `onBandwidthUpdate`
|
||||
@@ -84,18 +83,7 @@ export class WebEventEmitter implements VideoPlayerEventEmitterBase {
|
||||
this._onError = this._onError.bind(this);
|
||||
this.player.on("error", this._onError);
|
||||
|
||||
this._onTextTrackChange = this._onTextTrackChange.bind(this);
|
||||
this.player.textTracks().on("change", this._onTextTrackChange);
|
||||
|
||||
this._onAudioTrackChange = this._onAudioTrackChange.bind(this);
|
||||
this.player.audioTracks().on("change", this._onAudioTrackChange);
|
||||
|
||||
this._onVideoTrackChange = this._onVideoTrackChange.bind(this);
|
||||
this.player.videoTracks().on("change", this._onVideoTrackChange);
|
||||
|
||||
this._onQualityChange = this._onQualityChange.bind(this);
|
||||
// @ts-expect-error this isn't typed
|
||||
this.player.qualityLevels().on("change", this._onQualityChange);
|
||||
this.detachTracks = attachTrackHandlers(player, this._emit.bind(this));
|
||||
}
|
||||
|
||||
destroy() {
|
||||
@@ -123,14 +111,7 @@ export class WebEventEmitter implements VideoPlayerEventEmitterBase {
|
||||
|
||||
this.player.off("error", this._onError);
|
||||
|
||||
this.player.textTracks().off("change", this._onTextTrackChange);
|
||||
|
||||
this.player.audioTracks().off("change", this._onAudioTrackChange);
|
||||
|
||||
this.player.videoTracks().off("change", this._onVideoTrackChange);
|
||||
|
||||
// @ts-expect-error this isn't typed
|
||||
this.player.qualityLevels().off("change", this._onQualityChange);
|
||||
this.detachTracks();
|
||||
}
|
||||
|
||||
private _addListener(
|
||||
@@ -288,25 +269,25 @@ export class WebEventEmitter implements VideoPlayerEventEmitterBase {
|
||||
return this._addListener("onQualityChange", listener);
|
||||
}
|
||||
|
||||
_onTimeUpdate() {
|
||||
private _onTimeUpdate() {
|
||||
this._emit("onProgress", {
|
||||
currentTime: this.player.currentTime() ?? 0,
|
||||
bufferDuration: this.player.bufferedEnd(),
|
||||
});
|
||||
}
|
||||
|
||||
_onCanPlay() {
|
||||
private _onCanPlay() {
|
||||
this._isBuffering = false;
|
||||
this._emit("onBuffer", false);
|
||||
this._emit("onStatusChange", "readyToPlay");
|
||||
}
|
||||
_onWaiting() {
|
||||
private _onWaiting() {
|
||||
this._isBuffering = true;
|
||||
this._emit("onBuffer", true);
|
||||
this._emit("onStatusChange", "loading");
|
||||
}
|
||||
|
||||
_onDurationChange() {
|
||||
private _onDurationChange() {
|
||||
this._emit("onLoad", {
|
||||
currentTime: this.player.currentTime() ?? 0,
|
||||
duration: this.player.duration() ?? NaN,
|
||||
@@ -316,12 +297,12 @@ export class WebEventEmitter implements VideoPlayerEventEmitterBase {
|
||||
});
|
||||
}
|
||||
|
||||
_onEnded() {
|
||||
private _onEnded() {
|
||||
this._emit("onEnd");
|
||||
this._emit("onStatusChange", "idle");
|
||||
}
|
||||
|
||||
_onLoadStart() {
|
||||
private _onLoadStart() {
|
||||
this._emit("onLoadStart", {
|
||||
sourceType: "network",
|
||||
source: {
|
||||
@@ -346,40 +327,40 @@ export class WebEventEmitter implements VideoPlayerEventEmitterBase {
|
||||
});
|
||||
}
|
||||
|
||||
_onPlay() {
|
||||
private _onPlay() {
|
||||
this._emit("onPlaybackStateChange", {
|
||||
isPlaying: true,
|
||||
isBuffering: this._isBuffering,
|
||||
});
|
||||
}
|
||||
|
||||
_onPause() {
|
||||
private _onPause() {
|
||||
this._emit("onPlaybackStateChange", {
|
||||
isPlaying: false,
|
||||
isBuffering: this._isBuffering,
|
||||
});
|
||||
}
|
||||
|
||||
_onRateChange() {
|
||||
private _onRateChange() {
|
||||
this._emit("onPlaybackRateChange", this.player.playbackRate() ?? 1);
|
||||
}
|
||||
|
||||
_onLoadedData() {
|
||||
private _onLoadedData() {
|
||||
this._emit("onReadyToDisplay");
|
||||
}
|
||||
|
||||
_onSeeked() {
|
||||
private _onSeeked() {
|
||||
this._emit("onSeek", this.player.currentTime() ?? 0);
|
||||
}
|
||||
|
||||
_onVolumeChange() {
|
||||
private _onVolumeChange() {
|
||||
this._emit("onVolumeChange", {
|
||||
muted: this.player.muted() ?? false,
|
||||
volume: this.player.volume() ?? 1,
|
||||
});
|
||||
}
|
||||
|
||||
_onError() {
|
||||
private _onError() {
|
||||
this._emit("onStatusChange", "error");
|
||||
const err = this.player.error();
|
||||
if (!err) {
|
||||
@@ -402,65 +383,4 @@ export class WebEventEmitter implements VideoPlayerEventEmitterBase {
|
||||
>;
|
||||
this._emit("onError", new VideoError(codeMap[err.code] ?? "unknown/unknown", err.message));
|
||||
}
|
||||
|
||||
_onTextTrackChange() {
|
||||
// @ts-expect-error they define length & index properties via prototype
|
||||
const tracks: VideoJsTextTracks = this.player.textTracks();
|
||||
const selected = [...Array(tracks.length)]
|
||||
.map((_, i) => ({
|
||||
id: tracks[i]!.id,
|
||||
label: tracks[i]!.label,
|
||||
language: tracks[i]!.language,
|
||||
selected: tracks[i]!.mode === "showing",
|
||||
}))
|
||||
.find((x) => x.selected);
|
||||
|
||||
this._emit("onTrackChange", selected ?? null);
|
||||
}
|
||||
|
||||
_onAudioTrackChange() {
|
||||
// @ts-expect-error they define length & index properties via prototype
|
||||
const tracks: VideoJsTracks = this.player.audioTracks();
|
||||
const selected = [...Array(tracks.length)]
|
||||
.map((_, i) => ({
|
||||
id: tracks[i]!.id,
|
||||
label: tracks[i]!.label,
|
||||
language: tracks[i]!.language,
|
||||
selected: tracks[i]!.enabled,
|
||||
}))
|
||||
.find((x) => x.selected);
|
||||
|
||||
this._emit("onAudioTrackChange", selected ?? null);
|
||||
}
|
||||
|
||||
_onVideoTrackChange() {
|
||||
// @ts-expect-error they define length & index properties via prototype
|
||||
const tracks: VideoJsTracks = this.player.videoTracks();
|
||||
const selected = [...Array(tracks.length)]
|
||||
.map((_, i) => ({
|
||||
id: tracks[i]!.id,
|
||||
label: tracks[i]!.label,
|
||||
language: tracks[i]!.language,
|
||||
selected: tracks[i]!.enabled,
|
||||
}))
|
||||
.find((x) => x.selected);
|
||||
|
||||
this._emit("onVideoTrackChange", selected ?? null);
|
||||
}
|
||||
|
||||
_onQualityChange() {
|
||||
// @ts-expect-error this isn't typed
|
||||
const levels: VideoJsQualityArray = this.player.qualityLevels();
|
||||
if (levels.selectedIndex < 0) return;
|
||||
const quality = levels[levels.selectedIndex];
|
||||
if (!quality) return;
|
||||
|
||||
this._emit("onQualityChange", {
|
||||
id: quality.id,
|
||||
width: quality.width,
|
||||
height: quality.height,
|
||||
bitrate: quality.bitrate,
|
||||
selected: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import type { AudioTrack } from "../types/AudioTrack";
|
||||
import type { QualityLevel } from "../types/QualityLevel";
|
||||
import type { TextTrack } from "../types/TextTrack";
|
||||
import type { VideoTrack } from "../types/VideoTrack";
|
||||
import {
|
||||
mapVideoJsTracks,
|
||||
type VideoJsPlayer,
|
||||
type VideoJsQualityArray,
|
||||
type VideoJsTextTracks,
|
||||
type VideoJsTracks,
|
||||
} from "./WebVideoJsTypes";
|
||||
|
||||
type EmitFn = (event: string, ...args: unknown[]) => void;
|
||||
|
||||
/**
|
||||
* Attaches track change listeners to a video.js player and returns a cleanup function.
|
||||
* Handles text, audio, video track changes and quality level changes.
|
||||
*/
|
||||
export function attachTrackHandlers(
|
||||
player: VideoJsPlayer,
|
||||
emit: EmitFn,
|
||||
): () => void {
|
||||
const onTextTrackChange = () => {
|
||||
// @ts-expect-error video.js defines length & index properties via prototype
|
||||
const tracks: VideoJsTextTracks = player.textTracks();
|
||||
const selected = mapVideoJsTracks(tracks, (track): TextTrack => ({
|
||||
id: track.id,
|
||||
label: track.label,
|
||||
language: track.language,
|
||||
selected: track.mode === "showing",
|
||||
})).find((x) => x.selected);
|
||||
|
||||
emit("onTrackChange", selected ?? null);
|
||||
};
|
||||
|
||||
const onAudioTrackChange = () => {
|
||||
// @ts-expect-error video.js defines length & index properties via prototype
|
||||
const tracks: VideoJsTracks = player.audioTracks();
|
||||
const selected = mapVideoJsTracks(tracks, (track): AudioTrack => ({
|
||||
id: track.id,
|
||||
label: track.label,
|
||||
language: track.language,
|
||||
selected: track.enabled,
|
||||
})).find((x) => x.selected);
|
||||
|
||||
emit("onAudioTrackChange", selected ?? null);
|
||||
};
|
||||
|
||||
const onVideoTrackChange = () => {
|
||||
// @ts-expect-error video.js defines length & index properties via prototype
|
||||
const tracks: VideoJsTracks = player.videoTracks();
|
||||
const selected = mapVideoJsTracks(tracks, (track): VideoTrack => ({
|
||||
id: track.id,
|
||||
label: track.label,
|
||||
language: track.language,
|
||||
selected: track.enabled,
|
||||
})).find((x) => x.selected);
|
||||
|
||||
emit("onVideoTrackChange", selected ?? null);
|
||||
};
|
||||
|
||||
const onQualityChange = () => {
|
||||
// @ts-expect-error qualityLevels is from videojs-contrib-quality-levels plugin
|
||||
const levels: VideoJsQualityArray = player.qualityLevels();
|
||||
if (levels.selectedIndex < 0) return;
|
||||
const quality = levels[levels.selectedIndex];
|
||||
if (!quality) return;
|
||||
|
||||
emit("onQualityChange", {
|
||||
id: quality.id,
|
||||
width: quality.width,
|
||||
height: quality.height,
|
||||
bitrate: quality.bitrate,
|
||||
selected: true,
|
||||
} satisfies QualityLevel);
|
||||
};
|
||||
|
||||
// Attach listeners
|
||||
player.textTracks().on("change", onTextTrackChange);
|
||||
player.audioTracks().on("change", onAudioTrackChange);
|
||||
player.videoTracks().on("change", onVideoTrackChange);
|
||||
// @ts-expect-error qualityLevels is from videojs-contrib-quality-levels plugin
|
||||
player.qualityLevels().on("change", onQualityChange);
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
player.textTracks().off("change", onTextTrackChange);
|
||||
player.audioTracks().off("change", onAudioTrackChange);
|
||||
player.videoTracks().off("change", onVideoTrackChange);
|
||||
// @ts-expect-error qualityLevels is from videojs-contrib-quality-levels plugin
|
||||
player.qualityLevels().off("change", onQualityChange);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import type videojs from "video.js";
|
||||
|
||||
export type VideoJsPlayer = ReturnType<typeof videojs>;
|
||||
|
||||
export type VideoJsTextTracks = {
|
||||
length: number;
|
||||
[i: number]: {
|
||||
id: string;
|
||||
label: string;
|
||||
language: string;
|
||||
default: boolean;
|
||||
mode: "showing" | "disabled" | "hidden";
|
||||
};
|
||||
};
|
||||
|
||||
export type VideoJsTracks = {
|
||||
length: number;
|
||||
[i: number]: {
|
||||
id: string;
|
||||
label: string;
|
||||
language: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type VideoJsQualityArray = {
|
||||
length: number;
|
||||
selectedIndex: number;
|
||||
[i: number]: {
|
||||
id: string;
|
||||
label: string;
|
||||
width: number;
|
||||
height: number;
|
||||
bitrate: number;
|
||||
frameRate: number;
|
||||
enabled: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export function mapVideoJsTracks<T, R>(
|
||||
tracks: { length: number; [i: number]: T | undefined },
|
||||
mapper: (track: T, index: number) => R,
|
||||
): R[] {
|
||||
const result: R[] = [];
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
const track = tracks[i];
|
||||
if (track) {
|
||||
result.push(mapper(track, i));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user