import { getEmbeddedAlphaMap, getVeoWatermarkInfo, processVideo, type ProcessedVideo, type WatermarkInfo, } from './vendor/index.js'; /** * Browser-only helpers around the vendored reverse-alpha-blending engine: * check that the browser can encode video, find the Flow watermark in a few * sampled frames, then clean every frame locally. */ export const MAX_FILE_BYTES = 500 * 1024 * 1024; // Correlation between the alpha map and the pixels under it. Real Flow // exports score well above this; clips without the watermark stay near 0. const MIN_WATERMARK_SCORE = 0.25; const SAMPLE_POINTS = [0.1, 0.3, 0.5, 0.7, 0.9]; export type Detection = | { status: 'found'; width: number; height: number; duration: number; score: number; info: WatermarkInfo; } | { status: 'not_found' | 'unsupported_resolution'; width: number; height: number; duration: number; score: number; }; export function supportsLocalProcessing() { return ( typeof window !== 'undefined' && 'VideoEncoder' in window && 'VideoDecoder' in window && typeof OffscreenCanvas !== 'undefined' ); } function loadVideo(url: string) { return new Promise((resolve, reject) => { const video = document.createElement('video'); video.muted = true; video.preload = 'auto'; video.playsInline = true; video.onloadeddata = () => resolve(video); video.onerror = () => reject(new Error('This file could not be opened.')); video.src = url; }); } function seek(video: HTMLVideoElement, time: number) { return new Promise((resolve) => { video.onseeked = () => resolve(); video.currentTime = time; }); } /** Pearson correlation of alpha against pixel brightness, over all samples. */ function correlation(alpha: Float32Array, samples: Uint8ClampedArray[]) { let n = 0; let sumA = 0; let sumL = 0; let sumAA = 0; let sumLL = 0; let sumAL = 0; for (const pixels of samples) { for (let i = 0; i < alpha.length; i++) { const a = alpha[i]; const l = 0.299 * pixels[i * 4] + 0.587 * pixels[i * 4 + 1] + 0.114 * pixels[i * 4 + 2]; n++; sumA += a; sumL += l; sumAA += a * a; sumLL += l * l; sumAL += a * l; } } const cov = sumAL / n - (sumA / n) * (sumL / n); const varA = sumAA / n - (sumA / n) ** 2; const varL = sumLL / n - (sumL / n) ** 2; if (varA <= 0 || varL <= 0) return 0; return cov / Math.sqrt(varA * varL); } export async function detectWatermark(file: File): Promise { const url = URL.createObjectURL(file); try { const video = await loadVideo(url); const width = video.videoWidth; const height = video.videoHeight; const duration = Number.isFinite(video.duration) ? video.duration : 0; const info = getVeoWatermarkInfo(width, height, 'diamond'); const alpha = info ? getEmbeddedAlphaMap(info.alphaMapKey) : null; if (!info || !alpha) { return { status: 'unsupported_resolution', width, height, duration, score: 0, }; } const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d', { willReadFrequently: true }); if (!ctx) throw new Error('Canvas is not available in this browser.'); const { x, y, width: w, height: h } = info.position; const samples: Uint8ClampedArray[] = []; for (const point of SAMPLE_POINTS) { await seek(video, duration * point); ctx.drawImage(video, 0, 0, width, height); samples.push(ctx.getImageData(x, y, w, h).data); } const score = correlation(alpha.data, samples); return score >= MIN_WATERMARK_SCORE ? { status: 'found', width, height, duration, score, info } : { status: 'not_found', width, height, duration, score }; } finally { URL.revokeObjectURL(url); } } // Re-encode a little above the source bitrate so quality stays close to the // original without the file ballooning. function targetBitrate(file: File, duration: number) { if (!duration) return 8_000_000; const source = (file.size * 8) / duration; return Math.round(Math.min(16_000_000, Math.max(2_000_000, source * 1.3))); } export function removeFlowWatermark( file: File, duration: number, onProgress: (fraction: number) => void ): Promise { return processVideo(file, { videoProfile: 'diamond', bitrate: targetBitrate(file, duration), onProgress: (current, total) => onProgress(total ? current / total : 0), }); }