diff --git a/src/video/components/actions/ThumbnailAction.tsx b/src/video/components/actions/ThumbnailAction.tsx index e2538057..cbb72374 100644 --- a/src/video/components/actions/ThumbnailAction.tsx +++ b/src/video/components/actions/ThumbnailAction.tsx @@ -99,6 +99,7 @@ export default function ThumbnailAction({ const src = source.source?.thumbnails.find( (x) => x.from < hoverTime && x.to > hoverTime )?.imgUrl; + if (!source.source?.thumbnails.length) return null; return (
{!src ? ( diff --git a/src/video/components/internal/ThumbnailGeneratorInternal.tsx b/src/video/components/internal/ThumbnailGeneratorInternal.tsx index 69fc33e7..993b1d5a 100644 --- a/src/video/components/internal/ThumbnailGeneratorInternal.tsx +++ b/src/video/components/internal/ThumbnailGeneratorInternal.tsx @@ -8,52 +8,36 @@ import { updateSource, useSource } from "@/video/state/logic/source"; import { Thumbnail } from "@/video/state/types"; async function* generate( - videoUrl: string, - streamType: MWStreamType, videoRef: RefObject, canvasRef: RefObject, + index = 0, numThumbnails = 20 ): AsyncGenerator { const video = videoRef.current; const canvas = canvasRef.current; if (!video) return { from: -1, to: -1, imgUrl: "" }; if (!canvas) return { from: -1, to: -1, imgUrl: "" }; - console.log("extracting started", streamType.toString()); - if (streamType === MWStreamType.HLS) { - const hls = new Hls(); - console.log("new hls instance"); - - hls.attachMedia(video); - hls.loadSource(videoUrl); - } await new Promise((resolve, reject) => { video.addEventListener("loadedmetadata", resolve); video.addEventListener("error", reject); }); - canvas.height = video.videoHeight * 1; - canvas.width = video.videoWidth * 1; - let i = 0; - while (i < numThumbnails) { - const from = i * video.duration; - const to = (i + 1) * video.duration; - - // Seek to the specified time + canvas.height = video.videoHeight; + canvas.width = video.videoWidth; + const ctx = canvas.getContext("2d"); + if (!ctx) return { from: -1, to: -1, imgUrl: "" }; + let i = index; + const limit = numThumbnails - 1; + const step = video.duration / limit; + while (i < limit && !Number.isNaN(video.duration)) { + const from = i * step; + const to = (i + 1) * step; video.currentTime = from; - console.log(from, to); - console.time("seek loaded"); await new Promise((resolve) => { video.addEventListener("seeked", resolve); }); - console.timeEnd("seek loaded"); - console.log("loaded", video.currentTime, streamType.toString()); - const ctx = canvas.getContext("2d"); - if (!ctx) return { from: -1, to: -1, imgUrl: "" }; - // Draw the video frame on the canvas ctx.drawImage(video, 0, 0, canvas.width, canvas.height); - - // Convert the canvas to a data URL and add it to the list of thumbnails const imgUrl = canvas.toDataURL(); i += 1; yield { @@ -67,48 +51,60 @@ async function* generate( } export default function ThumbnailGeneratorInternal() { - const videoRef = useRef(document.createElement("video")); - const canvasRef = useRef(document.createElement("canvas")); const descriptor = useVideoPlayerDescriptor(); const source = useSource(descriptor); + + const videoRef = useRef(document.createElement("video")); + const canvasRef = useRef(document.createElement("canvas")); + const hlsRef = useRef(new Hls()); const thumbnails = useRef([]); const abortController = useRef(new AbortController()); + const generator = useCallback( - async (url: string, type: MWStreamType) => { - for await (const thumbnail of generate(url, type, videoRef, canvasRef)) { + async (videoUrl: string, streamType: MWStreamType) => { + const prevIndex = thumbnails.current.length; + const video = videoRef.current; + if (streamType === MWStreamType.HLS) { + hlsRef.current.attachMedia(video); + hlsRef.current.loadSource(videoUrl); + } else { + video.crossOrigin = "anonymous"; + video.src = videoUrl; + } + + for await (const thumbnail of generate(videoRef, canvasRef, prevIndex)) { if (abortController.current.signal.aborted) { - console.log("broke out of loop", type.toString()); + if (streamType === MWStreamType.HLS) hlsRef.current.detachMedia(); + abortController.current = new AbortController(); + const state = getPlayerState(descriptor); + if (!state.source) return; + const { url, type } = state.source; + generator(url, type); break; } + if (thumbnail.from === -1) continue; thumbnails.current = [...thumbnails.current, thumbnail]; const state = getPlayerState(descriptor); if (!state.source) return; - console.log("ran"); state.source.thumbnails = thumbnails.current; - console.log(thumbnails.current); - updateSource(descriptor, state); - console.log("ran 2"); } }, [descriptor] ); useEffect(() => { + const controller = abortController.current; const state = getPlayerState(descriptor); if (!state.source) return; const { url, type } = state.source; generator(url, type); - }, [descriptor, generator, source.source?.url]); - - useEffect(() => { - const controller = abortController.current; return () => { - console.log("abort"); + if (!source.source?.url) return; controller.abort(); }; - }, []); + }, [descriptor, generator, source.source?.url]); return null; }