42 changed files with 222 additions and 3 deletions
@ -0,0 +1,19 @@ |
|||||||
|
import { VideoPlayerContextProvider } from "../state/hooks"; |
||||||
|
|
||||||
|
export interface VideoPlayerProps { |
||||||
|
children?: React.ReactNode; |
||||||
|
} |
||||||
|
|
||||||
|
export function VideoPlayer(props: VideoPlayerProps) { |
||||||
|
// TODO error boundary
|
||||||
|
// TODO move error boundary to only decorated, <VideoPlayer /> shouldn't have styling
|
||||||
|
// TODO internal controls
|
||||||
|
|
||||||
|
return ( |
||||||
|
<VideoPlayerContextProvider> |
||||||
|
<div className="is-video-player relative h-full w-full select-none overflow-hidden bg-black [border-left:env(safe-area-inset-left)_solid_transparent] [border-right:env(safe-area-inset-right)_solid_transparent]"> |
||||||
|
<div className="absolute inset-0">{props.children}</div> |
||||||
|
</div> |
||||||
|
</VideoPlayerContextProvider> |
||||||
|
); |
||||||
|
} |
@ -1,12 +1,12 @@ |
|||||||
import { useGoBack } from "@/hooks/useGoBack"; |
import { useGoBack } from "@/hooks/useGoBack"; |
||||||
import { useVolumeControl } from "@/hooks/useVolumeToggle"; |
import { useVolumeControl } from "@/hooks/useVolumeToggle"; |
||||||
import { forwardRef, useContext, useEffect, useRef } from "react"; |
import { forwardRef, useContext, useEffect, useRef } from "react"; |
||||||
import { VideoErrorBoundary } from "./parts/VideoErrorBoundary"; |
import { VideoErrorBoundary } from "../../components/video/parts/VideoErrorBoundary"; |
||||||
import { |
import { |
||||||
useVideoPlayerState, |
useVideoPlayerState, |
||||||
VideoPlayerContext, |
VideoPlayerContext, |
||||||
VideoPlayerContextProvider, |
VideoPlayerContextProvider, |
||||||
} from "./VideoContext"; |
} from "../../video/components./../components/video/VideoContext"; |
||||||
|
|
||||||
export interface VideoPlayerProps { |
export interface VideoPlayerProps { |
||||||
autoPlay?: boolean; |
autoPlay?: boolean; |
@ -0,0 +1,9 @@ |
|||||||
|
import { VideoPlayerState } from "./types"; |
||||||
|
|
||||||
|
export const _players: Map<string, VideoPlayerState> = new Map(); |
||||||
|
|
||||||
|
export function getPlayerState(descriptor: string): VideoPlayerState { |
||||||
|
const state = _players.get(descriptor); |
||||||
|
if (!state) throw new Error("invalid descriptor or has been unregistered"); |
||||||
|
return state; |
||||||
|
} |
@ -0,0 +1,28 @@ |
|||||||
|
export type VideoPlayerEvent = "progress"; |
||||||
|
|
||||||
|
function createEventString(id: string, event: VideoPlayerEvent): string { |
||||||
|
return `_vid:::${id}:::${event}`; |
||||||
|
} |
||||||
|
|
||||||
|
export function sendEvent<T>(id: string, event: VideoPlayerEvent, data: T) { |
||||||
|
const evObj = new CustomEvent(createEventString(id, event), { |
||||||
|
detail: data, |
||||||
|
}); |
||||||
|
document.dispatchEvent(evObj); |
||||||
|
} |
||||||
|
|
||||||
|
export function listenEvent<T>( |
||||||
|
id: string, |
||||||
|
event: VideoPlayerEvent, |
||||||
|
cb: (data: T) => void |
||||||
|
) { |
||||||
|
document.addEventListener<any>(createEventString(id, event), cb); |
||||||
|
} |
||||||
|
|
||||||
|
export function unlistenEvent<T>( |
||||||
|
id: string, |
||||||
|
event: VideoPlayerEvent, |
||||||
|
cb: (data: T) => void |
||||||
|
) { |
||||||
|
document.removeEventListener<any>(createEventString(id, event), cb); |
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
import { |
||||||
|
createContext, |
||||||
|
ReactNode, |
||||||
|
useContext, |
||||||
|
useEffect, |
||||||
|
useState, |
||||||
|
} from "react"; |
||||||
|
import { registerVideoPlayer, unregisterVideoPlayer } from "./init"; |
||||||
|
|
||||||
|
const VideoPlayerContext = createContext<string>(""); |
||||||
|
|
||||||
|
export function VideoPlayerContextProvider(props: { children: ReactNode }) { |
||||||
|
const [id, setId] = useState<string | null>(null); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
const vidId = registerVideoPlayer(); |
||||||
|
setId(vidId); |
||||||
|
|
||||||
|
return () => { |
||||||
|
unregisterVideoPlayer(vidId); |
||||||
|
}; |
||||||
|
}, [setId]); |
||||||
|
|
||||||
|
if (!id) return null; |
||||||
|
|
||||||
|
return ( |
||||||
|
<VideoPlayerContext.Provider value={id}> |
||||||
|
{props.children} |
||||||
|
</VideoPlayerContext.Provider> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export function useVideoPlayerDescriptor(): string { |
||||||
|
const id = useContext(VideoPlayerContext); |
||||||
|
return id; |
||||||
|
} |
@ -0,0 +1,44 @@ |
|||||||
|
import { nanoid } from "nanoid"; |
||||||
|
import { _players } from "./cache"; |
||||||
|
import { VideoPlayerState } from "./types"; |
||||||
|
|
||||||
|
function initPlayer(): VideoPlayerState { |
||||||
|
return { |
||||||
|
isPlaying: false, |
||||||
|
isPaused: true, |
||||||
|
isFullscreen: false, |
||||||
|
isFocused: false, |
||||||
|
isLoading: false, |
||||||
|
isSeeking: false, |
||||||
|
isFirstLoading: true, |
||||||
|
time: 0, |
||||||
|
duration: 0, |
||||||
|
volume: 0, |
||||||
|
buffered: 0, |
||||||
|
pausedWhenSeeking: false, |
||||||
|
hasInitialized: false, |
||||||
|
leftControlHovering: false, |
||||||
|
hasPlayedOnce: false, |
||||||
|
error: null, |
||||||
|
popout: null, |
||||||
|
seasonData: { |
||||||
|
isSeries: false, |
||||||
|
}, |
||||||
|
canAirplay: false, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
export function registerVideoPlayer(): string { |
||||||
|
const id = nanoid(); |
||||||
|
|
||||||
|
if (_players.has(id)) { |
||||||
|
throw new Error("duplicate id"); |
||||||
|
} |
||||||
|
|
||||||
|
_players.set(id, initPlayer()); |
||||||
|
return id; |
||||||
|
} |
||||||
|
|
||||||
|
export function unregisterVideoPlayer(id: string) { |
||||||
|
if (_players.has(id)) _players.delete(id); |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
export type VideoPlayerStateProvider = { |
||||||
|
pause: () => void; |
||||||
|
play: () => void; |
||||||
|
providerStart: () => { |
||||||
|
destroy: () => void; |
||||||
|
}; |
||||||
|
}; |
@ -0,0 +1,40 @@ |
|||||||
|
import { getPlayerState } from "../cache"; |
||||||
|
import { VideoPlayerStateProvider } from "./providerTypes"; |
||||||
|
|
||||||
|
export function createVideoStateProvider( |
||||||
|
descriptor: string, |
||||||
|
player: HTMLVideoElement |
||||||
|
): VideoPlayerStateProvider { |
||||||
|
const state = getPlayerState(descriptor); |
||||||
|
|
||||||
|
return { |
||||||
|
play() { |
||||||
|
player.play(); |
||||||
|
}, |
||||||
|
pause() { |
||||||
|
player.pause(); |
||||||
|
}, |
||||||
|
providerStart() { |
||||||
|
// TODO reactivity through events
|
||||||
|
const pause = () => { |
||||||
|
state.isPaused = true; |
||||||
|
state.isPlaying = false; |
||||||
|
}; |
||||||
|
const playing = () => { |
||||||
|
state.isPaused = false; |
||||||
|
state.isPlaying = true; |
||||||
|
state.isLoading = false; |
||||||
|
state.hasPlayedOnce = true; |
||||||
|
}; |
||||||
|
|
||||||
|
player.addEventListener("pause", pause); |
||||||
|
player.addEventListener("playing", playing); |
||||||
|
return { |
||||||
|
destroy: () => { |
||||||
|
player.removeEventListener("pause", pause); |
||||||
|
player.removeEventListener("playing", playing); |
||||||
|
}, |
||||||
|
}; |
||||||
|
}, |
||||||
|
}; |
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
export type VideoPlayerState = { |
||||||
|
isPlaying: boolean; |
||||||
|
isPaused: boolean; |
||||||
|
isSeeking: boolean; |
||||||
|
isLoading: boolean; |
||||||
|
isFirstLoading: boolean; |
||||||
|
isFullscreen: boolean; |
||||||
|
time: number; |
||||||
|
duration: number; |
||||||
|
volume: number; |
||||||
|
buffered: number; |
||||||
|
pausedWhenSeeking: boolean; |
||||||
|
hasInitialized: boolean; |
||||||
|
leftControlHovering: boolean; |
||||||
|
hasPlayedOnce: boolean; |
||||||
|
popout: string | null; |
||||||
|
isFocused: boolean; |
||||||
|
seasonData: { |
||||||
|
isSeries: boolean; |
||||||
|
current?: { |
||||||
|
episodeId: string; |
||||||
|
seasonId: string; |
||||||
|
}; |
||||||
|
seasons?: { |
||||||
|
id: string; |
||||||
|
number: number; |
||||||
|
title: string; |
||||||
|
episodes?: { id: string; number: number; title: string }[]; |
||||||
|
}[]; |
||||||
|
}; |
||||||
|
error: null | { |
||||||
|
name: string; |
||||||
|
description: string; |
||||||
|
}; |
||||||
|
canAirplay: boolean; |
||||||
|
}; |
Loading…
Reference in new issue