|
|
|
|
|
|
|
|
|
|
|
|
|
|
<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> |