42 changed files with 222 additions and 3 deletions
@ -0,0 +1,19 @@
@@ -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 @@
@@ -1,12 +1,12 @@
|
||||
import { useGoBack } from "@/hooks/useGoBack"; |
||||
import { useVolumeControl } from "@/hooks/useVolumeToggle"; |
||||
import { forwardRef, useContext, useEffect, useRef } from "react"; |
||||
import { VideoErrorBoundary } from "./parts/VideoErrorBoundary"; |
||||
import { VideoErrorBoundary } from "../../components/video/parts/VideoErrorBoundary"; |
||||
import { |
||||
useVideoPlayerState, |
||||
VideoPlayerContext, |
||||
VideoPlayerContextProvider, |
||||
} from "./VideoContext"; |
||||
} from "../../video/components./../components/video/VideoContext"; |
||||
|
||||
export interface VideoPlayerProps { |
||||
autoPlay?: boolean; |
@ -0,0 +1,9 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,7 @@
|
||||
export type VideoPlayerStateProvider = { |
||||
pause: () => void; |
||||
play: () => void; |
||||
providerStart: () => { |
||||
destroy: () => void; |
||||
}; |
||||
}; |
@ -0,0 +1,40 @@
@@ -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 @@
@@ -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