import { useState, useRef, useCallback, useEffect } from 'react'; import type { PresetName } from '@core/types.js'; import { getPreset, PRESET_DESCRIPTIONS } from '@core/presets.js'; import { streamExtractAndEmbed } from '../lib/video-io.js'; import StrengthSlider from './StrengthSlider.js'; import ComparisonView from './ComparisonView.js'; import RobustnessTest from './RobustnessTest.js'; const PLEASE_HOLD = [ { art: ' (•_•)\n ( •_•)>⌐■-■\n (⌐■_■)', caption: 'Putting on invisibility shades...' }, { art: ' ┌(▀Ĺ̯▀)┐\n ┌(▀Ĺ̯▀)┘\n └(▀Ĺ̯▀)┐', caption: 'Robot dance while we wait...' }, { art: ' ╔══╗\n ║▓▓║ ☕\n ╚══╝', caption: 'Brewing the perfect watermark...' }, { art: ' 🎬 → 🔬 → 💎', caption: 'Turning pixels into secrets...' }, { art: ' [▓▓▓░░░░░░░]\n [▓▓▓▓▓░░░░░]\n [▓▓▓▓▓▓▓▓░░]', caption: 'Hiding bits in plain sight...' }, { art: ' /\\_/\\\n ( o.o )\n > ^ <', caption: 'Even the cat can\'t see the watermark...' }, { art: ' ┌─────────┐\n │ 01101001 │\n └─────────┘', caption: 'Whispering bits into wavelets...' }, { art: ' ~~ 🌊 ~~\n ~🏄~ ~~ \n ~~~~~~~~', caption: 'Surfing the frequency domain...' }, { art: ' 📼 ➜ 🧬 ➜ 📼', caption: 'Splicing invisible DNA into frames...' }, { art: ' ¯\\_(ツ)_/¯', caption: 'Trust us, the watermark is there...' }, ]; /** Max frames to keep in memory for the comparison view */ const COMPARISON_SAMPLE = 30; export default function EmbedPanel() { const [videoUrl, setVideoUrl] = useState(null); const [videoName, setVideoName] = useState(''); const [key, setKey] = useState(''); const [payload, setPayload] = useState('DEADBEEF'); const [preset, setPreset] = useState('light'); const [alpha, setAlpha] = useState(0.00); const [processing, setProcessing] = useState(false); const [progress, setProgress] = useState({ phase: '', current: 0, total: 0 }); const [jokeIndex, setJokeIndex] = useState(0); const [result, setResult] = useState<{ blob: Blob; psnr: number; frames: number; originalFrames: ImageData[]; watermarkedFrames: ImageData[]; width: number; height: number; fps: number; embedTimeMs: number; pixelsPerSecond: number; } | null>(null); const fileRef = useRef(null); // Rotate jokes every 4s while processing useEffect(() => { if (!processing) return; setJokeIndex(Math.floor(Math.random() * PLEASE_HOLD.length)); const id = setInterval(() => { setJokeIndex((i) => (i + 1) % PLEASE_HOLD.length); }, 4000); return () => clearInterval(id); }, [processing]); const handleFile = useCallback((file: File) => { const url = URL.createObjectURL(file); setVideoUrl(url); setVideoName(file.name); setResult(null); }, [result]); const handleDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); const file = e.dataTransfer.files[0]; if (file?.type.startsWith('video/')) handleFile(file); }, [handleFile] ); const handleEmbed = async () => { if (!videoUrl || !key) return; setProcessing(true); setResult(null); try { // Parse payload const payloadHex = payload.replace(/^0x/, ''); const payloadBytes = new Uint8Array( (payloadHex.length % 2 ? '0' + payloadHex : payloadHex) .match(/.{2}/g)! .map((b) => parseInt(b, 16)) ); const config = getPreset(preset); // Stream: extract -> watermark -> encode in chunks const embedResult = await streamExtractAndEmbed( videoUrl, payloadBytes, key, config, COMPARISON_SAMPLE, (phase, current, total) => setProgress({ phase, current, total }) ); setResult({ blob: embedResult.blob, psnr: embedResult.avgPsnr, frames: embedResult.totalFrames, originalFrames: embedResult.sampleOriginal, watermarkedFrames: embedResult.sampleWatermarked, width: embedResult.width, height: embedResult.height, fps: embedResult.fps, embedTimeMs: embedResult.embedTimeMs, pixelsPerSecond: embedResult.pixelsPerSecond, }); } catch (e) { console.error('Embed error:', e); alert(`Error: ${e}`); } finally { setProcessing(false); } }; const handleDownload = () => { if (!result) return; const url = URL.createObjectURL(result.blob); const a = document.createElement('a'); a.href = url; a.download = videoName.replace(/\.[^.]+$/, '') + '_watermarked.mp4'; a.click(); URL.revokeObjectURL(url); }; const maxPayloadHexChars = 8; // Always 32 bits = 8 hex chars return (
{/* Upload area */}
e.preventDefault()} onClick={() => fileRef.current?.click()} className={`group cursor-pointer rounded-2xl border-2 border-dashed p-10 text-center transition-all duration-200 ${videoUrl ? 'border-zinc-700 bg-zinc-900/30' : 'border-zinc-800 bg-zinc-900/20 hover:border-blue-600/40 hover:bg-zinc-900/40 hover:shadow-lg hover:shadow-blue-600/5' }`} > { const file = e.target.files?.[0]; if (file) handleFile(file); }} /> {videoUrl ? (

🎬 {videoName}

Click or drop to replace

) : (

Drop a video file or click to browse

MP4, WebM, MOV supported

)}
{/* Configuration */}
setKey(e.target.value)} placeholder="Enter a secret key..." className="w-full rounded-lg border border-zinc-800 bg-zinc-900/50 px-3 py-2 text-sm text-zinc-100 placeholder:text-zinc-600 focus:border-blue-600 focus:outline-none focus:ring-1 focus:ring-blue-600 transition-colors" />
setPayload(e.target.value.replace(/[^0-9a-fA-F]/g, '').slice(0, maxPayloadHexChars))} placeholder="DEADBEEF" className="w-full rounded-lg border border-zinc-800 bg-zinc-900/50 px-3 py-2 font-mono text-sm text-zinc-100 placeholder:text-zinc-600 focus:border-blue-600 focus:outline-none focus:ring-1 focus:ring-blue-600 transition-colors" />

32-bit payload, 4 bytes hex

{ setAlpha(v); setPreset(p); }} disabled={processing} />

{PRESET_DESCRIPTIONS[preset]}

{/* Embed button */} {/* Please-hold jokes while processing */} {processing && (
            {PLEASE_HOLD[jokeIndex].art}
          

{PLEASE_HOLD[jokeIndex].caption}

)} {/* Results */} {result && (

✅ Embedding Complete

{result.frames} frames processed — Average PSNR: {result.psnr.toFixed(1)} dB
Embedded in {(result.embedTimeMs / 1000).toFixed(1)}s — {(result.pixelsPerSecond / 1e6).toFixed(1)} Mpx/s

)}
); }