From bd491f2d146290e89662a067135374b2b79c1950 Mon Sep 17 00:00:00 2001 From: Pas <74743263+Pasithea0@users.noreply.github.com> Date: Sun, 9 Nov 2025 15:02:27 -0700 Subject: [PATCH] Improve thumbnail generation and quality selection Refactors thumbnail queue to generate 127 evenly distributed thumbnails instead of using a layered approach. Adds a new selectLowestQuality function to consistently select the lowest available video quality for thumbnail extraction, replacing the previous selectQuality usage. --- .../player/internals/ThumbnailScraper.tsx | 73 ++++++++++++++----- 1 file changed, 53 insertions(+), 20 deletions(-) diff --git a/src/components/player/internals/ThumbnailScraper.tsx b/src/components/player/internals/ThumbnailScraper.tsx index 1766d69f..27cb66cc 100644 --- a/src/components/player/internals/ThumbnailScraper.tsx +++ b/src/components/player/internals/ThumbnailScraper.tsx @@ -3,27 +3,64 @@ import { useCallback, useEffect, useRef } from "react"; import { ThumbnailImage } from "@/stores/player/slices/thumbnails"; import { usePlayerStore } from "@/stores/player/store"; -import { LoadableSource, selectQuality } from "@/stores/player/utils/qualities"; +import { + LoadableSource, + SourceQuality, + SourceSliceSource, +} from "@/stores/player/utils/qualities"; import { usePreferencesStore } from "@/stores/preferences"; import { processCdnLink } from "@/utils/cdn"; import { isSafari } from "@/utils/detectFeatures"; -function makeQueue(layers: number): number[] { - const output = [0, 1]; - let segmentSize = 0.5; - let lastSegmentAmount = 0; - for (let layer = 0; layer < layers; layer += 1) { - const segmentAmount = 1 / segmentSize - 1; - for (let i = 0; i < segmentAmount - lastSegmentAmount; i += 1) { - const offset = i * segmentSize * 2; - output.push(offset + segmentSize); - } - lastSegmentAmount = segmentAmount; - segmentSize /= 2; +function makeQueue(thumbnails: number): number[] { + const output = []; + for (let i = 0; i < thumbnails; i += 1) { + output.push(i / (thumbnails - 1)); } return output; } +function selectLowestQuality(source: SourceSliceSource): LoadableSource { + if (source.type === "hls") return source; + + if (source.type === "file") { + const availableQualities = Object.entries(source.qualities) + .filter((entry) => (entry[1].url.length ?? 0) > 0) + .map((entry) => entry[0]) as SourceQuality[]; + + // Quality sorting by priority (higher number = higher quality) + const qualityPriority: Record = { + "360": 10, + "480": 20, + "720": 30, + "4k": 35, + "1080": 40, + unknown: 50, // unknown is typically the largest quality + }; + + // Find the lowest quality (smallest priority number) that's available + let lowestQuality: SourceQuality | null = null; + let lowestPriority = Infinity; + + for (const quality of availableQualities) { + const priority = qualityPriority[quality] ?? 0; + if (priority < lowestPriority) { + lowestPriority = priority; + lowestQuality = quality; + } + } + + if (lowestQuality) { + const stream = source.qualities[lowestQuality]; + if (stream) { + return stream; + } + } + } + + throw new Error("couldn't select lowest quality"); +} + class ThumnbnailWorker { interrupted: boolean; @@ -114,7 +151,7 @@ class ThumnbnailWorker { if (!vid) return; await this.initVideo(); - const queue = makeQueue(6); // 7 layers is 63 thumbnails evenly distributed + const queue = makeQueue(127); // 127 thumbnails evenly distributed across the video for (let i = 0; i < queue.length; i += 1) { if (this.interrupted) return; await this.takeSnapshot(vid.duration * queue[i]); @@ -137,11 +174,7 @@ export function ThumbnailScraper() { const start = useCallback(() => { let inputStream = null; - if (source) - inputStream = selectQuality(source, { - automaticQuality: false, - lastChosenQuality: "360", - }); + if (source) inputStream = selectLowestQuality(source); // dont interrupt existing working if (workerRef.current) return; // Allow thumbnail generation when video is loaded and has duration @@ -152,7 +185,7 @@ export function ThumbnailScraper() { addImage, }); workerRef.current = ins; - ins.start(inputStream.stream); + ins.start(inputStream); }, [source, addImage, resetImages, hasPlayedOnce, duration]); const startRef = useRef(start);