diff --git a/src/api/models/Image.ts b/src/api/models/Image.ts index 351160b4f73647ca78f4cfc929859d9e40101c79..62511e9938fe1e5a324b9e85abcaa812c5ec94f3 100644 --- a/src/api/models/Image.ts +++ b/src/api/models/Image.ts @@ -4,3 +4,24 @@ export interface Image { language: string, src: string } + +export function findImage( + images: Image[], + type: string, + languages: (string | null)[] +): Image | null { + const imageList = images.filter(it => it.kind === type) + + for (let language of languages) { + const image = imageList.find(it => it.language === language); + if (image) { + return image; + } + } + + if (languages.includes(null) && imageList.length) { + return imageList[0]; + } + + return null; +} diff --git a/src/routes/main/MediaEntry.tsx b/src/routes/main/MediaEntry.tsx index 5b5402f67abc16a4a1c2ea23b94a0a4821092a71..ad87bae0233231eca74e34138c1ce15e59764725 100644 --- a/src/routes/main/MediaEntry.tsx +++ b/src/routes/main/MediaEntry.tsx @@ -5,33 +5,12 @@ import {createUseStyles} from "react-jss"; import {useLocale} from "../../util/locale/LocalizedContext"; import {useShowEpisodes} from "../../api/ApiHooks"; import {MediaEpisode} from "./MediaEpisode"; -import {Image} from "../../api/models/Image"; +import {findImage} from "../../api/models/Image"; export interface Props { item: Content } -function findImage( - images: Image[], - type: string, - languages: (string | null)[] -): Image | null { - const imageList = images.filter(it => it.kind === type) - - for (let language of languages) { - const image = imageList.find(it => it.language === language); - if (image) { - return image; - } - } - - if (languages.includes(null) && imageList.length) { - return imageList[0]; - } - - return null; -} - export function MediaEntry( {item}: Props ) { @@ -43,10 +22,11 @@ export function MediaEntry( const title = getLocalizedName(item, locale); const description = getLocalizedDescription(item, locale); const rating = getLocalizedRating(item, locale); - const [logo, poster, backdrop] = useMemo(() => [ + const [logo, poster, backdrop, still] = useMemo(() => [ findImage(item.images, "logo", [locale.language]), findImage(item.images, "poster", [locale.language, item.originalLanguage, null]), - findImage(item.images, "backdrop", [locale.language, item.originalLanguage, null]) + findImage(item.images, "backdrop", [locale.language, item.originalLanguage, null]), + findImage(item.images, "still", [null, locale.language, item.originalLanguage]) ], [item.images, item.originalLanguage, locale.language]); return ( @@ -96,6 +76,13 @@ export function MediaEntry( src={backdrop.src} /> )} + {still && ( + <img + className={classes.poster} + alt={`Movie still for ${title?.name}`} + src={still.src} + /> + )} </div> ) } diff --git a/src/routes/main/MediaEpisode.tsx b/src/routes/main/MediaEpisode.tsx index ec815e17fc238380bf509f2e7301faf0b96f0120..c171c083df5a1f88dfc5d666f65bc6c1fd5df514 100644 --- a/src/routes/main/MediaEpisode.tsx +++ b/src/routes/main/MediaEpisode.tsx @@ -2,6 +2,9 @@ import {Instalment} from "../../api/models/Instalment"; import {getLocalizedDescription, getLocalizedName} from "../../api/models/Content"; import {Link} from "react-router-dom"; import {useLocale} from "../../util/locale/LocalizedContext"; +import {useMemo} from "react"; +import {findImage} from "../../api/models/Image"; +import {createUseStyles} from "react-jss"; export interface Props { item: Instalment, @@ -9,9 +12,16 @@ export interface Props { } export function MediaEpisode({item, disabled}: Props) { + const classes = useStyles(); const locale = useLocale(); const episodeTitle = getLocalizedName(item.content, locale); const episodeDescription = getLocalizedDescription(item.content, locale); + const [backdrop, still] = useMemo(() => [ + findImage(item.content.images, "backdrop", [locale.language, item.content.originalLanguage, null]), + findImage(item.content.images, "still", [null, locale.language, item.content.originalLanguage]) + ], [item.content.images, item.content.originalLanguage, locale.language]); + + const episodeImage = still || backdrop; return ( <li> @@ -21,6 +31,13 @@ export function MediaEpisode({item, disabled}: Props) { <p><strong>{episodeTitle?.name}</strong></p> <p><strong>{episodeDescription?.tagline}</strong></p> <p>{episodeDescription?.overview}</p> + {episodeImage && ( + <img + className={classes.poster} + alt={episodeTitle?.name} + src={episodeImage.src} + /> + )} {disabled ? ( <p><u>Play</u></p> ) : ( @@ -29,3 +46,10 @@ export function MediaEpisode({item, disabled}: Props) { </li> ) } + +const useStyles = createUseStyles({ + poster: { + maxWidth: "20rem", + maxHeight: "20rem", + } +}); diff --git a/src/routes/player/Player.tsx b/src/routes/player/Player.tsx index 40f04b583cf2d862ff4a2c725c939b2bf16eabd9..b6936f07f8579d6cd2a31c3766885ac02b41aeec 100644 --- a/src/routes/player/Player.tsx +++ b/src/routes/player/Player.tsx @@ -1,4 +1,4 @@ -import {Fragment, useState} from "react"; +import {ChangeEvent, Fragment, useCallback, useState} from "react"; import {createUseStyles} from "react-jss"; import {Link} from "react-router-dom"; import {ContentMeta} from "../../api/models/dto/ContentMeta"; @@ -6,8 +6,6 @@ import {Media} from "../../api/models/Media"; import {getLocalizedDescription, getLocalizedName, getLocalizedRating} from "../../api/models/Content"; import {formatDuration} from "../../util/formatDuration"; import {useLocale} from "../../util/locale/LocalizedContext"; -import {useDuration} from "../../util/media/useDuration"; -import {usePosition} from "../../util/media/usePosition"; import {SeekBar} from "./SeekBar"; import {VideoElement} from "./video/VideoElement"; import {PlayerApi} from "./video/PlayerApi"; @@ -20,6 +18,8 @@ import {SubtitleRenderer} from "./subtitles/SubtitleRenderer"; import {useShowEpisodes} from "../../api/ApiHooks"; import {MediaEpisode} from "../main/MediaEpisode"; import {usePaused} from "../../util/media/usePaused"; +import {useCurrentTime} from "../../util/media/useCurrentTime"; +import {useDuration} from "../../util/media/useDuration"; interface Props { meta: ContentMeta, @@ -27,8 +27,11 @@ interface Props { } export function Player( - {meta, media}: Props + props: Props ) { + console.log("Rendering Player") + const {meta, media} = props; + const {content, instalment} = meta; const classes = useStyles(); const locale = useLocale(); @@ -44,18 +47,43 @@ export function Player( const [playerApi, setPlayerApi] = useState<PlayerApi | null>(null); const [mousePosition, setMousePosition] = useState<MousePosition | null>(null); + const [volume, setVolume] = useState<number>(1); const paused = usePaused(playerApi); - const position = usePosition(playerApi); + const currentTime = useCurrentTime(playerApi) const duration = useDuration(playerApi); const [audioTracks, currentTrack, setCurrentTrack] = useAudioTracks(playerApi); useDebugInfo("player", playerApi); useDebugInfo("content", content); useDebugInfo("subtitle", subtitle); - useDebugInfo("position", position); + useDebugInfo("currentTime", currentTime); useDebugInfo("duration", duration); useDebugInfo("audioTracks", audioTracks); + const onPause = useCallback(() => { + if (playerApi) { + playerApi.pause() + } + }, [playerApi]); + + const onPlay = useCallback(() => { + if (playerApi) { + playerApi.play() + } + }, [playerApi]); + + const onFastForward = useCallback(() => { + if (playerApi) { + playerApi.setCurrentTime(playerApi.getCurrentTime() + 10) + } + }, [playerApi]); + + const onRewind = useCallback(() => { + if (playerApi) { + playerApi.setCurrentTime(playerApi.getCurrentTime() - 10) + } + }, [playerApi]); + return ( <VideoProvider value={playerApi?.getVideoElement() || null}> <div> @@ -73,6 +101,7 @@ export function Player( <ul> {relatedEpisodes.map(episode => <MediaEpisode + key={episode.content.ids.uuid} item={episode} disabled={episode.content.ids.uuid === content.ids.uuid} /> @@ -98,20 +127,42 @@ export function Player( </div> </div> <p style={{fontVariant: "tabular-nums"}}> - {formatDuration(mousePosition ? (mousePosition.relative * duration) : position)} / {formatDuration(duration)} + {formatDuration(mousePosition ? (mousePosition.relative * duration) : currentTime)} / {formatDuration(duration)} </p> <button - onClick={playerApi?.play} + onClick={onPlay} disabled={!paused} > Play </button> <button - onClick={playerApi?.pause} + onClick={onPause} disabled={paused} > Pause </button> + <button + onClick={onRewind} + > + Rewind + </button> + <button + onClick={onFastForward} + > + Fast Forward + </button> + <p> + <input + type="range" + min="0" + max="100" + value={volume * 100} + onChange={(event: ChangeEvent<HTMLInputElement>) => { + setVolume(event.target.valueAsNumber / 100); + playerApi?.setVolume(event.target.valueAsNumber / 100) + }} + /> + </p> <h3>Audio</h3> <ul> {audioTracks.map(track => ( @@ -151,7 +202,7 @@ export function Player( mousePosition={mousePosition} setMousePosition={setMousePosition} duration={duration} - position={position} + position={currentTime} /> </div> </VideoProvider> diff --git a/src/routes/player/PlayerPage.tsx b/src/routes/player/PlayerPage.tsx index d65c9eb569a12ba6240c949dfaea02aadec609e4..6feaaad445eb92903bb871f1f616b98c30b572cc 100644 --- a/src/routes/player/PlayerPage.tsx +++ b/src/routes/player/PlayerPage.tsx @@ -6,6 +6,8 @@ import {PlayerError} from "./PlayerError"; import {PlayerLoading} from "./PlayerLoading"; export function PlayerPage() { + console.log("Rendering player page"); + const content = useCurrentContent(); const playabilityRating = usePlayabilityRating(); diff --git a/src/routes/player/PreviewBar.tsx b/src/routes/player/PreviewBar.tsx index a3dee23dee95b628e9848d2b219021a5cfdc3a3b..58dd9fcef9a836b05b1b4333a6641a94a16a3a84 100644 --- a/src/routes/player/PreviewBar.tsx +++ b/src/routes/player/PreviewBar.tsx @@ -13,6 +13,7 @@ interface Props { } export function PreviewBar({previewSrc, duration, position, hidden}: Props) { + //console.log("Rendering PreviewBar") const classes = useStyles(); const [previewTrack, setPreviewTrack] = useState<HTMLTrackElement | null>(null); diff --git a/src/routes/player/PreviewViewer.tsx b/src/routes/player/PreviewViewer.tsx index 4649bb42be22116f6824ff831e09ab2e5a06cf66..71ba1a376af67c5640252afdd4d599d402f29493 100644 --- a/src/routes/player/PreviewViewer.tsx +++ b/src/routes/player/PreviewViewer.tsx @@ -12,6 +12,8 @@ interface Props { } export function PreviewViewer({previewTrack, position}: Props) { + //console.log("Rendering PreviewViewer#1") + const classes = useStyles(); const cues = useTextTrackCues(previewTrack); @@ -25,16 +27,19 @@ export function PreviewViewer({previewTrack, position}: Props) { url.hash = ""; return url.toString(); }))), [cues, previewTrack]); - return useMemo(() => ( - <Fragment> - <HeadPortal> - {sources?.map(it => ( - <link key={it} rel="preload" href={it} as="image" crossOrigin="anonymous"/> - ))} - </HeadPortal> - <img alt="" className={classes.preview} src={sprite || undefined}/> - </Fragment> - ), [classes.preview, sources, sprite]); + return useMemo(() => { + //console.log("Rendering PreviewViewer#2") + return ( + <Fragment> + <HeadPortal> + {sources?.map(it => ( + <link key={it} rel="preload" href={it} as="image" crossOrigin="anonymous"/> + ))} + </HeadPortal> + <img alt="" className={classes.preview} src={sprite || undefined}/> + </Fragment> + ) + }, [classes.preview, sources, sprite]); } const useStyles = createUseStyles({ diff --git a/src/routes/player/SeekBar.tsx b/src/routes/player/SeekBar.tsx index 2a2de12f182732061cfa7e80a36828be584d68bb..315d531b6ac5224ec7ef33dba928ef7e0a4f47a2 100644 --- a/src/routes/player/SeekBar.tsx +++ b/src/routes/player/SeekBar.tsx @@ -42,8 +42,11 @@ export function SeekBar({videoApi, previewSrc, mousePosition, setMousePosition, const onMouseMove = useCallback((event: MouseEvent<HTMLDivElement>) => { const position = getMousePosition(event); - setMousePosition(position); - }, [setMousePosition]); + const visible = position !== null; + if (visible || isVisible !== visible) { + setMousePosition(position); + } + }, [isVisible, setMousePosition]); const seekHead = useMemo(() => ( <div diff --git a/src/routes/player/subtitles/AssRenderer.tsx b/src/routes/player/subtitles/SsaRenderer.tsx similarity index 89% rename from src/routes/player/subtitles/AssRenderer.tsx rename to src/routes/player/subtitles/SsaRenderer.tsx index 037280b941a8a04deb0f9a1e1a0e1b951a4701ca..f64d31a9137dd65e9f99d2506bb09c0ca8cc0bee 100644 --- a/src/routes/player/subtitles/AssRenderer.tsx +++ b/src/routes/player/subtitles/SsaRenderer.tsx @@ -1,6 +1,6 @@ import {useEffect, useState} from "react"; import {Subtitle} from "../../../api/models/Subtitle"; -import SubtitlesOctopus from "../../../util/subtitles/ass/SubtitlesOctopus"; +import SubtitlesOctopus from "../../../util/subtitles/ssa/SubtitlesOctopus"; interface Props { videoElement: HTMLVideoElement | null, @@ -9,7 +9,7 @@ interface Props { className?: string, } -export function AssRenderer( +export function SsaRenderer( {videoElement, subtitle, className}: Props ): JSX.Element { const [subtitleCanvas, setSubtitleCanvas] = useState<HTMLCanvasElement | null>(null); @@ -24,7 +24,7 @@ export function AssRenderer( legacyWorkerUrl: '/assets/js/subtitles-octopus-worker-legacy.js', lossyRender: true, onError: (error: any) => { - console.log("Error rendering ass subtitles:", error); + console.log("Error rendering SSA subtitles:", error); }, onReady: () => { instance.setCurrentTime(videoElement?.currentTime); diff --git a/src/routes/player/subtitles/SubtitleRenderer.tsx b/src/routes/player/subtitles/SubtitleRenderer.tsx index 4439f3bcd99a1ab31015ff8f73c9b6f2defb2dee..8c3a2273c64c3f4d333e3c23f41760d4e4ffa3d6 100644 --- a/src/routes/player/subtitles/SubtitleRenderer.tsx +++ b/src/routes/player/subtitles/SubtitleRenderer.tsx @@ -1,7 +1,7 @@ import {Subtitle} from "../../../api/models/Subtitle"; import {Fragment} from "react"; import {TtmlRenderer} from "./TtmlRenderer"; -import {AssRenderer} from "./AssRenderer"; +import {SsaRenderer} from "./SsaRenderer"; interface Props { videoElement: HTMLVideoElement | null, @@ -17,7 +17,7 @@ export function SubtitleRenderer( case "ttml": return (<TtmlRenderer {...props} />); case "ass": - return (<AssRenderer {...props} />); + return (<SsaRenderer {...props} />); default: return (<Fragment/>); } diff --git a/src/routes/player/video/DashVideoElement.tsx b/src/routes/player/video/DashVideoElement.tsx index 3f7c0a63a98b5b9e314b4ec6f33f802906e0147a..227ec5b82d4749610ee3678ed0dca2184c4e186a 100644 --- a/src/routes/player/video/DashVideoElement.tsx +++ b/src/routes/player/video/DashVideoElement.tsx @@ -1,4 +1,4 @@ -import {forwardRef, PropsWithChildren, useEffect, useImperativeHandle, useMemo, useState,} from "react"; +import {forwardRef, PropsWithChildren, useEffect, useImperativeHandle, useRef, useState,} from "react"; import {Media} from "../../../api/models/Media"; import dashjs, {MediaInfo} from "dashjs"; import {PlayerApi} from "./PlayerApi"; @@ -13,33 +13,35 @@ export const DashVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>( {media, autoPlay, className, children}, ref ) { - const player = useMemo(() => dashjs.MediaPlayer().create(), []); + const playerContainer = useRef<dashjs.MediaPlayerClass>(dashjs.MediaPlayer().create()); + const player = playerContainer.current; const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(null); useImperativeHandle(ref, () => ({ - METADATA_EVENT: dashjs.MediaPlayer.events.PLAYBACK_METADATA_LOADED, + DURATIONCHANGE_EVENT: dashjs.MediaPlayer.events.PLAYBACK_METADATA_LOADED, + CANPLAY_EVENT: dashjs.MediaPlayer.events.PLAYBACK_METADATA_LOADED, TIMECHANGE_EVENT: dashjs.MediaPlayer.events.PLAYBACK_TIME_UPDATED, PLAY_EVENT: dashjs.MediaPlayer.events.PLAYBACK_PLAYING, PAUSE_EVENT: dashjs.MediaPlayer.events.PLAYBACK_PAUSED, - play() { + play(): void { if (!player.isReady()) { return; } player.play(); }, - pause() { + pause(): void { if (!player.isReady()) { return; } player.pause(); }, - isPaused() { + isPaused(): boolean { if (!player.isReady()) { return true; } return player.isPaused(); }, - setPlaybackRate(value: number) { + setPlaybackRate(value: number): void { if (!player.isReady()) { return; } @@ -51,12 +53,26 @@ export const DashVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>( } return player.getPlaybackRate(); }, - setCurrentTime(value: number) { + setCurrentTime(value: number): void { if (!player.isReady()) { return; } player.seek(value); }, + setVolume(value: number): void { + if (!videoElement) { + return; + } + + videoElement.volume = value; + }, + getVolume(): number { + if (!videoElement) { + return 0; + } + + return videoElement.volume; + }, getCurrentTime(): number { if (!player.isReady()) { return 0; @@ -90,7 +106,7 @@ export const DashVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>( return player.getCurrentTrackFor("audio"); }, - setCurrentAudioTrack(track: MediaInfo) { + setCurrentAudioTrack(track: MediaInfo): void { if (!player.isReady()) { return; } @@ -99,10 +115,10 @@ export const DashVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>( } player.setCurrentTrack(track); }, - addEventListener(event: string, listener: () => void) { + addEventListener(event: string, listener: () => void): void { player.on(event, listener); }, - removeEventListener(event: string, listener: () => void) { + removeEventListener(event: string, listener: () => void): void { player.off(event, listener); }, debug(): any { diff --git a/src/routes/player/video/PlayerApi.ts b/src/routes/player/video/PlayerApi.ts index 48ff51b811374f3b9c685adc868aa91d5726529c..728a0c0a4600fc8aa3e07e3044cccb860c76a520 100644 --- a/src/routes/player/video/PlayerApi.ts +++ b/src/routes/player/video/PlayerApi.ts @@ -1,7 +1,8 @@ import {MediaInfo} from "dashjs"; export interface PlayerApi { - METADATA_EVENT: string + DURATIONCHANGE_EVENT: string + CANPLAY_EVENT: string TIMECHANGE_EVENT: string PLAY_EVENT: string PAUSE_EVENT: string @@ -22,6 +23,10 @@ export interface PlayerApi { getDuration(): number + setVolume(value: number): void + + getVolume(): number + canPlay(): boolean getAudioTracks(): MediaInfo[] diff --git a/src/routes/player/video/RawVideoElement.tsx b/src/routes/player/video/RawVideoElement.tsx index 5b1765c68459a00e79f10e42395a604958ced1a3..7a2955ced200b6d58f7917dd14492cde9a93741a 100644 --- a/src/routes/player/video/RawVideoElement.tsx +++ b/src/routes/player/video/RawVideoElement.tsx @@ -16,18 +16,20 @@ export const RawVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>(f const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(null); useImperativeHandle(ref, () => ({ - METADATA_EVENT: "loadedmetadata", + DURATIONCHANGE_EVENT: "durationchange", + CANPLAY_EVENT: "canplay", TIMECHANGE_EVENT: "timeupdate", PLAY_EVENT: "play", PAUSE_EVENT: "pause", - play() { + + play(): void { if (!videoElement) { return; } videoElement.play(); }, - pause() { + pause(): void { if (!videoElement) { return; } @@ -41,7 +43,7 @@ export const RawVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>(f return videoElement.paused; }, - setPlaybackRate(value: number) { + setPlaybackRate(value: number): void { if (!videoElement) { return; } @@ -55,7 +57,21 @@ export const RawVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>(f return videoElement.playbackRate; }, - setCurrentTime(value: number) { + setVolume(value: number): void { + if (!videoElement) { + return; + } + + videoElement.volume = value; + }, + getVolume(): number { + if (!videoElement) { + return 0; + } + + return videoElement.volume; + }, + setCurrentTime(value: number): void { if (!videoElement) { return; } @@ -91,14 +107,14 @@ export const RawVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>(f getCurrentAudioTrack(): MediaInfo | null { return null; }, - addEventListener(event: string, listener: () => void) { + addEventListener(event: string, listener: () => void): void { if (!videoElement) { return; } videoElement.addEventListener(event, listener); }, - removeEventListener(event: string, listener: () => void) { + removeEventListener(event: string, listener: () => void): void { if (!videoElement) { return; } diff --git a/src/util/media/useAudioTracks.ts b/src/util/media/useAudioTracks.ts index ca9ee8f924f42a5c76509fb3bf470d3ca83ba58f..c08a1af240214530a5eb6095fbe10dbde39b19d5 100644 --- a/src/util/media/useAudioTracks.ts +++ b/src/util/media/useAudioTracks.ts @@ -26,9 +26,9 @@ export function useAudioTracks(video: PlayerApi | null): setAudioTracks(video.getAudioTracks()); setCurrentTrack(video.getCurrentAudioTrack); } else { - video?.addEventListener(video.METADATA_EVENT, listener) + video?.addEventListener(video.CANPLAY_EVENT, listener) return () => { - video?.removeEventListener(video.METADATA_EVENT, listener) + video?.removeEventListener(video.CANPLAY_EVENT, listener) } } }, [listener, video]); diff --git a/src/util/media/usePosition.ts b/src/util/media/useCurrentTime.ts similarity index 91% rename from src/util/media/usePosition.ts rename to src/util/media/useCurrentTime.ts index d8a3b8a1be664463eef7493b6e0598ce6bf78c6f..ead1f4348973597d7d1abe08a1dd4ab454086464 100644 --- a/src/util/media/usePosition.ts +++ b/src/util/media/useCurrentTime.ts @@ -1,7 +1,7 @@ import {useEffect, useState} from "react"; import {PlayerApi} from "../../routes/player/video/PlayerApi"; -export const usePosition = (video: PlayerApi | null) => { +export const useCurrentTime = (video: PlayerApi | null) => { const [position, setPosition] = useState<number>(0); useEffect(() => { if (video !== null) { diff --git a/src/util/media/useDuration.ts b/src/util/media/useDuration.ts index c0259a32e85c2be667c7ae9cf0ec6d8a5b14f700..52bb3c775695c2eb02b5d8545e93d5f29bdca9fc 100644 --- a/src/util/media/useDuration.ts +++ b/src/util/media/useDuration.ts @@ -1,24 +1,14 @@ -import {useEffect, useState} from "react"; import {PlayerApi} from "../../routes/player/video/PlayerApi"; +import {useVideoApi} from "./useVideoApi"; +import {useCallback} from "react"; export const useDuration = (video: PlayerApi | null) => { - const [duration, setDuration] = useState<number>(0); - useEffect(() => { - if (video !== null) { - if (video.canPlay()) { - setDuration(video.getDuration()); - } else { - const listener = () => { - window.requestAnimationFrame(() => { - setDuration(video.getDuration()); - }) - }; - video.addEventListener(video.METADATA_EVENT, listener) - return () => { - video.removeEventListener(video.METADATA_EVENT, listener) - } - } - } - }, [video]); - return duration; + const callback = useCallback((videoApi: PlayerApi) => + videoApi.getDuration(), []); + return useVideoApi<number>( + [video?.DURATIONCHANGE_EVENT], + video, + 0, + callback + ) } diff --git a/src/util/media/usePaused.ts b/src/util/media/usePaused.ts index 019e98a6df5d97c2bce196a3be1e9b21b1943291..4f3e62f991dbd3c111f10948a7dedf536727e233 100644 --- a/src/util/media/usePaused.ts +++ b/src/util/media/usePaused.ts @@ -1,22 +1,14 @@ -import {useEffect, useState} from "react"; import {PlayerApi} from "../../routes/player/video/PlayerApi"; +import {useVideoApi} from "./useVideoApi"; +import {useCallback} from "react"; export const usePaused = (video: PlayerApi | null) => { - const [paused, setPaused] = useState<boolean>(false); - useEffect(() => { - if (video !== null) { - const listener = () => { - window.requestAnimationFrame(() => { - setPaused(video.isPaused()); - }) - }; - video.addEventListener(video.PLAY_EVENT, listener) - video.addEventListener(video.PAUSE_EVENT, listener) - return () => { - video.removeEventListener(video.PLAY_EVENT, listener) - video.removeEventListener(video.PAUSE_EVENT, listener) - } - } - }, [video]); - return paused; + const callback = useCallback((videoApi: PlayerApi) => + videoApi.isPaused(), []); + return useVideoApi<boolean>( + [video?.PLAY_EVENT, video?.PAUSE_EVENT], + video, + true, + callback + ); } diff --git a/src/util/media/usePlayerState.ts b/src/util/media/usePlayerState.ts new file mode 100644 index 0000000000000000000000000000000000000000..620fb5605106d8532fb76fd9a4cf2591f5d041a5 --- /dev/null +++ b/src/util/media/usePlayerState.ts @@ -0,0 +1,30 @@ +import {PlayerApi} from "../../routes/player/video/PlayerApi"; +import {useVideoApi} from "./useVideoApi"; + +interface PlayerState { + currentTime: number, + duration: number, + paused: boolean, +} + +export const usePlayerState = (video: PlayerApi | null) => useVideoApi<PlayerState>( + [ + video?.DURATIONCHANGE_EVENT, + video?.TIMECHANGE_EVENT, + video?.PLAY_EVENT, + video?.PAUSE_EVENT, + ], + video, + { + currentTime: 0, + duration: 0, + paused: true, + }, + (videoApi: PlayerApi) => { + return { + currentTime: videoApi.getCurrentTime(), + duration: videoApi.getDuration(), + paused: videoApi.isPaused(), + } + } +) diff --git a/src/util/media/useVideoApi.ts b/src/util/media/useVideoApi.ts new file mode 100644 index 0000000000000000000000000000000000000000..b6ede3138df5baf98e4be0e160a785e1b8c6ea0d --- /dev/null +++ b/src/util/media/useVideoApi.ts @@ -0,0 +1,41 @@ +import {useEffect, useState} from "react"; +import {PlayerApi} from "../../routes/player/video/PlayerApi"; + +export function useVideoApi<T>( + events: (string|undefined|null)[], + video: PlayerApi | null, + initial: T, + mapper: (video: PlayerApi) => T +): T { + const [result, setResult] = useState<T>(initial); + useEffect(() => { + if (video !== null) { + let requestFrame: number | null = null; + const listener = () => { + if (requestFrame !== null) { + window.cancelAnimationFrame(requestFrame) + } + requestFrame = window.requestAnimationFrame(() => { + setResult(mapper(video)); + }) + }; + listener(); + for (let event in events) { + if (event !== null && event !== undefined) { + video.addEventListener(event, listener); + } + } + return () => { + if (requestFrame) { + window.cancelAnimationFrame(requestFrame); + } + for (let event in events) { + if (event !== null && event !== undefined) { + video.removeEventListener(event, listener); + } + } + } + } + }, [events, mapper, video]); + return result; +} diff --git a/src/util/subtitles/ass/SubtitlesOctopus.ts b/src/util/subtitles/ssa/SubtitlesOctopus.ts similarity index 100% rename from src/util/subtitles/ass/SubtitlesOctopus.ts rename to src/util/subtitles/ssa/SubtitlesOctopus.ts diff --git a/src/util/subtitles/ass/SubtitlesOctopusOptions.ts b/src/util/subtitles/ssa/SubtitlesOctopusOptions.ts similarity index 100% rename from src/util/subtitles/ass/SubtitlesOctopusOptions.ts rename to src/util/subtitles/ssa/SubtitlesOctopusOptions.ts diff --git a/src/util/subtitles/ass/browserHasAlphaBug.ts b/src/util/subtitles/ssa/browserHasAlphaBug.ts similarity index 100% rename from src/util/subtitles/ass/browserHasAlphaBug.ts rename to src/util/subtitles/ssa/browserHasAlphaBug.ts diff --git a/src/util/subtitles/ass/browserSupportsWebAssembly.ts b/src/util/subtitles/ssa/browserSupportsWebAssembly.ts similarity index 100% rename from src/util/subtitles/ass/browserSupportsWebAssembly.ts rename to src/util/subtitles/ssa/browserSupportsWebAssembly.ts