diff --git a/src/routes/player/PreviewViewer.tsx b/src/routes/player/PreviewViewer.tsx index fdca963f27e32be1e7dee91502e77e7e91bcb4ca..3ba98292639a73a600964c19443fd6988c07217f 100644 --- a/src/routes/player/PreviewViewer.tsx +++ b/src/routes/player/PreviewViewer.tsx @@ -1,7 +1,7 @@ import React, {useMemo} from "react"; +import {useImage} from "../../util/media/useImage"; import {useTextTrackCues} from "../../util/media/useTextTrackCues"; import {parseImageSprite} from "../../util/sprite/parseImageSprite"; -import {useImage} from "../../util/media/useImage"; import {useImageSprite} from "../../util/sprite/useImageSprite"; interface Props { @@ -14,10 +14,9 @@ export function PreviewViewer({previewTrack, position}: Props) { const activeCue = cues.find(it => it.startTime <= position && it.endTime >= position) const activeUrl = activeCue ? new URL(activeCue.text, previewTrack.src).toString() : null; const imageSprite = useMemo(() => parseImageSprite(activeUrl), [activeUrl]); - console.log("active", activeUrl, imageSprite); const image = useImage(imageSprite?.src || null); const sprite = useImageSprite(imageSprite, image); return useMemo(() => ( - <img src={sprite || undefined}/> + <img alt="" src={sprite || undefined}/> ), [sprite]); } diff --git a/src/routes/player/SeekBar.tsx b/src/routes/player/SeekBar.tsx index 792c0ff120f7d3a32288667256e2fab45ff4cf2b..3a38e70d72ee720f5a9887238403108973dadfd3 100644 --- a/src/routes/player/SeekBar.tsx +++ b/src/routes/player/SeekBar.tsx @@ -13,9 +13,10 @@ interface MousePosition { relative: number } -function getMousePosition(event: React.MouseEvent<HTMLDivElement>): MousePosition { +function getMousePosition(event: React.MouseEvent<HTMLDivElement>): MousePosition | null { const position = event.clientX - event.currentTarget.offsetLeft; const width = event.currentTarget.offsetWidth; + if (position > width) return null; return { absolute: position, relative: position / width @@ -35,12 +36,18 @@ export function SeekBar({video, previewTrack, duration}: Props) { onMouseLeave={() => setSeekPosition(null)} onClick={(event) => { const position = getMousePosition(event); + if (position === null) { + return; + } if (video) { video.currentTime = position.relative * duration; } }} onMouseMove={(event) => { const position = getMousePosition(event); + if (position === null) { + return; + } if (seekHeadRef.current) { seekHeadRef.current.style.transform = `translate3d(${position.absolute}px, 0, 0)` } @@ -57,7 +64,7 @@ export function SeekBar({video, previewTrack, duration}: Props) { }} /> </div> - ), [classes.seekBar, classes.seekHead, duration, isVisible]); + ), [classes.seekBar, classes.seekHead, duration, isVisible, video]); return ( <div> @@ -80,7 +87,7 @@ const useStyles = createUseStyles({ position: "absolute", top: 0, bottom: 0, - left: 0, + left: "-0.05rem", width: "0.1rem", background: "#f00", } diff --git a/src/util/media/useImage.ts b/src/util/media/useImage.ts index 7ec2d7e72f054410617a2a0df1ce53ca23ba1841..4ac4010a68fd4df8ce43c7d52a0837d8ef12bb37 100644 --- a/src/util/media/useImage.ts +++ b/src/util/media/useImage.ts @@ -1,18 +1,30 @@ -import {useEffect, useRef, useState} from "react"; +import {useEffect, useMemo, useState} from "react"; export const useImage = (url: string | null): HTMLImageElement | null => { - const image = useRef(new Image()) - image.current.setAttribute("crossorigin", "anonymous"); const [imageData, setImageData] = useState<HTMLImageElement | null>(null); - useEffect(() => { - try { - image.current.src = url || ""; - image.current.decode().then(() => { - setImageData(image.current); - }) - } catch (_) { + const image = useMemo(() => { + if (url === null) { + return null; + } else { + const image = new Image(); + image.setAttribute("crossorigin", "anonymous"); + image.src = url; + return image; } }, [url]); + useEffect(() => { + let isCancelled = false; + image?.decode().then(() => { + if (!isCancelled) { + setImageData(image); + } + }).catch(err => { + console.error("Error while decoding image " + url + ": ", err); + }); + return () => { + isCancelled = true; + } + }, [image, url]); return imageData; } diff --git a/src/util/media/usePosition.ts b/src/util/media/usePosition.ts index 770ea75bbbc42644f2927e9c15eca2825ff5d2c8..fb1b390d09dfce0efbec4d7443405a745aaaa559 100644 --- a/src/util/media/usePosition.ts +++ b/src/util/media/usePosition.ts @@ -9,9 +9,9 @@ export const usePosition = (video: HTMLVideoElement | null) => { setPosition(video.currentTime); }) }; - video.addEventListener("progress", listener) + video.addEventListener("timeupdate", listener) return () => { - video.removeEventListener("progress", listener) + video.removeEventListener("timeupdate", listener) } } }, [video]); diff --git a/src/util/media/useTextTrackCues.ts b/src/util/media/useTextTrackCues.ts index 136cab655cf2ed2fd0b3680650b55d3c06ebb2b7..1309d104f1ba5fafef51cbf46d4ae2f0d6aa4922 100644 --- a/src/util/media/useTextTrackCues.ts +++ b/src/util/media/useTextTrackCues.ts @@ -7,14 +7,18 @@ export const useTextTrackCues = (track: HTMLTrackElement) => { if (track.readyState >= 1) { setCues(Array.from(track.track.cues || [])) } else { + let animationFrame: number | null = null; const listener = () => { - window.requestAnimationFrame(() => { + animationFrame = window.requestAnimationFrame(() => { setCues(Array.from(track.track.cues || [])) }) }; track.addEventListener("load", listener) return () => { - track.removeEventListener("load", listener) + track.removeEventListener("load", listener); + if (animationFrame) { + window.cancelAnimationFrame(animationFrame); + } } } }, [track]);