export function buildThumbnailCandidateTimes(durationSeconds) { const duration = Number(durationSeconds); if (!Number.isFinite(duration) || duration <= 0) { return [0.15]; } const upperBound = Math.max(0.05, duration - 0.05); const baseCandidates = [0.15, 0.5, 1, 2, 3.5, 5]; const candidates = []; for (const candidate of baseCandidates) { const clamped = Math.max(0, Math.min(candidate, upperBound)); if (candidates.length === 0 || Math.abs(candidates[candidates.length - 1] - clamped) > 0.08) { candidates.push(Number(clamped.toFixed(3))); } } if (upperBound > 0.25) { const fallback = Number(upperBound.toFixed(3)); if (Math.abs(candidates[candidates.length - 1] - fallback) > 0.08) { candidates.push(fallback); } } return candidates; } export function analyzeFrameLuma(data, options = {}) { const bytes = data instanceof Uint8ClampedArray ? data : new Uint8ClampedArray(data ?? []); const pixelCount = Math.floor(bytes.length / 4); if (pixelCount === 0) { return { sampleCount: 0, averageLuma: 0, darkRatio: 1, isBlackFrame: true }; } const { darknessThreshold = 18, maxDarkRatio = 0.985, averageThreshold = 22, stride = 16 } = options; let samples = 0; let darkSamples = 0; let lumaTotal = 0; for (let index = 0; index < pixelCount; index += stride) { const offset = index * 4; const red = bytes[offset]; const green = bytes[offset + 1]; const blue = bytes[offset + 2]; const alpha = bytes[offset + 3]; const luma = alpha === 0 ? 0 : 0.2126 * red + 0.7152 * green + 0.0722 * blue; lumaTotal += luma; samples += 1; if (luma <= darknessThreshold) { darkSamples += 1; } } const averageLuma = samples > 0 ? lumaTotal / samples : 0; const darkRatio = samples > 0 ? darkSamples / samples : 1; const isBlackFrame = averageLuma <= averageThreshold && darkRatio >= maxDarkRatio; return { sampleCount: samples, averageLuma, darkRatio, isBlackFrame }; }