| | |
| | |
| | |
| | |
| | <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> |
| | |
| | |
| | <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'); |
| | } |
| | |
| | |
| | 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')) { |
| | |
| | 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 { |
| | |
| | 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)...'; |
| | } |
| | |
| | |
| | let mimeType; |
| | let fileExtension; |
| | |
| | if (exportFormat.value === 'mp4') { |
| | |
| | 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 { |
| | |
| | mimeType = 'video/webm;codecs=vp9'; |
| | fileExtension = 'webm'; |
| | progressText.textContent = 'MP4 not supported by browser, using WebM...'; |
| | } |
| | } else { |
| | |
| | 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> |