Overview
Watermarking adds logos, text, or graphics to video frames. Common use cases:- Brand identification (company logo)
- Copyright protection (watermark text)
- Timestamping (date/time overlay)
- Attribution (creator credits)
Prerequisites
Install thecanvas package for image/text rendering:
Copy
npm install canvas
Add Logo Watermark
Copy
const { VideoEncoder, VideoDecoder, VideoFrame } = require('node-webcodecs');
const { createCanvas, loadImage } = require('canvas');
const fs = require('fs');
async function addLogoWatermark(inputChunks, decoderConfig, logoPath) {
const outputChunks = [];
// Load watermark logo
const logo = await loadImage(logoPath);
const logoWidth = 200;
const logoHeight = Math.floor(logo.height * (logoWidth / logo.width));
// Create encoder
const encoder = new VideoEncoder({
output: (chunk) => {
const buffer = Buffer.alloc(chunk.byteLength);
chunk.copyTo(buffer);
outputChunks.push(buffer);
},
error: (err) => { throw err; }
});
encoder.configure({
codec: decoderConfig.codec,
width: decoderConfig.codedWidth,
height: decoderConfig.codedHeight,
bitrate: 5_000_000
});
// Create decoder
const decoder = new VideoDecoder({
output: async (frame) => {
// Get frame pixels
const size = frame.allocationSize({ format: 'RGBA' });
const pixels = new Uint8Array(size);
await frame.copyTo(pixels);
// Create canvas
const canvas = createCanvas(frame.codedWidth, frame.codedHeight);
const ctx = canvas.getContext('2d');
// Draw original frame
const imageData = ctx.createImageData(frame.codedWidth, frame.codedHeight);
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
// Draw logo watermark (bottom-right corner)
const x = frame.codedWidth - logoWidth - 20;
const y = frame.codedHeight - logoHeight - 20;
ctx.globalAlpha = 0.7; // Semi-transparent
ctx.drawImage(logo, x, y, logoWidth, logoHeight);
// Get watermarked pixels
const watermarked = ctx.getImageData(0, 0, frame.codedWidth, frame.codedHeight);
// Create new frame
const newFrame = new VideoFrame(watermarked.data, {
format: 'RGBA',
codedWidth: frame.codedWidth,
codedHeight: frame.codedHeight,
timestamp: frame.timestamp
});
encoder.encode(newFrame);
newFrame.close();
frame.close();
},
error: (err) => { throw err; }
});
decoder.configure(decoderConfig);
// Process
for (const chunk of inputChunks) {
decoder.decode(chunk);
}
await decoder.flush();
decoder.close();
await encoder.flush();
encoder.close();
return Buffer.concat(outputChunks);
}
// Usage
const output = await addLogoWatermark(chunks, config, 'logo.png');
fs.writeFileSync('watermarked.h264', output);
Add Text Watermark
Copy
async function addTextWatermark(inputChunks, decoderConfig, text) {
const outputChunks = [];
const encoder = new VideoEncoder({
output: (chunk) => {
const buffer = Buffer.alloc(chunk.byteLength);
chunk.copyTo(buffer);
outputChunks.push(buffer);
},
error: (err) => { throw err; }
});
encoder.configure({
codec: decoderConfig.codec,
width: decoderConfig.codedWidth,
height: decoderConfig.codedHeight,
bitrate: 5_000_000
});
const decoder = new VideoDecoder({
output: async (frame) => {
const size = frame.allocationSize({ format: 'RGBA' });
const pixels = new Uint8Array(size);
await frame.copyTo(pixels);
const canvas = createCanvas(frame.codedWidth, frame.codedHeight);
const ctx = canvas.getContext('2d');
// Draw frame
const imageData = ctx.createImageData(frame.codedWidth, frame.codedHeight);
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
// Draw text watermark
ctx.font = 'bold 48px Arial';
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
ctx.lineWidth = 3;
// Center text at bottom
const textMetrics = ctx.measureText(text);
const x = (frame.codedWidth - textMetrics.width) / 2;
const y = frame.codedHeight - 40;
ctx.strokeText(text, x, y);
ctx.fillText(text, x, y);
// Encode watermarked frame
const watermarked = ctx.getImageData(0, 0, frame.codedWidth, frame.codedHeight);
const newFrame = new VideoFrame(watermarked.data, {
format: 'RGBA',
codedWidth: frame.codedWidth,
codedHeight: frame.codedHeight,
timestamp: frame.timestamp
});
encoder.encode(newFrame);
newFrame.close();
frame.close();
},
error: (err) => { throw err; }
});
decoder.configure(decoderConfig);
for (const chunk of inputChunks) {
decoder.decode(chunk);
}
await decoder.flush();
decoder.close();
await encoder.flush();
encoder.close();
return Buffer.concat(outputChunks);
}
// Usage
const output = await addTextWatermark(chunks, config, '© 2024 Your Company');
Animated Watermark
Create a watermark that moves or changes over time:Copy
async function addAnimatedWatermark(inputChunks, decoderConfig) {
const outputChunks = [];
let frameCount = 0;
const encoder = new VideoEncoder({
output: (chunk) => {
const buffer = Buffer.alloc(chunk.byteLength);
chunk.copyTo(buffer);
outputChunks.push(buffer);
},
error: (err) => { throw err; }
});
encoder.configure({
codec: decoderConfig.codec,
width: decoderConfig.codedWidth,
height: decoderConfig.codedHeight,
bitrate: 5_000_000
});
const decoder = new VideoDecoder({
output: async (frame) => {
const size = frame.allocationSize({ format: 'RGBA' });
const pixels = new Uint8Array(size);
await frame.copyTo(pixels);
const canvas = createCanvas(frame.codedWidth, frame.codedHeight);
const ctx = canvas.getContext('2d');
// Draw frame
const imageData = ctx.createImageData(frame.codedWidth, frame.codedHeight);
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
// Animated watermark (moves across screen)
const progress = (frameCount % 60) / 60; // Loop every 2 seconds at 30fps
const x = progress * frame.codedWidth;
const y = 50;
// Draw watermark
ctx.font = 'bold 36px Arial';
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
ctx.shadowBlur = 5;
ctx.fillText('WATERMARK', x, y);
frameCount++;
// Encode
const watermarked = ctx.getImageData(0, 0, frame.codedWidth, frame.codedHeight);
const newFrame = new VideoFrame(watermarked.data, {
format: 'RGBA',
codedWidth: frame.codedWidth,
codedHeight: frame.codedHeight,
timestamp: frame.timestamp
});
encoder.encode(newFrame);
newFrame.close();
frame.close();
},
error: (err) => { throw err; }
});
decoder.configure(decoderConfig);
for (const chunk of inputChunks) {
decoder.decode(chunk);
}
await decoder.flush();
decoder.close();
await encoder.flush();
encoder.close();
return Buffer.concat(outputChunks);
}
Best Practices
- Use semi-transparent watermarks (alpha 0.5-0.8)
- Position in corner to avoid obscuring content
- Keep file size reasonable (10-20% of frame size)
- Use high contrast colors for visibility
- Consider animated watermarks to prevent removal