From 8eb7a305759661e8786f30cd558e0d7e6474f38f Mon Sep 17 00:00:00 2001 From: Janne Koschinski <janne@kuschku.de> Date: Fri, 15 Jan 2021 15:27:00 +0100 Subject: [PATCH] Fix unnecessary re-renders --- src/api/models/Image.ts | 21 ++++++ src/routes/main/MediaEntry.tsx | 35 +++------ src/routes/main/MediaEpisode.tsx | 24 +++++++ src/routes/player/Player.tsx | 71 ++++++++++++++++--- src/routes/player/PlayerPage.tsx | 2 + src/routes/player/PreviewBar.tsx | 1 + src/routes/player/PreviewViewer.tsx | 25 ++++--- src/routes/player/SeekBar.tsx | 7 +- .../{AssRenderer.tsx => SsaRenderer.tsx} | 6 +- .../player/subtitles/SubtitleRenderer.tsx | 4 +- src/routes/player/video/DashVideoElement.tsx | 38 +++++++--- src/routes/player/video/PlayerApi.ts | 7 +- src/routes/player/video/RawVideoElement.tsx | 30 ++++++-- src/util/media/useAudioTracks.ts | 4 +- .../{usePosition.ts => useCurrentTime.ts} | 2 +- src/util/media/useDuration.ts | 30 +++----- src/util/media/usePaused.ts | 28 +++----- src/util/media/usePlayerState.ts | 30 ++++++++ src/util/media/useVideoApi.ts | 41 +++++++++++ .../{ass => ssa}/SubtitlesOctopus.ts | 0 .../{ass => ssa}/SubtitlesOctopusOptions.ts | 0 .../{ass => ssa}/browserHasAlphaBug.ts | 0 .../browserSupportsWebAssembly.ts | 0 23 files changed, 295 insertions(+), 111 deletions(-) rename src/routes/player/subtitles/{AssRenderer.tsx => SsaRenderer.tsx} (89%) rename src/util/media/{usePosition.ts => useCurrentTime.ts} (91%) create mode 100644 src/util/media/usePlayerState.ts create mode 100644 src/util/media/useVideoApi.ts rename src/util/subtitles/{ass => ssa}/SubtitlesOctopus.ts (100%) rename src/util/subtitles/{ass => ssa}/SubtitlesOctopusOptions.ts (100%) rename src/util/subtitles/{ass => ssa}/browserHasAlphaBug.ts (100%) rename src/util/subtitles/{ass => ssa}/browserSupportsWebAssembly.ts (100%) diff --git a/src/api/models/Image.ts b/src/api/models/Image.ts index 351160b..62511e9 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 5b5402f..ad87bae 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 ec815e1..c171c08 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 40f04b5..b6936f0 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 d65c9eb..6feaaad 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 a3dee23..58dd9fc 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 4649bb4..71ba1a3 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 2a2de12..315d531 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 037280b..f64d31a 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 4439f3b..8c3a227 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 3f7c0a6..227ec5b 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 48ff51b..728a0c0 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 5b1765c..7a2955c 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 ca9ee8f..c08a1af 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 d8a3b8a..ead1f43 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 c0259a3..52bb3c7 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 019e98a..4f3e62f 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 0000000..620fb56 --- /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 0000000..b6ede31 --- /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 -- GitLab