Download index.html from Open-Hat-Lab/Add-Video-Watermark: direct link, hf CLI and curl.
- Browser
- Download file 33.7 kB
-
https://huggingface.co/spaces/Open-Hat-Lab/Add-Video-Watermark/resolve/main/index.html
- Command line
-
hf download hf://spaces/Open-Hat-Lab/Add-Video-Watermark/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Open-Hat-Lab/Add-Video-Watermark/resolve/main/index.html
33.7 kB
| <!-- https://poe.com/AddVideoWatermark-01 | |
| https://huggingface.co/spaces/Selfit/Remove-Watermark | |
| https://huggingface.co/spaces/abdul9999/NoWatermark | |
| --> | |
| <html> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script> | |
| tailwind.config = { | |
| darkMode: 'class', | |
| theme: { | |
| extend: { | |
| colors: { | |
| primary: '#5D5CDE' | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| </head> | |
| <body class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen"> | |
| <div class="container mx-auto px-4 py-8 max-w-6xl"> | |
| <div class="text-center mb-8"> | |
| <h1 class="text-4xl font-bold mb-2 bg-gradient-to-r from-primary to-purple-600 bg-clip-text text-transparent"> | |
| Add-Video-Watermark | |
| </h1> | |
| <p class="text-gray-600 dark:text-gray-400">Add custom text or image watermarks to your videos using canvas technology</p> | |
| </div> | |
| <div class="bg-gray-50 dark:bg-gray-800 rounded-xl p-6 mb-6"> | |
| <h2 class="text-xl font-semibold mb-4">1. Upload Video</h2> | |
| <div class="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-8 text-center"> | |
| <input type="file" id="videoInput" accept="video/*" class="hidden"> | |
| <button onclick="document.getElementById('videoInput').click()" class="bg-primary text-white px-6 py-3 rounded-lg hover:bg-purple-600 transition-colors mb-2"> | |
| Choose Video File | |
| </button> | |
| <p class="text-sm text-gray-500 dark:text-gray-400">Supports MP4, WebM and other web-compatible formats</p> | |
| <div id="videoInfo" class="mt-4 hidden"> | |
| <p class="text-sm font-medium text-green-600 dark:text-green-400"></p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-gray-50 dark:bg-gray-800 rounded-xl p-6 mb-6" id="watermarkSection" style="display: none;"> | |
| <h2 class="text-xl font-semibold mb-4">2. Watermark Type</h2> | |
| <div class="flex flex-wrap gap-4 mb-4"> | |
| <label class="flex items-center space-x-2 cursor-pointer"> | |
| <input type="radio" name="watermarkType" value="text" class="text-primary" checked> | |
| <span>Text Watermark</span> | |
| </label> | |
| <label class="flex items-center space-x-2 cursor-pointer"> | |
| <input type="radio" name="watermarkType" value="image" class="text-primary"> | |
| <span>Image Watermark</span> | |
| </label> | |
| </div> | |
| <div id="textOptions"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-sm font-medium mb-2">Watermark Text</label> | |
| <input type="text" id="watermarkText" placeholder="Enter your watermark text" value="https://poe.com/Add-Video-Watermark" class="w-full px-3 py-2 text-base border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 focus:ring-2 focus:ring-primary focus:border-primary"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-2">Font Size</label> | |
| <input type="range" id="fontSize" min="12" max="72" value="24" class="w-full h-2 bg-gray-200 dark:bg-gray-600 rounded-lg appearance-none cursor-pointer"> | |
| <div class="text-sm text-gray-500 dark:text-gray-400 text-center mt-1"> | |
| <span id="fontSizeValue">24</span>px | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-2">Text Color</label> | |
| <input type="color" id="textColor" value="#ffffff" class="w-16 h-10 border border-gray-300 dark:border-gray-600 rounded cursor-pointer"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-2">Opacity</label> | |
| <input type="range" id="textOpacity" min="0.1" max="1" step="0.1" value="0.8" class="w-full h-2 bg-gray-200 dark:bg-gray-600 rounded-lg appearance-none cursor-pointer"> | |
| <div class="text-sm text-gray-500 dark:text-gray-400 text-center mt-1"> | |
| <span id="textOpacityValue">80</span>% | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="imageOptions" class="hidden"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-sm font-medium mb-2">Watermark Image</label> | |
| <input type="file" id="imageInput" accept="image/png,image/jpg,image/jpeg" class="w-full px-3 py-2 text-base border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 focus:ring-2 focus:ring-primary focus:border-primary"> | |
| <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">PNG recommended for transparency</p> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-2">Size (%)</label> | |
| <input type="range" id="imageSize" min="5" max="50" value="15" class="w-full h-2 bg-gray-200 dark:bg-gray-600 rounded-lg appearance-none cursor-pointer"> | |
| <div class="text-sm text-gray-500 dark:text-gray-400 text-center mt-1"> | |
| <span id="imageSizeValue">15</span>% | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-2">Opacity</label> | |
| <input type="range" id="imageOpacity" min="0.1" max="1" step="0.1" value="0.8" class="w-full h-2 bg-gray-200 dark:bg-gray-600 rounded-lg appearance-none cursor-pointer"> | |
| <div class="text-sm text-gray-500 dark:text-gray-400 text-center mt-1"> | |
| <span id="imageOpacityValue">80</span>% | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-gray-50 dark:bg-gray-800 rounded-xl p-6 mb-6" id="previewSection" style="display: none;"> | |
| <h2 class="text-xl font-semibold mb-4">3. Position Preview</h2> | |
| <div class="relative max-w-4xl mx-auto"> | |
| <video id="previewVideo" class="w-full rounded-lg shadow-lg" controls crossorigin="anonymous"></video> | |
| <canvas id="previewCanvas" class="absolute top-0 left-0 w-full h-full pointer-events-none rounded-lg"></canvas> | |
| </div> | |
| <div class="mt-4 text-center"> | |
| <p class="text-sm text-gray-600 dark:text-gray-400 mb-2">Click on the video to position your watermark</p> | |
| <div class="flex flex-wrap justify-center gap-2"> | |
| <button onclick="setPosition('top-left')" class="px-3 py-1 text-sm bg-gray-200 dark:bg-gray-700 rounded hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">Top Left</button> | |
| <button onclick="setPosition('top-right')" class="px-3 py-1 text-sm bg-gray-200 dark:bg-gray-700 rounded hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">Top Right</button> | |
| <button onclick="setPosition('bottom-left')" class="px-3 py-1 text-sm bg-gray-200 dark:bg-gray-700 rounded hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">Bottom Left</button> | |
| <button onclick="setPosition('bottom-right')" class="px-3 py-1 text-sm bg-gray-200 dark:bg-gray-700 rounded hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">Bottom Right</button> | |
| <button onclick="setPosition('center')" class="px-3 py-1 text-sm bg-gray-200 dark:bg-gray-700 rounded hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">Center</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-gray-50 dark:bg-gray-800 rounded-xl p-6" id="processSection" style="display: none;"> | |
| <h2 class="text-xl font-semibold mb-4">4. Process Video</h2> | |
| <!-- Export Format Selection --> | |
| <div class="mb-6"> | |
| <h3 class="text-lg font-medium mb-3">Export Format</h3> | |
| <div class="flex flex-wrap gap-4"> | |
| <label class="flex items-center space-x-2 cursor-pointer"> | |
| <input type="radio" name="exportFormat" value="webm" class="text-primary" checked> | |
| <span>WebM (Free)</span> | |
| </label> | |
| <label class="flex items-center space-x-2 cursor-pointer" id="mp4Option"> | |
| <input type="radio" name="exportFormat" value="mp4" class="text-primary" disabled> | |
| <span class="text-gray-400">MP4 (Premium - Unlock Required)</span> | |
| </label> | |
| </div> | |
| <div id="unlockSection" class="mt-4 p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <p class="text-sm font-medium text-yellow-800 dark:text-yellow-200">Unlock MP4 Export</p> | |
| <p class="text-xs text-yellow-600 dark:text-yellow-300">Get premium features with @Cheap-Unlock-Bot</p> | |
| </div> | |
| <button id="unlockBtn" onclick="unlockMP4Feature()" class="bg-yellow-600 text-white px-4 py-2 rounded hover:bg-yellow-700 transition-colors text-sm"> | |
| Unlock MP4 | |
| </button> | |
| </div> | |
| <div id="unlockStatus" class="mt-2 hidden"> | |
| <p class="text-sm text-yellow-700 dark:text-yellow-300">Contacting @Cheap-Unlock-Bot...</p> | |
| </div> | |
| </div> | |
| <div id="unlockedSection" class="mt-4 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg hidden"> | |
| <div class="flex items-center"> | |
| <svg class="w-5 h-5 text-green-600 dark:text-green-400 mr-2" fill="currentColor" viewBox="0 0 20 20"> | |
| <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path> | |
| </svg> | |
| <p class="text-sm font-medium text-green-800 dark:text-green-200">MP4 Export Unlocked!</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="text-center"> | |
| <button id="processBtn" onclick="processVideo()" class="bg-primary text-white px-8 py-3 rounded-lg hover:bg-purple-600 transition-colors text-lg font-medium"> | |
| Add Watermark for free | |
| </button> | |
| <div id="progressContainer" class="mt-4 hidden"> | |
| <div class="bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-2"> | |
| <div id="progressBar" class="bg-primary h-2 rounded-full transition-all duration-300" style="width: 0%"></div> | |
| </div> | |
| <p id="progressText" class="text-sm text-gray-600 dark:text-gray-400">Starting...</p> | |
| </div> | |
| <div id="downloadContainer" class="mt-4 hidden"> | |
| <a id="downloadLink" class="inline-block bg-green-600 text-white px-6 py-3 rounded-lg hover:bg-green-700 transition-colors"> | |
| Download Watermarked Video | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> | |
| <div class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-lg max-w-sm w-full mx-4"> | |
| <p id="modalText" class="text-gray-700 dark:text-gray-300 mb-4"></p> | |
| <div class="flex justify-end"> | |
| <button onclick="closeModal()" class="px-4 py-2 bg-primary text-white hover:bg-purple-600 rounded">OK</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { | |
| document.documentElement.classList.add('dark'); | |
| } | |
| window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => { | |
| if (event.matches) { | |
| document.documentElement.classList.add('dark'); | |
| } else { | |
| document.documentElement.classList.remove('dark'); | |
| } | |
| }); | |
| let currentVideo = null; | |
| let watermarkPosition = { x: 50, y: 50 }; | |
| let watermarkImage = null; | |
| let previewCanvas = null; | |
| let previewCtx = null; | |
| let mp4Unlocked = false; | |
| function showModal(message) { | |
| document.getElementById('modalText').textContent = message; | |
| document.getElementById('modal').classList.remove('hidden'); | |
| } | |
| function closeModal() { | |
| document.getElementById('modal').classList.add('hidden'); | |
| } | |
| // Register handler for unlock bot response | |
| if (window.Poe && window.Poe.registerHandler) { | |
| window.Poe.registerHandler("unlock-handler", (result, context) => { | |
| const unlockStatus = document.getElementById('unlockStatus'); | |
| const unlockBtn = document.getElementById('unlockBtn'); | |
| if (result.responses && result.responses.length > 0) { | |
| const response = result.responses[0]; | |
| if (response.status === "complete") { | |
| const responseText = response.content.toLowerCase(); | |
| if (responseText.includes('cheapunl0ck')) { | |
| // Unlock successful | |
| mp4Unlocked = true; | |
| document.getElementById('unlockSection').classList.add('hidden'); | |
| document.getElementById('unlockedSection').classList.remove('hidden'); | |
| const mp4Radio = document.querySelector('input[name="exportFormat"][value="mp4"]'); | |
| const mp4Label = mp4Radio.parentElement.querySelector('span'); | |
| mp4Radio.disabled = false; | |
| mp4Label.textContent = 'MP4 (Premium)'; | |
| mp4Label.classList.remove('text-gray-400'); | |
| mp4Label.classList.add('text-gray-900', 'dark:text-gray-100'); | |
| unlockStatus.innerHTML = '<p class="text-sm text-green-700 dark:text-green-300">✅ Successfully unlocked MP4 export!</p>'; | |
| } else { | |
| // Unlock failed | |
| unlockStatus.innerHTML = '<p class="text-sm text-red-700 dark:text-red-300">❌ Unlock failed. Please try again.</p>'; | |
| unlockBtn.disabled = false; | |
| unlockBtn.textContent = 'Unlock MP4'; | |
| } | |
| } else if (response.status === "error") { | |
| unlockStatus.innerHTML = '<p class="text-sm text-red-700 dark:text-red-300">❌ Error contacting unlock bot.</p>'; | |
| unlockBtn.disabled = false; | |
| unlockBtn.textContent = 'Unlock MP4'; | |
| } | |
| } else { | |
| unlockStatus.innerHTML = '<p class="text-sm text-red-700 dark:text-red-300">❌ No response from unlock bot.</p>'; | |
| unlockBtn.disabled = false; | |
| unlockBtn.textContent = 'Unlock MP4'; | |
| } | |
| }); | |
| } | |
| async function unlockMP4Feature() { | |
| const unlockBtn = document.getElementById('unlockBtn'); | |
| const unlockStatus = document.getElementById('unlockStatus'); | |
| if (!window.Poe || !window.Poe.sendUserMessage) { | |
| showModal('Poe API not available. MP4 unlock feature requires the Poe environment.'); | |
| return; | |
| } | |
| unlockBtn.disabled = true; | |
| unlockBtn.textContent = 'Unlocking...'; | |
| unlockStatus.classList.remove('hidden'); | |
| try { | |
| await window.Poe.sendUserMessage( | |
| "@Cheap-Unlock-Bot I want to unlock MP4 export feature for Video Watermarker", | |
| { | |
| handler: "unlock-handler", | |
| stream: false, | |
| openChat: false | |
| } | |
| ); | |
| } catch (error) { | |
| console.error('Error contacting unlock bot:', error); | |
| unlockStatus.innerHTML = '<p class="text-sm text-red-700 dark:text-red-300">❌ Failed to contact unlock bot.</p>'; | |
| unlockBtn.disabled = false; | |
| unlockBtn.textContent = 'Unlock MP4'; | |
| } | |
| } | |
| document.getElementById('videoInput').addEventListener('change', function(e) { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| currentVideo = file; | |
| const url = URL.createObjectURL(file); | |
| const video = document.getElementById('previewVideo'); | |
| video.src = url; | |
| document.getElementById('videoInfo').classList.remove('hidden'); | |
| document.getElementById('videoInfo').querySelector('p').textContent = | |
| `Selected: ${file.name} (${(file.size / 1024 / 1024).toFixed(2)} MB)`; | |
| document.getElementById('watermarkSection').style.display = 'block'; | |
| document.getElementById('previewSection').style.display = 'block'; | |
| document.getElementById('processSection').style.display = 'block'; | |
| previewCanvas = document.getElementById('previewCanvas'); | |
| previewCtx = previewCanvas.getContext('2d'); | |
| video.addEventListener('click', function(e) { | |
| const rect = video.getBoundingClientRect(); | |
| const x = ((e.clientX - rect.left) / rect.width) * 100; | |
| const y = ((e.clientY - rect.top) / rect.height) * 100; | |
| watermarkPosition.x = Math.max(0, Math.min(95, x)); | |
| watermarkPosition.y = Math.max(0, Math.min(95, y)); | |
| drawWatermarkPreview(); | |
| }); | |
| video.addEventListener('loadedmetadata', function() { | |
| resizeCanvas(); | |
| drawWatermarkPreview(); | |
| }); | |
| video.addEventListener('timeupdate', drawWatermarkPreview); | |
| } | |
| }); | |
| function resizeCanvas() { | |
| const video = document.getElementById('previewVideo'); | |
| const canvas = document.getElementById('previewCanvas'); | |
| const rect = video.getBoundingClientRect(); | |
| canvas.width = rect.width; | |
| canvas.height = rect.height; | |
| canvas.style.width = rect.width + 'px'; | |
| canvas.style.height = rect.height + 'px'; | |
| } | |
| document.addEventListener('change', function(e) { | |
| if (e.target.name === 'watermarkType') { | |
| const textOptions = document.getElementById('textOptions'); | |
| const imageOptions = document.getElementById('imageOptions'); | |
| if (e.target.value === 'text') { | |
| textOptions.classList.remove('hidden'); | |
| imageOptions.classList.add('hidden'); | |
| } else { | |
| textOptions.classList.add('hidden'); | |
| imageOptions.classList.remove('hidden'); | |
| } | |
| drawWatermarkPreview(); | |
| } | |
| }); | |
| document.getElementById('imageInput').addEventListener('change', function(e) { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| const img = new Image(); | |
| img.onload = function() { | |
| watermarkImage = img; | |
| drawWatermarkPreview(); | |
| }; | |
| img.src = e.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| }); | |
| document.getElementById('fontSize').addEventListener('input', function(e) { | |
| document.getElementById('fontSizeValue').textContent = e.target.value; | |
| drawWatermarkPreview(); | |
| }); | |
| document.getElementById('textOpacity').addEventListener('input', function(e) { | |
| document.getElementById('textOpacityValue').textContent = Math.round(e.target.value * 100); | |
| drawWatermarkPreview(); | |
| }); | |
| document.getElementById('imageSize').addEventListener('input', function(e) { | |
| document.getElementById('imageSizeValue').textContent = e.target.value; | |
| drawWatermarkPreview(); | |
| }); | |
| document.getElementById('imageOpacity').addEventListener('input', function(e) { | |
| document.getElementById('imageOpacityValue').textContent = Math.round(e.target.value * 100); | |
| drawWatermarkPreview(); | |
| }); | |
| document.getElementById('watermarkText').addEventListener('input', drawWatermarkPreview); | |
| document.getElementById('textColor').addEventListener('input', drawWatermarkPreview); | |
| function setPosition(position) { | |
| switch(position) { | |
| case 'top-left': | |
| watermarkPosition = { x: 5, y: 5 }; | |
| break; | |
| case 'top-right': | |
| watermarkPosition = { x: 85, y: 5 }; | |
| break; | |
| case 'bottom-left': | |
| watermarkPosition = { x: 5, y: 85 }; | |
| break; | |
| case 'bottom-right': | |
| watermarkPosition = { x: 85, y: 85 }; | |
| break; | |
| case 'center': | |
| watermarkPosition = { x: 45, y: 45 }; | |
| break; | |
| } | |
| drawWatermarkPreview(); | |
| } | |
| function drawWatermarkPreview() { | |
| if (!previewCtx) return; | |
| const canvas = previewCanvas; | |
| const ctx = previewCtx; | |
| const watermarkType = document.querySelector('input[name="watermarkType"]:checked'); | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| if (!watermarkType) return; | |
| const x = (watermarkPosition.x / 100) * canvas.width; | |
| const y = (watermarkPosition.y / 100) * canvas.height; | |
| if (watermarkType.value === 'text') { | |
| const text = document.getElementById('watermarkText').value || 'Sample Text'; | |
| const fontSize = parseInt(document.getElementById('fontSize').value); | |
| const color = document.getElementById('textColor').value; | |
| const opacity = parseFloat(document.getElementById('textOpacity').value); | |
| const scaledFontSize = fontSize * (canvas.width / 800); | |
| ctx.font = `bold ${scaledFontSize}px Arial`; | |
| ctx.fillStyle = color; | |
| ctx.globalAlpha = opacity; | |
| ctx.shadowColor = 'black'; | |
| ctx.shadowBlur = 4; | |
| ctx.shadowOffsetX = 2; | |
| ctx.shadowOffsetY = 2; | |
| ctx.fillText(text, x, y + scaledFontSize); | |
| ctx.shadowColor = 'transparent'; | |
| ctx.shadowBlur = 0; | |
| ctx.shadowOffsetX = 0; | |
| ctx.shadowOffsetY = 0; | |
| ctx.globalAlpha = 1; | |
| } else if (watermarkType.value === 'image' && watermarkImage) { | |
| const size = parseInt(document.getElementById('imageSize').value); | |
| const opacity = parseFloat(document.getElementById('imageOpacity').value); | |
| const imgWidth = (size / 100) * canvas.width; | |
| const imgHeight = (watermarkImage.height / watermarkImage.width) * imgWidth; | |
| ctx.globalAlpha = opacity; | |
| ctx.drawImage(watermarkImage, x, y, imgWidth, imgHeight); | |
| ctx.globalAlpha = 1; | |
| } | |
| } | |
| async function processVideo() { | |
| if (!currentVideo) { | |
| showModal('Please select a video file first.'); | |
| return; | |
| } | |
| const watermarkType = document.querySelector('input[name="watermarkType"]:checked'); | |
| if (!watermarkType) { | |
| showModal('Please select a watermark type.'); | |
| return; | |
| } | |
| if (watermarkType.value === 'text' && !document.getElementById('watermarkText').value) { | |
| showModal('Please enter watermark text.'); | |
| return; | |
| } | |
| if (watermarkType.value === 'image' && !watermarkImage) { | |
| showModal('Please select a watermark image.'); | |
| return; | |
| } | |
| const exportFormat = document.querySelector('input[name="exportFormat"]:checked'); | |
| if (!exportFormat) { | |
| showModal('Please select an export format.'); | |
| return; | |
| } | |
| if (exportFormat.value === 'mp4' && !mp4Unlocked) { | |
| showModal('Please unlock MP4 export first using the unlock button.'); | |
| return; | |
| } | |
| const processBtn = document.getElementById('processBtn'); | |
| const progressContainer = document.getElementById('progressContainer'); | |
| const progressBar = document.getElementById('progressBar'); | |
| const progressText = document.getElementById('progressText'); | |
| processBtn.disabled = true; | |
| processBtn.textContent = 'Processing...'; | |
| progressContainer.classList.remove('hidden'); | |
| try { | |
| progressText.textContent = 'Preparing video...'; | |
| progressBar.style.width = '10%'; | |
| const video = document.createElement('video'); | |
| video.src = URL.createObjectURL(currentVideo); | |
| video.crossOrigin = 'anonymous'; | |
| await new Promise((resolve) => { | |
| video.addEventListener('loadedmetadata', resolve); | |
| video.load(); | |
| }); | |
| progressText.textContent = 'Setting up recording...'; | |
| progressBar.style.width = '20%'; | |
| const canvas = document.createElement('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| canvas.width = video.videoWidth; | |
| canvas.height = video.videoHeight; | |
| const videoStream = canvas.captureStream(30); | |
| let combinedStream = videoStream; | |
| try { | |
| const originalStream = video.captureStream(); | |
| const audioTracks = originalStream.getAudioTracks(); | |
| if (audioTracks.length > 0) { | |
| combinedStream = new MediaStream([ | |
| ...videoStream.getVideoTracks(), | |
| ...audioTracks | |
| ]); | |
| progressText.textContent = 'Recording with audio...'; | |
| } else { | |
| progressText.textContent = 'Recording video (no audio track found)...'; | |
| } | |
| } catch (audioError) { | |
| console.warn('Could not capture audio:', audioError); | |
| progressText.textContent = 'Recording video (audio capture failed)...'; | |
| } | |
| // Determine MIME type based on selected format | |
| let mimeType; | |
| let fileExtension; | |
| if (exportFormat.value === 'mp4') { | |
| // Try MP4 format if unlocked | |
| if (MediaRecorder.isTypeSupported('video/mp4;codecs=h264')) { | |
| mimeType = 'video/mp4;codecs=h264'; | |
| fileExtension = 'mp4'; | |
| } else if (MediaRecorder.isTypeSupported('video/mp4')) { | |
| mimeType = 'video/mp4'; | |
| fileExtension = 'mp4'; | |
| } else { | |
| // Fall back to WebM if MP4 not supported | |
| mimeType = 'video/webm;codecs=vp9'; | |
| fileExtension = 'webm'; | |
| progressText.textContent = 'MP4 not supported by browser, using WebM...'; | |
| } | |
| } else { | |
| // Default to WebM | |
| mimeType = 'video/webm;codecs=vp9'; | |
| fileExtension = 'webm'; | |
| } | |
| const recorder = new MediaRecorder(combinedStream, { mimeType }); | |
| const chunks = []; | |
| recorder.ondataavailable = (e) => chunks.push(e.data); | |
| progressBar.style.width = '40%'; | |
| recorder.start(); | |
| video.play(); | |
| const drawFrame = () => { | |
| if (video.ended || video.paused) { | |
| recorder.stop(); | |
| return; | |
| } | |
| ctx.drawImage(video, 0, 0, canvas.width, canvas.height); | |
| const x = (watermarkPosition.x / 100) * canvas.width; | |
| const y = (watermarkPosition.y / 100) * canvas.height; | |
| if (watermarkType.value === 'text') { | |
| const text = document.getElementById('watermarkText').value; | |
| const fontSize = parseInt(document.getElementById('fontSize').value); | |
| const color = document.getElementById('textColor').value; | |
| const opacity = parseFloat(document.getElementById('textOpacity').value); | |
| const scaledFontSize = fontSize * (canvas.width / 800); | |
| ctx.font = `bold ${scaledFontSize}px Arial`; | |
| ctx.fillStyle = color; | |
| ctx.globalAlpha = opacity; | |
| ctx.shadowColor = 'black'; | |
| ctx.shadowBlur = 4; | |
| ctx.shadowOffsetX = 2; | |
| ctx.shadowOffsetY = 2; | |
| ctx.fillText(text, x, y + scaledFontSize); | |
| ctx.shadowColor = 'transparent'; | |
| ctx.shadowBlur = 0; | |
| ctx.shadowOffsetX = 0; | |
| ctx.shadowOffsetY = 0; | |
| ctx.globalAlpha = 1; | |
| } else if (watermarkType.value === 'image' && watermarkImage) { | |
| const size = parseInt(document.getElementById('imageSize').value); | |
| const opacity = parseFloat(document.getElementById('imageOpacity').value); | |
| const imgWidth = (size / 100) * canvas.width; | |
| const imgHeight = (watermarkImage.height / watermarkImage.width) * imgWidth; | |
| ctx.globalAlpha = opacity; | |
| ctx.drawImage(watermarkImage, x, y, imgWidth, imgHeight); | |
| ctx.globalAlpha = 1; | |
| } | |
| const progress = (video.currentTime / video.duration) * 50; | |
| progressBar.style.width = (40 + progress) + '%'; | |
| requestAnimationFrame(drawFrame); | |
| }; | |
| drawFrame(); | |
| recorder.onstop = () => { | |
| progressText.textContent = 'Finalizing video...'; | |
| progressBar.style.width = '95%'; | |
| const blob = new Blob(chunks, { type: mimeType }); | |
| const url = URL.createObjectURL(blob); | |
| progressText.textContent = 'Complete!'; | |
| progressBar.style.width = '100%'; | |
| const downloadLink = document.getElementById('downloadLink'); | |
| downloadLink.href = url; | |
| downloadLink.download = `watermarked_${currentVideo.name.replace(/\.[^/.]+$/, '')}.${fileExtension}`; | |
| document.getElementById('downloadContainer').classList.remove('hidden'); | |
| }; | |
| } catch (error) { | |
| console.error('Processing error:', error); | |
| showModal('Error processing video: ' + error.message); | |
| } finally { | |
| processBtn.disabled = false; | |
| processBtn.textContent = 'Add Watermark & Download'; | |
| } | |
| } | |
| window.addEventListener('resize', () => { | |
| if (previewCanvas) { | |
| setTimeout(() => { | |
| resizeCanvas(); | |
| drawWatermarkPreview(); | |
| }, 100); | |
| } | |
| }); | |
| </script> | |
| </html> |