9 changed files with 218 additions and 121 deletions
@ -1,51 +0,0 @@
@@ -1,51 +0,0 @@
|
||||
export interface Thumbnail { |
||||
from: number; |
||||
to: number; |
||||
imgUrl: string; |
||||
} |
||||
export const SCALE_FACTOR = 1; |
||||
export default async function* extractThumbnails( |
||||
videoUrl: string, |
||||
numThumbnails: number |
||||
): AsyncGenerator<Thumbnail, Thumbnail> { |
||||
const canvas = document.createElement("canvas"); |
||||
const ctx = canvas.getContext("2d"); |
||||
if (!ctx) return { from: -1, to: -1, imgUrl: "" }; |
||||
const video = document.createElement("video"); |
||||
video.src = videoUrl; |
||||
video.crossOrigin = "anonymous"; |
||||
|
||||
// Wait for the video metadata to load
|
||||
await new Promise((resolve, reject) => { |
||||
video.addEventListener("loadedmetadata", resolve); |
||||
video.addEventListener("error", reject); |
||||
}); |
||||
|
||||
canvas.height = video.videoHeight * SCALE_FACTOR; |
||||
canvas.width = video.videoWidth * SCALE_FACTOR; |
||||
|
||||
for (let i = 0; i <= numThumbnails; i += 1) { |
||||
const from = (i / (numThumbnails + 1)) * video.duration; |
||||
const to = ((i + 1) / (numThumbnails + 1)) * video.duration; |
||||
|
||||
// Seek to the specified time
|
||||
video.currentTime = from; |
||||
await new Promise((resolve) => { |
||||
video.addEventListener("seeked", resolve); |
||||
}); |
||||
|
||||
// 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(); |
||||
|
||||
yield { |
||||
from, |
||||
to, |
||||
imgUrl, |
||||
}; |
||||
} |
||||
|
||||
return { from: -1, to: -1, imgUrl: "" }; |
||||
} |
@ -0,0 +1,114 @@
@@ -0,0 +1,114 @@
|
||||
import Hls from "hls.js"; |
||||
import { RefObject, useCallback, useEffect, useRef, useState } from "react"; |
||||
|
||||
import { MWStreamType } from "@/backend/helpers/streams"; |
||||
import { getPlayerState } from "@/video/state/cache"; |
||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks"; |
||||
import { updateSource, useSource } from "@/video/state/logic/source"; |
||||
import { Thumbnail } from "@/video/state/types"; |
||||
|
||||
async function* generate( |
||||
videoUrl: string, |
||||
streamType: MWStreamType, |
||||
videoRef: RefObject<HTMLVideoElement>, |
||||
canvasRef: RefObject<HTMLCanvasElement>, |
||||
numThumbnails = 20 |
||||
): AsyncGenerator<Thumbnail, Thumbnail> { |
||||
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
|
||||
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 { |
||||
from, |
||||
to, |
||||
imgUrl, |
||||
}; |
||||
} |
||||
|
||||
return { from: -1, to: -1, imgUrl: "" }; |
||||
} |
||||
|
||||
export default function ThumbnailGeneratorInternal() { |
||||
const videoRef = useRef<HTMLVideoElement>(document.createElement("video")); |
||||
const canvasRef = useRef<HTMLCanvasElement>(document.createElement("canvas")); |
||||
const descriptor = useVideoPlayerDescriptor(); |
||||
const source = useSource(descriptor); |
||||
const thumbnails = useRef<Thumbnail[]>([]); |
||||
const abortController = useRef<AbortController>(new AbortController()); |
||||
const generator = useCallback( |
||||
async (url: string, type: MWStreamType) => { |
||||
for await (const thumbnail of generate(url, type, videoRef, canvasRef)) { |
||||
if (abortController.current.signal.aborted) { |
||||
console.log("broke out of loop", type.toString()); |
||||
break; |
||||
} |
||||
|
||||
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 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"); |
||||
controller.abort(); |
||||
}; |
||||
}, []); |
||||
|
||||
return null; |
||||
} |
Loading…
Reference in new issue