Last active
December 28, 2024 13:33
-
-
Save badrshs/9b7f3c3042896b621aed1fe974ea7eed to your computer and use it in GitHub Desktop.
Add watermark to videos directly in the browser. Upload a video and a watermark image, adjust scale and position, and download the processed video. Simple and user-friendly interface using pure JavaScript.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Watermark into Video </title> | |
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
background-color: #f8f9fa; | |
} | |
.main-container { | |
max-width: 1400px; | |
margin: 0 auto; | |
} | |
.options-panel { | |
background: white; | |
border-radius: 15px; | |
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.05); | |
height: calc(100vh - 2rem); | |
overflow-y: auto; | |
} | |
.video-panel { | |
background: white; | |
border-radius: 15px; | |
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.05); | |
height: calc(100vh - 2rem); | |
display: flex; | |
flex-direction: column; | |
} | |
.upload-zone { | |
border: 2px dashed #e9ecef; | |
border-radius: 10px; | |
padding: 1.5rem; | |
text-align: center; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
background: #fcfcfc; | |
} | |
.upload-zone:hover { | |
border-color: #0d6efd; | |
background: #f8f9fa; | |
} | |
.file-name { | |
margin-top: 8px; | |
font-size: 0.85rem; | |
color: #6c757d; | |
} | |
#videoContainer { | |
flex-grow: 1; | |
position: relative; | |
background: #000; | |
border-radius: 10px; | |
overflow: hidden; | |
margin: 1rem; | |
} | |
#outputVideo { | |
width: 100%; | |
height: 100%; | |
object-fit: contain; | |
} | |
.progress { | |
height: 8px; | |
border-radius: 4px; | |
} | |
.options-section { | |
padding: 1.25rem; | |
border-bottom: 1px solid #f0f0f0; | |
} | |
.options-section:last-child { | |
border-bottom: none; | |
} | |
.section-title { | |
font-size: 0.9rem; | |
text-transform: uppercase; | |
letter-spacing: 0.5px; | |
color: #6c757d; | |
margin-bottom: 1rem; | |
} | |
.btn-process { | |
width: 100%; | |
padding: 0.8rem; | |
} | |
#canvas { | |
display: none; | |
} | |
.panel-header { | |
padding: 1.25rem; | |
border-bottom: 1px solid #f0f0f0; | |
} | |
.panel-title { | |
font-size: 1.1rem; | |
font-weight: 600; | |
color: #212529; | |
margin: 0; | |
} | |
.controls-footer { | |
padding: 1rem; | |
background: #fcfcfc; | |
border-top: 1px solid #f0f0f0; | |
border-radius: 0 0 15px 15px; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="main-container p-3"> | |
<div class="row g-4"> | |
<!-- Options Panel --> | |
<div class="col-lg-4"> | |
<div class="options-panel"> | |
<div class="panel-header"> | |
<h5 class="panel-title"> | |
<i class="fas fa-cog me-2"></i>Watermark Settings | |
</h5> | |
</div> | |
<!-- File Uploads Section --> | |
<div class="options-section"> | |
<div class="section-title">Upload Files</div> | |
<div class="mb-3"> | |
<label class="upload-zone w-100" id="videoUploadLabel"> | |
<input type="file" id="videoInput" accept="video/*" class="d-none"> | |
<i class="fas fa-video fa-2x mb-2 text-primary"></i> | |
<h6 class="mb-1">Select Video</h6> | |
<div class="file-name" id="videoFileName">No file chosen</div> | |
</label> | |
</div> | |
<div> | |
<label class="upload-zone w-100" id="watermarkUploadLabel"> | |
<input type="file" id="watermarkInput" accept="image/*" class="d-none"> | |
<i class="fas fa-image fa-2x mb-2 text-primary"></i> | |
<h6 class="mb-1">Select Watermark</h6> | |
<div class="file-name" id="watermarkFileName">No file chosen</div> | |
</label> | |
</div> | |
</div> | |
<!-- Watermark Options Section --> | |
<div class="options-section"> | |
<div class="section-title">Watermark Options</div> | |
<div class="mb-3"> | |
<label class="form-label">Scale</label> | |
<div class="d-flex align-items-center gap-2"> | |
<input type="range" class="form-range" id="watermarkScale" min="1" max="50" value="30"> | |
<span class="badge bg-primary" id="scaleValue" style="width: 4rem;">30%</span> | |
</div> | |
</div> | |
<div> | |
<label class="form-label">Position</label> | |
<select class="form-select" id="watermarkPosition"> | |
<option value="top-right">Top Right</option> | |
<option value="bottom-right">Bottom Right</option> | |
<option value="bottom-left">Bottom Left</option> | |
<option value="top-left">Top Left</option> | |
<option value="center">Center</option> | |
</select> | |
</div> | |
</div> | |
<!-- Process Controls --> | |
<div class="options-section"> | |
<div id="processingStatus" class="alert alert-info mb-3" style="display: none;"> | |
<i class="fas fa-spinner fa-spin me-2"></i>Processing video... | |
</div> | |
<div class="progress mb-3" style="display: none;" id="progressBar"> | |
<div class="progress-bar progress-bar-striped progress-bar-animated" id="progressFill"></div> | |
</div> | |
<button id="processButton" class="btn btn-primary btn-process"> | |
<i class="fas fa-cog me-2"></i>Process Video | |
</button> | |
</div> | |
</div> | |
</div> | |
<!-- Video Preview Panel --> | |
<div class="col-lg-8"> | |
<div class="video-panel"> | |
<div class="panel-header"> | |
<h5 class="panel-title"> | |
<i class="fas fa-film me-2"></i>Video Preview | |
</h5> | |
</div> | |
<div id="videoContainer"> | |
<canvas id="canvas"></canvas> | |
<video id="outputVideo" controls></video> | |
</div> | |
<div class="controls-footer"> | |
<a id="downloadLink" class="btn btn-success w-100" style="display: none;"> | |
<i class="fas fa-download me-2"></i>Download Processed Video | |
</a> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> | |
<script> | |
const videoInput = document.getElementById('videoInput'); | |
const watermarkInput = document.getElementById('watermarkInput'); | |
const canvas = document.getElementById('canvas'); | |
const ctx = canvas.getContext('2d'); | |
const outputVideo = document.getElementById('outputVideo'); | |
const processButton = document.getElementById('processButton'); | |
const downloadLink = document.getElementById('downloadLink'); | |
const watermarkScale = document.getElementById('watermarkScale'); | |
const scaleValue = document.getElementById('scaleValue'); | |
const watermarkPosition = document.getElementById('watermarkPosition'); | |
const processingStatus = document.getElementById('processingStatus'); | |
const progressBar = document.getElementById('progressBar'); | |
const progressFill = document.getElementById('progressFill'); | |
// Add file name display functionality | |
videoInput.addEventListener('change', (e) => { | |
const fileName = e.target.files[0]?.name || 'No file chosen'; | |
document.getElementById('videoFileName').textContent = fileName; | |
}); | |
watermarkInput.addEventListener('change', (e) => { | |
const fileName = e.target.files[0]?.name || 'No file chosen'; | |
document.getElementById('watermarkFileName').textContent = fileName; | |
}); | |
let mediaRecorder; | |
let recordedChunks = []; | |
let originalVideoType; | |
let isProcessing = false; | |
watermarkScale.addEventListener('input', (e) => { | |
scaleValue.textContent = `${e.target.value}%`; | |
}); | |
async function processVideo() { | |
if (isProcessing) return; | |
const videoFile = videoInput.files[0]; | |
const watermarkFile = watermarkInput.files[0]; | |
if (!videoFile || !watermarkFile) { | |
alert('Please select both video and watermark files'); | |
return; | |
} | |
isProcessing = true; | |
originalVideoType = videoFile.type; | |
processingStatus.style.display = 'block'; | |
progressBar.style.display = 'block'; | |
progressFill.style.width = '0%'; | |
downloadLink.style.display = 'none'; | |
recordedChunks = []; | |
processButton.disabled = true; | |
try { | |
const videoURL = URL.createObjectURL(videoFile); | |
const tempVideo = document.createElement('video'); | |
tempVideo.src = videoURL; | |
await new Promise((resolve, reject) => { | |
tempVideo.onloadedmetadata = resolve; | |
tempVideo.onerror = reject; | |
}); | |
canvas.width = tempVideo.videoWidth; | |
canvas.height = tempVideo.videoHeight; | |
const watermark = new Image(); | |
watermark.src = URL.createObjectURL(watermarkFile); | |
await new Promise(resolve => { | |
watermark.onload = resolve; | |
}); | |
const audioContext = new AudioContext(); | |
const audioSource = audioContext.createMediaElementSource(tempVideo); | |
const audioDestination = audioContext.createMediaStreamDestination(); | |
audioSource.connect(audioDestination); | |
audioSource.connect(audioContext.destination); | |
const canvasStream = canvas.captureStream(30); | |
const combinedStream = new MediaStream(); | |
canvasStream.getVideoTracks().forEach(track => { | |
combinedStream.addTrack(track); | |
}); | |
audioDestination.stream.getAudioTracks().forEach(track => { | |
combinedStream.addTrack(track); | |
}); | |
// Prioritized codec list including MP4 | |
const supportedCodecs = [ | |
'video/mp4;codecs=h264,aac', | |
'video/mp4;codecs=avc1', | |
'video/webm;codecs=h264,opus', | |
'video/webm;codecs=vp9,opus', | |
'video/webm;codecs=vp8,opus', | |
'video/webm' | |
]; | |
let selectedCodec = supportedCodecs.find(codec => MediaRecorder.isTypeSupported(codec)); | |
if (!selectedCodec) { | |
throw new Error('No supported codec found'); | |
} | |
mediaRecorder = new MediaRecorder(combinedStream, { | |
mimeType: selectedCodec, | |
videoBitsPerSecond: 8000000 | |
}); | |
mediaRecorder.ondataavailable = (event) => { | |
if (event.data.size > 0) { | |
recordedChunks.push(event.data); | |
} | |
}; | |
mediaRecorder.onstop = async () => { | |
const blob = new Blob(recordedChunks, { type: selectedCodec }); | |
// If the original video was MP4 and we couldn't record in MP4, convert using MediaSource | |
if (originalVideoType.includes('mp4') && !selectedCodec.includes('mp4')) { | |
try { | |
const response = await fetch(URL.createObjectURL(blob)); | |
const arrayBuffer = await response.arrayBuffer(); | |
// Create a new Blob with MP4 container | |
const mp4Blob = new Blob([arrayBuffer], { type: 'video/mp4' }); | |
const url = URL.createObjectURL(mp4Blob); | |
outputVideo.src = url; | |
downloadLink.href = url; | |
downloadLink.download = 'watermarked-video.mp4'; | |
} catch (error) { | |
console.error('Error converting to MP4:', error); | |
// Fallback to original format | |
const url = URL.createObjectURL(blob); | |
outputVideo.src = url; | |
downloadLink.href = url; | |
downloadLink.download = 'watermarked-video.webm'; | |
} | |
} else { | |
const url = URL.createObjectURL(blob); | |
outputVideo.src = url; | |
downloadLink.href = url; | |
downloadLink.download = selectedCodec.includes('mp4') ? | |
'watermarked-video.mp4' : 'watermarked-video.webm'; | |
} | |
downloadLink.style.display = 'inline-block'; | |
processingStatus.style.display = 'none'; | |
progressBar.style.display = 'none'; | |
processButton.disabled = false; | |
isProcessing = false; | |
canvasStream.getTracks().forEach(track => track.stop()); | |
combinedStream.getTracks().forEach(track => track.stop()); | |
}; | |
mediaRecorder.start(0); | |
const renderFrame = () => { | |
if (tempVideo.ended || tempVideo.paused) { | |
mediaRecorder.stop(); | |
return; | |
} | |
ctx.drawImage(tempVideo, 0, 0, canvas.width, canvas.height); | |
const scale = watermarkScale.value / 100; | |
const watermarkRatio = watermark.width / watermark.height; | |
const newWatermarkWidth = canvas.width * scale; | |
const newWatermarkHeight = newWatermarkWidth / watermarkRatio; | |
let x, y; | |
const padding = 20; | |
switch (watermarkPosition.value) { | |
case 'bottom-right': | |
x = canvas.width - newWatermarkWidth - padding; | |
y = canvas.height - newWatermarkHeight - padding; | |
break; | |
case 'bottom-left': | |
x = padding; | |
y = canvas.height - newWatermarkHeight - padding; | |
break; | |
case 'top-right': | |
x = canvas.width - newWatermarkWidth - padding; | |
y = padding; | |
break; | |
case 'top-left': | |
x = padding; | |
y = padding; | |
break; | |
case 'center': | |
x = (canvas.width - newWatermarkWidth) / 2; | |
y = (canvas.height - newWatermarkHeight) / 2; | |
break; | |
} | |
ctx.drawImage(watermark, x, y, newWatermarkWidth, newWatermarkHeight); | |
const progress = (tempVideo.currentTime / tempVideo.duration) * 100; | |
progressFill.style.width = `${progress}%`; | |
requestAnimationFrame(renderFrame); | |
}; | |
tempVideo.addEventListener('ended', () => { | |
mediaRecorder.stop(); | |
}); | |
await tempVideo.play(); | |
renderFrame(); | |
} catch (error) { | |
console.error('Error processing video:', error); | |
alert('Error processing video. Please try again.'); | |
processingStatus.style.display = 'none'; | |
progressBar.style.display = 'none'; | |
processButton.disabled = false; | |
isProcessing = false; | |
} | |
} | |
processButton.addEventListener('click', processVideo); | |
</script> | |
</body> | |
</html> |
Author
badrshs
commented
Dec 28, 2024
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment