2023-01-10 19:53:55 +01:00
|
|
|
import { canChangeVolume } from "@/utils/detectFeatures";
|
2023-01-10 01:01:51 +01:00
|
|
|
import fscreen from "fscreen";
|
2023-01-10 19:53:55 +01:00
|
|
|
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
2023-01-08 15:37:16 +01:00
|
|
|
import {
|
|
|
|
initialControls,
|
|
|
|
PlayerControls,
|
|
|
|
populateControls,
|
|
|
|
} from "./controlVideo";
|
2023-01-08 17:51:38 +01:00
|
|
|
import { handleBuffered } from "./utils";
|
2023-01-08 15:37:16 +01:00
|
|
|
|
|
|
|
export type PlayerState = {
|
|
|
|
isPlaying: boolean;
|
|
|
|
isPaused: boolean;
|
2023-01-08 16:23:42 +01:00
|
|
|
isSeeking: boolean;
|
2023-01-08 21:18:45 +01:00
|
|
|
isLoading: boolean;
|
2023-01-14 00:27:40 +01:00
|
|
|
isFirstLoading: boolean;
|
2023-01-08 16:23:42 +01:00
|
|
|
isFullscreen: boolean;
|
2023-01-08 17:51:38 +01:00
|
|
|
time: number;
|
|
|
|
duration: number;
|
|
|
|
volume: number;
|
|
|
|
buffered: number;
|
2023-01-10 19:53:55 +01:00
|
|
|
pausedWhenSeeking: boolean;
|
|
|
|
hasInitialized: boolean;
|
|
|
|
leftControlHovering: boolean;
|
|
|
|
hasPlayedOnce: boolean;
|
2023-01-21 23:45:26 +01:00
|
|
|
seasonData: {
|
|
|
|
isSeries: boolean;
|
|
|
|
current?: {
|
2023-01-22 19:26:08 +01:00
|
|
|
episodeId: string;
|
|
|
|
seasonId: string;
|
2023-01-21 23:45:26 +01:00
|
|
|
};
|
|
|
|
};
|
2023-01-15 16:51:55 +01:00
|
|
|
error: null | {
|
|
|
|
name: string;
|
|
|
|
description: string;
|
|
|
|
};
|
2023-01-22 20:51:58 +01:00
|
|
|
canAirplay: boolean;
|
2023-01-10 19:53:55 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
export type PlayerContext = PlayerState & PlayerControls;
|
2023-01-08 15:37:16 +01:00
|
|
|
|
2023-01-10 19:53:55 +01:00
|
|
|
export const initialPlayerState: PlayerContext = {
|
2023-01-08 15:37:16 +01:00
|
|
|
isPlaying: false,
|
|
|
|
isPaused: true,
|
2023-01-08 16:23:42 +01:00
|
|
|
isFullscreen: false,
|
2023-01-08 21:18:45 +01:00
|
|
|
isLoading: false,
|
2023-01-08 16:23:42 +01:00
|
|
|
isSeeking: false,
|
2023-01-14 00:27:40 +01:00
|
|
|
isFirstLoading: true,
|
2023-01-08 17:51:38 +01:00
|
|
|
time: 0,
|
|
|
|
duration: 0,
|
|
|
|
volume: 0,
|
|
|
|
buffered: 0,
|
2023-01-10 19:53:55 +01:00
|
|
|
pausedWhenSeeking: false,
|
|
|
|
hasInitialized: false,
|
|
|
|
leftControlHovering: false,
|
|
|
|
hasPlayedOnce: false,
|
2023-01-15 16:51:55 +01:00
|
|
|
error: null,
|
2023-01-21 23:45:26 +01:00
|
|
|
seasonData: {
|
|
|
|
isSeries: false,
|
|
|
|
},
|
2023-01-22 20:51:58 +01:00
|
|
|
canAirplay: false,
|
2023-01-08 15:37:16 +01:00
|
|
|
...initialControls,
|
|
|
|
};
|
|
|
|
|
2023-01-10 19:53:55 +01:00
|
|
|
type SetPlayer = (s: React.SetStateAction<PlayerContext>) => void;
|
2023-01-08 15:37:16 +01:00
|
|
|
|
|
|
|
function readState(player: HTMLVideoElement, update: SetPlayer) {
|
|
|
|
const state = {
|
|
|
|
...initialPlayerState,
|
|
|
|
};
|
|
|
|
state.isPaused = player.paused;
|
|
|
|
state.isPlaying = !player.paused;
|
2023-01-08 16:23:42 +01:00
|
|
|
state.isFullscreen = !!document.fullscreenElement;
|
|
|
|
state.isSeeking = player.seeking;
|
2023-01-08 17:51:38 +01:00
|
|
|
state.time = player.currentTime;
|
|
|
|
state.duration = player.duration;
|
|
|
|
state.volume = player.volume;
|
|
|
|
state.buffered = handleBuffered(player.currentTime, player.buffered);
|
2023-01-08 21:18:45 +01:00
|
|
|
state.isLoading = false;
|
2023-01-10 19:53:55 +01:00
|
|
|
state.hasInitialized = true;
|
2023-01-15 16:51:55 +01:00
|
|
|
state.error = null;
|
2023-01-08 15:37:16 +01:00
|
|
|
|
2023-01-10 19:53:55 +01:00
|
|
|
update((s) => ({
|
|
|
|
...state,
|
|
|
|
pausedWhenSeeking: s.pausedWhenSeeking,
|
|
|
|
hasPlayedOnce: s.hasPlayedOnce,
|
2023-01-14 00:27:40 +01:00
|
|
|
isFirstLoading: s.isFirstLoading,
|
2023-01-10 19:53:55 +01:00
|
|
|
}));
|
2023-01-08 15:37:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
function registerListeners(player: HTMLVideoElement, update: SetPlayer) {
|
2023-01-08 17:51:38 +01:00
|
|
|
const pause = () => {
|
2023-01-08 21:18:45 +01:00
|
|
|
update((s) => ({
|
|
|
|
...s,
|
|
|
|
isPaused: true,
|
|
|
|
isPlaying: false,
|
|
|
|
}));
|
2023-01-08 17:51:38 +01:00
|
|
|
};
|
2023-01-08 21:18:45 +01:00
|
|
|
const playing = () => {
|
|
|
|
update((s) => ({
|
|
|
|
...s,
|
|
|
|
isPaused: false,
|
|
|
|
isPlaying: true,
|
|
|
|
isLoading: false,
|
2023-01-10 19:53:55 +01:00
|
|
|
hasPlayedOnce: true,
|
2023-01-08 21:18:45 +01:00
|
|
|
}));
|
2023-01-08 17:51:38 +01:00
|
|
|
};
|
|
|
|
const seeking = () => {
|
2023-01-08 16:23:42 +01:00
|
|
|
update((s) => ({ ...s, isSeeking: true }));
|
2023-01-08 17:51:38 +01:00
|
|
|
};
|
|
|
|
const seeked = () => {
|
2023-01-08 16:23:42 +01:00
|
|
|
update((s) => ({ ...s, isSeeking: false }));
|
2023-01-08 17:51:38 +01:00
|
|
|
};
|
2023-01-08 21:18:45 +01:00
|
|
|
const waiting = () => {
|
|
|
|
update((s) => ({ ...s, isLoading: true }));
|
|
|
|
};
|
2023-01-08 17:51:38 +01:00
|
|
|
const fullscreenchange = () => {
|
2023-01-08 16:23:42 +01:00
|
|
|
update((s) => ({ ...s, isFullscreen: !!document.fullscreenElement }));
|
2023-01-08 17:51:38 +01:00
|
|
|
};
|
|
|
|
const timeupdate = () => {
|
|
|
|
update((s) => ({
|
|
|
|
...s,
|
|
|
|
duration: player.duration,
|
|
|
|
time: player.currentTime,
|
|
|
|
}));
|
|
|
|
};
|
|
|
|
const loadedmetadata = () => {
|
|
|
|
update((s) => ({
|
|
|
|
...s,
|
|
|
|
duration: player.duration,
|
|
|
|
}));
|
|
|
|
};
|
2023-01-10 19:53:55 +01:00
|
|
|
const volumechange = async () => {
|
|
|
|
if (await canChangeVolume())
|
|
|
|
update((s) => ({
|
|
|
|
...s,
|
|
|
|
volume: player.volume,
|
|
|
|
}));
|
2023-01-08 17:51:38 +01:00
|
|
|
};
|
|
|
|
const progress = () => {
|
|
|
|
update((s) => ({
|
|
|
|
...s,
|
|
|
|
buffered: handleBuffered(player.currentTime, player.buffered),
|
|
|
|
}));
|
|
|
|
};
|
2023-01-14 00:27:40 +01:00
|
|
|
const canplay = () => {
|
|
|
|
update((s) => ({
|
|
|
|
...s,
|
|
|
|
isFirstLoading: false,
|
|
|
|
}));
|
|
|
|
};
|
2023-01-15 16:51:55 +01:00
|
|
|
const error = () => {
|
|
|
|
console.error("Native video player threw error", player.error);
|
|
|
|
update((s) => ({
|
|
|
|
...s,
|
|
|
|
error: player.error
|
|
|
|
? {
|
|
|
|
description: player.error.message,
|
|
|
|
name: `Error ${player.error.code}`,
|
|
|
|
}
|
|
|
|
: null,
|
|
|
|
}));
|
|
|
|
};
|
2023-01-22 20:51:58 +01:00
|
|
|
const canAirplay = (e: any) => {
|
|
|
|
if (e.availability === "available") {
|
|
|
|
update((s) => ({
|
|
|
|
...s,
|
|
|
|
canAirplay: true,
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
};
|
2023-01-08 17:51:38 +01:00
|
|
|
|
|
|
|
player.addEventListener("pause", pause);
|
2023-01-08 21:18:45 +01:00
|
|
|
player.addEventListener("playing", playing);
|
2023-01-08 17:51:38 +01:00
|
|
|
player.addEventListener("seeking", seeking);
|
|
|
|
player.addEventListener("seeked", seeked);
|
2023-01-10 01:01:51 +01:00
|
|
|
fscreen.addEventListener("fullscreenchange", fullscreenchange);
|
2023-01-08 17:51:38 +01:00
|
|
|
player.addEventListener("timeupdate", timeupdate);
|
|
|
|
player.addEventListener("loadedmetadata", loadedmetadata);
|
|
|
|
player.addEventListener("volumechange", volumechange);
|
|
|
|
player.addEventListener("progress", progress);
|
2023-01-08 21:18:45 +01:00
|
|
|
player.addEventListener("waiting", waiting);
|
2023-01-14 00:27:40 +01:00
|
|
|
player.addEventListener("canplay", canplay);
|
2023-01-15 16:51:55 +01:00
|
|
|
player.addEventListener("error", error);
|
2023-01-22 20:51:58 +01:00
|
|
|
player.addEventListener(
|
|
|
|
"webkitplaybacktargetavailabilitychanged",
|
|
|
|
canAirplay
|
|
|
|
);
|
2023-01-08 17:51:38 +01:00
|
|
|
|
|
|
|
return () => {
|
|
|
|
player.removeEventListener("pause", pause);
|
2023-01-08 21:18:45 +01:00
|
|
|
player.removeEventListener("playing", playing);
|
2023-01-08 17:51:38 +01:00
|
|
|
player.removeEventListener("seeking", seeking);
|
|
|
|
player.removeEventListener("seeked", seeked);
|
2023-01-10 01:01:51 +01:00
|
|
|
fscreen.removeEventListener("fullscreenchange", fullscreenchange);
|
2023-01-08 17:51:38 +01:00
|
|
|
player.removeEventListener("timeupdate", timeupdate);
|
|
|
|
player.removeEventListener("loadedmetadata", loadedmetadata);
|
|
|
|
player.removeEventListener("volumechange", volumechange);
|
|
|
|
player.removeEventListener("progress", progress);
|
2023-01-08 21:18:45 +01:00
|
|
|
player.removeEventListener("waiting", waiting);
|
2023-01-14 00:27:40 +01:00
|
|
|
player.removeEventListener("canplay", canplay);
|
2023-01-15 16:51:55 +01:00
|
|
|
player.removeEventListener("error", error);
|
2023-01-22 20:51:58 +01:00
|
|
|
player.removeEventListener(
|
|
|
|
"webkitplaybacktargetavailabilitychanged",
|
|
|
|
canAirplay
|
|
|
|
);
|
2023-01-08 17:51:38 +01:00
|
|
|
};
|
2023-01-08 15:37:16 +01:00
|
|
|
}
|
|
|
|
|
2023-01-08 16:23:42 +01:00
|
|
|
export function useVideoPlayer(
|
|
|
|
ref: MutableRefObject<HTMLVideoElement | null>,
|
|
|
|
wrapperRef: MutableRefObject<HTMLDivElement | null>
|
|
|
|
) {
|
2023-01-08 15:37:16 +01:00
|
|
|
const [state, setState] = useState(initialPlayerState);
|
2023-01-10 19:53:55 +01:00
|
|
|
const stateRef = useRef<PlayerState | null>(null);
|
2023-01-08 15:37:16 +01:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const player = ref.current;
|
2023-01-08 16:23:42 +01:00
|
|
|
const wrapper = wrapperRef.current;
|
|
|
|
if (player && wrapper) {
|
2023-01-08 15:37:16 +01:00
|
|
|
readState(player, setState);
|
|
|
|
registerListeners(player, setState);
|
2023-01-10 19:53:55 +01:00
|
|
|
setState((s) => ({
|
|
|
|
...s,
|
|
|
|
...populateControls(player, wrapper, setState as any, stateRef),
|
|
|
|
}));
|
2023-01-08 15:37:16 +01:00
|
|
|
}
|
2023-01-10 19:53:55 +01:00
|
|
|
}, [ref, wrapperRef, stateRef]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
stateRef.current = state;
|
|
|
|
}, [state, stateRef]);
|
2023-01-08 15:37:16 +01:00
|
|
|
|
|
|
|
return {
|
|
|
|
playerState: state,
|
|
|
|
};
|
|
|
|
}
|