Skip to content

Instantly share code, notes, and snippets.

@badrshs
Last active December 28, 2024 13:33
Show Gist options
  • Save badrshs/9b7f3c3042896b621aed1fe974ea7eed to your computer and use it in GitHub Desktop.
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.
<!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>
@badrshs
Copy link
Author

badrshs commented Dec 28, 2024

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment