mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-05 13:46:59 +00:00
Cleanup usePlayerStatus on web
This commit is contained in:
Generated
+11373
File diff suppressed because it is too large
Load Diff
+67
-55
@@ -1,7 +1,16 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { usePlayer } from "./provider.web";
|
||||
import type { OmniEvents } from "./types/events";
|
||||
import type { OmniPlayerState, PlayerStatus } from "./types/player";
|
||||
import {
|
||||
selectPlayback,
|
||||
selectTime,
|
||||
selectBuffer,
|
||||
selectPlaybackRate,
|
||||
selectVolume,
|
||||
selectSource,
|
||||
usePlayer,
|
||||
type Selector,
|
||||
} from "@videojs/react";
|
||||
|
||||
export const useEvent = <Event extends keyof OmniEvents>(
|
||||
event: Event,
|
||||
@@ -65,8 +74,7 @@ export const useEvent = <Event extends keyof OmniEvents>(
|
||||
}
|
||||
};
|
||||
media.videoTracks?.addEventListener("change", handler);
|
||||
return () =>
|
||||
media.videoTracks?.removeEventListener("change", handler);
|
||||
return () => media.videoTracks?.removeEventListener("change", handler);
|
||||
}
|
||||
case "audioTrackChange": {
|
||||
const handler = () => {
|
||||
@@ -85,8 +93,7 @@ export const useEvent = <Event extends keyof OmniEvents>(
|
||||
}
|
||||
};
|
||||
media.audioTracks?.addEventListener("change", handler);
|
||||
return () =>
|
||||
media.audioTracks?.removeEventListener("change", handler);
|
||||
return () => media.audioTracks?.removeEventListener("change", handler);
|
||||
}
|
||||
case "subtitleChange": {
|
||||
const handler = () => {
|
||||
@@ -116,6 +123,56 @@ export const useEvent = <Event extends keyof OmniEvents>(
|
||||
}, [player, event]);
|
||||
};
|
||||
|
||||
type MapperConfig = {
|
||||
[Key in keyof OmniPlayerState]?: {
|
||||
selector: Selector<any, any>;
|
||||
mapper: (ret: any) => OmniPlayerState[Key];
|
||||
};
|
||||
};
|
||||
|
||||
function createMapper<
|
||||
Key extends keyof OmniPlayerState,
|
||||
Result,
|
||||
Ret extends OmniPlayerState[Key],
|
||||
>(key: Key, selector: Selector<any, Result>, mapper: (ret: Result) => Ret) {
|
||||
return { [key]: { selector, mapper } };
|
||||
}
|
||||
|
||||
const mapper: MapperConfig = {
|
||||
...createMapper("status", selectPlayback, (s) => {
|
||||
if (s?.waiting) return "loading";
|
||||
if (s?.ended) return "idle";
|
||||
return "readyToPlay";
|
||||
}),
|
||||
...createMapper("isPlaying", selectPlayback, (s) => {
|
||||
if (!s) return false;
|
||||
return !s.paused;
|
||||
}),
|
||||
...createMapper("currentTime", selectTime, (s) => {
|
||||
return s?.currentTime ?? 0;
|
||||
}),
|
||||
...createMapper("buffered", selectBuffer, (s) => {
|
||||
if (!s?.buffered.length) return 0;
|
||||
const last = s.buffered[s.buffered.length - 1];
|
||||
return last?.[1] ?? 0;
|
||||
}),
|
||||
...createMapper("duration", selectTime, (s) => {
|
||||
return s?.duration ?? 0;
|
||||
}),
|
||||
...createMapper("playbackRate", selectPlaybackRate, (s) => {
|
||||
return s?.playbackRate ?? 1;
|
||||
}),
|
||||
...createMapper("volume", selectVolume, (s) => {
|
||||
return s?.volume ?? 1;
|
||||
}),
|
||||
...createMapper("muted", selectVolume, (s) => {
|
||||
return s?.muted ?? false;
|
||||
}),
|
||||
...createMapper("isAutoQuality", selectSource, (s) => {
|
||||
return false;
|
||||
}),
|
||||
};
|
||||
|
||||
export function usePlayerState<Key extends keyof OmniPlayerState>(
|
||||
key: Key,
|
||||
): OmniPlayerState[Key];
|
||||
@@ -125,55 +182,10 @@ export function usePlayerState(
|
||||
): OmniPlayerState["currentTime"];
|
||||
export function usePlayerState<Key extends keyof OmniPlayerState>(
|
||||
key: Key,
|
||||
refresh?: number,
|
||||
_refresh?: number,
|
||||
): OmniPlayerState[Key] {
|
||||
const player = usePlayer();
|
||||
const store = player.store;
|
||||
const [ret, setState] = useState<any>(player[key]);
|
||||
|
||||
useEffect(() => {
|
||||
switch (key) {
|
||||
case "currentTime":
|
||||
case "buffered":
|
||||
case "duration":
|
||||
case "playbackRate":
|
||||
case "volume": {
|
||||
setState(store.state[key]);
|
||||
const handler = () => setState(store.state[key]);
|
||||
const unsub = store.subscribe(handler);
|
||||
return unsub;
|
||||
}
|
||||
case "isPlaying":
|
||||
case "muted":
|
||||
case "isAutoQuality": {
|
||||
setState(player[key]);
|
||||
const handler = () => setState(player[key]);
|
||||
const unsub = store.subscribe(handler);
|
||||
return unsub;
|
||||
}
|
||||
case "status": {
|
||||
const prevRef = { current: player.status as PlayerStatus };
|
||||
setState(prevRef.current);
|
||||
const int = setInterval(() => {
|
||||
const status = player.status;
|
||||
if (status !== prevRef.current) {
|
||||
prevRef.current = status;
|
||||
setState(status);
|
||||
}
|
||||
}, 100);
|
||||
return () => clearInterval(int);
|
||||
}
|
||||
}
|
||||
}, [player, key, store]);
|
||||
|
||||
if (key === "currentTime") refresh ??= 1;
|
||||
useEffect(() => {
|
||||
if (!refresh || refresh <= 0) return;
|
||||
const int = setInterval(() => {
|
||||
setState(player[key]);
|
||||
}, refresh * 1000);
|
||||
return () => clearInterval(int);
|
||||
}, [refresh, key, player]);
|
||||
|
||||
return ret;
|
||||
const config = mapper[key];
|
||||
if (!config) throw new Error(`No mapper for ${key}`);
|
||||
const ret = usePlayer(config.selector);
|
||||
return config.mapper(ret);
|
||||
}
|
||||
|
||||
+134
-49
@@ -1,18 +1,57 @@
|
||||
import { createPlayer } from "@videojs/react";
|
||||
import { videoFeatures } from "@videojs/react/video";
|
||||
import type { VideoPlayerStore } from "@videojs/core/dom";
|
||||
import {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import type { PlayerStatus, Rendition, Track } from "./types/player";
|
||||
import type {
|
||||
OmniPlayer,
|
||||
PlayerStatus,
|
||||
Rendition,
|
||||
Track,
|
||||
} from "./types/player";
|
||||
import type { Source } from "./types/source";
|
||||
import { useLazyRef } from "./utils/lazy-ref";
|
||||
|
||||
interface VideoTrack {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
readonly language: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface VideoTrackList {
|
||||
readonly length: number;
|
||||
[index: number]: VideoTrack;
|
||||
addEventListener(type: "change", listener: () => void): void;
|
||||
removeEventListener(type: "change", listener: () => void): void;
|
||||
}
|
||||
|
||||
interface AudioTrack {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
readonly language: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface AudioTrackList {
|
||||
readonly length: number;
|
||||
[index: number]: AudioTrack;
|
||||
addEventListener(type: "change", listener: () => void): void;
|
||||
removeEventListener(type: "change", listener: () => void): void;
|
||||
}
|
||||
|
||||
interface HtmlVideoElementWithTracks extends HTMLVideoElement {
|
||||
videoTracks?: VideoTrackList;
|
||||
audioTracks?: AudioTrackList;
|
||||
}
|
||||
|
||||
export const VideoPlayer = createPlayer({ features: videoFeatures });
|
||||
|
||||
const PlayerCtx = createContext<WebOmniPlayer>(null!);
|
||||
const PlayerCtx = createContext<OmniPlayer>(null!);
|
||||
|
||||
export const OmniProvider = ({
|
||||
children,
|
||||
@@ -57,26 +96,37 @@ const PlayerInitializer = ({
|
||||
|
||||
export const usePlayer = () => useContext(PlayerCtx);
|
||||
|
||||
class WebOmniPlayer {
|
||||
private _store: any;
|
||||
interface ExtendedStoreState {
|
||||
onPrev?: () => void;
|
||||
onNext?: () => void;
|
||||
}
|
||||
|
||||
export class WebOmniPlayer implements OmniPlayer {
|
||||
private _store: VideoPlayerStore;
|
||||
private _source: Source;
|
||||
private _showNotification = false;
|
||||
private _mediaElement: HtmlVideoElementWithTracks | null = null;
|
||||
|
||||
constructor(store: any, source: Source) {
|
||||
constructor(store: VideoPlayerStore, source: Source) {
|
||||
this._store = store;
|
||||
this._source = source;
|
||||
}
|
||||
|
||||
get store() {
|
||||
return this._store;
|
||||
}
|
||||
|
||||
setMediaElement(media: HTMLVideoElement | null) {
|
||||
this._mediaElement = media as HtmlVideoElementWithTracks | null;
|
||||
if (media) {
|
||||
this.setupSubtitleTracks(media);
|
||||
}
|
||||
}
|
||||
|
||||
get mediaElement(): HtmlVideoElementWithTracks | null {
|
||||
return this._mediaElement;
|
||||
}
|
||||
|
||||
get store(): VideoPlayerStore {
|
||||
return this._store;
|
||||
}
|
||||
|
||||
updateSource(source: Source) {
|
||||
this._source = source;
|
||||
}
|
||||
@@ -115,7 +165,7 @@ class WebOmniPlayer {
|
||||
|
||||
get buffered(): number {
|
||||
const buffered = this._store.state.buffered;
|
||||
if (!buffered || buffered.length === 0) return 0;
|
||||
if (buffered.length === 0) return 0;
|
||||
const lastRange = buffered[buffered.length - 1];
|
||||
return lastRange ? lastRange[1] : 0;
|
||||
}
|
||||
@@ -166,12 +216,28 @@ class WebOmniPlayer {
|
||||
this._store.seek(this.currentTime + offset).catch(() => {});
|
||||
}
|
||||
|
||||
get onPrev(): (() => void) | undefined {
|
||||
return (this._store.state as ExtendedStoreState).onPrev;
|
||||
}
|
||||
|
||||
set onPrev(cb: (() => void) | undefined) {
|
||||
(this._store.state as ExtendedStoreState).onPrev = cb;
|
||||
}
|
||||
|
||||
get onNext(): (() => void) | undefined {
|
||||
return (this._store.state as ExtendedStoreState).onNext;
|
||||
}
|
||||
|
||||
set onNext(cb: (() => void) | undefined) {
|
||||
(this._store.state as ExtendedStoreState).onNext = cb;
|
||||
}
|
||||
|
||||
playPrev(): void {
|
||||
this._store.state.onPrev?.();
|
||||
this.onPrev?.();
|
||||
}
|
||||
|
||||
playNext(): void {
|
||||
this._store.state.onNext?.();
|
||||
this.onNext?.();
|
||||
}
|
||||
|
||||
get hasPrev(): boolean {
|
||||
@@ -183,42 +249,60 @@ class WebOmniPlayer {
|
||||
}
|
||||
|
||||
get videos(): Track[] {
|
||||
const media = this._store.state.media;
|
||||
if (!media?.videoTracks) return [];
|
||||
return Array.from(media.videoTracks).map(
|
||||
(track: any, i: number) => ({
|
||||
id: track.id || `video-${i}`,
|
||||
label: track.label,
|
||||
language: track.language,
|
||||
selected: track.enabled,
|
||||
}),
|
||||
);
|
||||
const videoTracks = this._mediaElement?.videoTracks;
|
||||
if (!videoTracks) return [];
|
||||
const tracks: Track[] = [];
|
||||
for (let i = 0; i < videoTracks.length; i++) {
|
||||
const track = videoTracks[i];
|
||||
if (track) {
|
||||
tracks.push({
|
||||
id: track.id || `video-${i}`,
|
||||
label: track.label,
|
||||
language: track.language,
|
||||
selected: track.enabled,
|
||||
});
|
||||
}
|
||||
}
|
||||
return tracks;
|
||||
}
|
||||
|
||||
selectVideo(video: Track): void {
|
||||
const media = this._store.state.media;
|
||||
if (!media?.videoTracks) return;
|
||||
for (let i = 0; i < media.videoTracks.length; i++) {
|
||||
media.videoTracks[i].enabled = media.videoTracks[i].id === video.id;
|
||||
const videoTracks = this._mediaElement?.videoTracks;
|
||||
if (!videoTracks) return;
|
||||
for (let i = 0; i < videoTracks.length; i++) {
|
||||
const track = videoTracks[i];
|
||||
if (track) {
|
||||
track.enabled = track.id === video.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get audios(): Track[] {
|
||||
const media = this._store.state.media;
|
||||
if (!media?.audioTracks) return [];
|
||||
return Array.from(media.audioTracks).map((track: any, i: number) => ({
|
||||
id: track.id || `audio-${i}`,
|
||||
label: track.label,
|
||||
language: track.language,
|
||||
selected: track.enabled,
|
||||
}));
|
||||
const audioTracks = this._mediaElement?.audioTracks;
|
||||
if (!audioTracks) return [];
|
||||
const tracks: Track[] = [];
|
||||
for (let i = 0; i < audioTracks.length; i++) {
|
||||
const track = audioTracks[i];
|
||||
if (track) {
|
||||
tracks.push({
|
||||
id: track.id || `audio-${i}`,
|
||||
label: track.label,
|
||||
language: track.language,
|
||||
selected: track.enabled,
|
||||
});
|
||||
}
|
||||
}
|
||||
return tracks;
|
||||
}
|
||||
|
||||
selectAudio(audio: Track): void {
|
||||
const media = this._store.state.media;
|
||||
if (!media?.audioTracks) return;
|
||||
for (let i = 0; i < media.audioTracks.length; i++) {
|
||||
media.audioTracks[i].enabled = media.audioTracks[i].id === audio.id;
|
||||
const audioTracks = this._mediaElement?.audioTracks;
|
||||
if (!audioTracks) return;
|
||||
for (let i = 0; i < audioTracks.length; i++) {
|
||||
const track = audioTracks[i];
|
||||
if (track) {
|
||||
track.enabled = track.id === audio.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,12 +314,12 @@ class WebOmniPlayer {
|
||||
selected: false,
|
||||
}));
|
||||
|
||||
const media = this._store.state.media as any;
|
||||
if (!media?.textTracks) return sourceSubtitles;
|
||||
const textTracks = this._mediaElement?.textTracks;
|
||||
if (!textTracks) return sourceSubtitles;
|
||||
|
||||
const mediaTracks: Track[] = [];
|
||||
for (let i = 0; i < media.textTracks.length; i++) {
|
||||
const track = media.textTracks[i];
|
||||
for (let i = 0; i < textTracks.length; i++) {
|
||||
const track = textTracks[i];
|
||||
if (track && (track.kind === "subtitles" || track.kind === "captions")) {
|
||||
mediaTracks.push({
|
||||
id: track.id || `text-${i}`,
|
||||
@@ -250,14 +334,15 @@ class WebOmniPlayer {
|
||||
}
|
||||
|
||||
selectSubtitle(subtitle?: Track): void {
|
||||
const media = this._store.state.media as any;
|
||||
if (!media?.textTracks) return;
|
||||
for (let i = 0; i < media.textTracks.length; i++) {
|
||||
const track = media.textTracks[i];
|
||||
const textTracks = this._mediaElement?.textTracks;
|
||||
if (!textTracks) return;
|
||||
for (let i = 0; i < textTracks.length; i++) {
|
||||
const track = textTracks[i];
|
||||
if (track && (track.kind === "subtitles" || track.kind === "captions")) {
|
||||
track.mode = subtitle && (track.id === subtitle.id || track.label === subtitle.label)
|
||||
? "showing"
|
||||
: "hidden";
|
||||
track.mode =
|
||||
subtitle && (track.id === subtitle.id || track.label === subtitle.label)
|
||||
? "showing"
|
||||
: "hidden";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user