plugin / index.html
TDN-M's picture
Update index.html
9b97325 verified
raw
history blame
10.5 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OBS AI Background Remover</title>
<link rel="stylesheet" href="style.css">
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation/selfie_segmentation.js" crossorigin="anonymous"></script>
</head>
<body>
<div class="card">
<h1>OBS AI Background Remover</h1>
<div>
<h2>Live Preview</h2>
<video id="videoPreview" autoplay playsinline></video>
<canvas id="canvasOutput" width="1920" height="1080" style="display: none;"></canvas>
<p id="status">Loading AI Model...</p>
</div>
<div>
<h2>Background Replacement</h2>
<div>
<input type="radio" name="bgType" id="bgTransparent" value="transparent" checked>
<label for="bgTransparent">Transparent</label>
</div>
<div>
<input type="radio" name="bgType" id="bgBlack" value="black">
<label for="bgBlack">Solid Black</label>
</div>
<div>
<input type="radio" name="bgType" id="bgGray" value="gray">
<label for="bgGray">Solid Gray</label>
</div>
<div>
<input type="radio" name="bgType" id="bgGreen" value="green">
<label for="bgGreen">Solid Green</label>
</div>
<div>
<input type="radio" name="bgType" id="bgCustomImage" value="customImage">
<label for="bgCustomImage">Custom Image</label>
<input type pigeonhole="file" id="customImageInput" accept="image/*">
</div>
<div>
<input type="radio" name="bgType" id="bgCustomVideo" value="customVideo">
<label for="bgCustomVideo">Custom Video</label>
<input type="file" id="customVideoInput" accept="video/*">
</div>
<div>
<input type="radio" name="bgType" id="bgBlur" value="blur">
<label for="bgBlur">Blurred</label>
</div>
</div>
<div>
<h2>Controls</h2>
<div>
<label for="videoSource">Video Source</label>
<select id="videoSource">
<option value="">Select a video source</option>
</select>
</div>
<div>
<input type="checkbox" id="enableBgRemoval">
<label for="enableBgRemoval">Enable Background Removal</label>
</div>
<div>
<input type="checkbox" id="showPreview" checked>
<label for="showPreview">Show Preview</label>
</div>
</div>
<div>
<h2>Performance</h2>
<p>Processing Time: <span id="processingTime">0</span> ms</p>
<p>FPS: <span id="fps">0</span></p>
</div>
<div>
<h2>OBS Integration</h2>
<p>Add this as a Browser Source in OBS with these settings:</p>
<p>Width: 1920</p>
<p>Height: 1080</p>
<p>Custom CSS: None</p>
<button id="copyUrl">Copy OBS Browser Source URL</button>
</div>
</div>
<script>
const videoElement = document.getElementById('videoPreview');
const canvasElement = document.getElementById('canvasOutput');
const canvasCtx = canvasElement.getContext('2d');
const statusElement = document.getElementById('status');
const videoSourceSelect = document.getElementById('videoSource');
const enableBgRemoval = document.getElementById('enableBgRemoval');
const showPreview = document.getElementById('showPreview');
const processingTimeElement = document.getElementById('processingTime');
const fpsElement = document.getElementById('fps');
const copyUrlButton = document.getElementById('copyUrl');
let selfieSegmentation;
let lastFrameTime = 0;
let frameCount = 0;
let lastFpsUpdate = 0;
// Initialize MediaPipe Selfie Segmentation
async function initModel() {
selfieSegmentation = new SelfieSegmentation({
locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation/${file}`
});
selfieSegmentation.setOptions({
modelSelection: 1, // 0 for general, 1 for landscape
});
selfieSegmentation.onResults(onResults);
await selfieSegmentation.initialize();
statusElement.textContent = 'AI Model Loaded';
}
// Populate video sources
async function populateVideoSources() {
const devices = await navigator.mediaDevices.enumerateDevices();
videoSourceSelect.innerHTML = '<option value="">Select a video source</option>';
devices.forEach(device => {
if (device.kind === 'videoinput') {
const option = document.createElement('option');
option.value = device.deviceId;
option.text = device.label || `Camera ${videoSourceSelect.options.length + 1}`;
videoSourceSelect.appendChild(option);
}
});
}
// Start video stream
async function startVideo(deviceId) {
const stream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: deviceId ? { exact: deviceId } : undefined }
});
videoElement.srcObject = stream;
videoElement.play();
}
// Process video frames
function onResults(results) {
if (!enableBgRemoval.checked) {
canvasCtx.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height);
updatePerformance();
return;
}
canvasCtx.save();
canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
// Draw segmented background
const bgType = document.querySelector('input[name="bgType"]:checked').value;
if (bgType === 'transparent') {
canvasCtx.globalCompositeOperation = 'destination-over';
canvasCtx.fillStyle = 'rgba(0, 0, 0, 0)';
canvasCtx.fillRect(0, 0, canvasElement.width, canvasElement.height);
} else if (bgType === 'black') {
canvasCtx.fillStyle = 'black';
canvasCtx.fillRect(0, 0, canvasElement.width, canvasElement.height);
} else if (bgType === 'gray') {
canvasCtx.fillStyle = 'gray';
canvasCtx.fillRect(0, 0, canvasElement.width, canvasElement.height);
} else if (bgType === 'green') {
canvasCtx.fillStyle = 'green';
canvasCtx.fillRect(0, 0, canvasElement.width, canvasElement.height);
} else if (bgType === 'customImage') {
const img = document.getElementById('customImageInput').files[0];
if (img) {
const image = new Image();
image.src = URL.createObjectURL(img);
canvasCtx.drawImage(image, 0, 0, canvasElement.width, canvasElement.height);
}
} else if (bgType === 'customVideo') {
const vid = document.getElementById('customVideoInput').files[0];
if (vid) {
const video = document.createElement('video');
video.src = URL.createObjectURL(vid);
video.loop = true;
video.play();
canvasCtx.drawImage(video, 0, 0, canvasElement.width, canvasElement.height);
}
} else if (bgType === 'blur') {
canvasCtx.filter = 'blur(10px)';
canvasCtx.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height);
canvasCtx.filter = 'none';
}
// Draw foreground (person)
canvasCtx.globalCompositeOperation = 'destination-atop';
canvasCtx.drawImage(results.segmentationMask, 0, 0, canvasElement.width, canvasElement.height);
canvasCtx.globalCompositeOperation = 'source-over';
canvasCtx.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height);
canvasCtx.restore();
updatePerformance();
}
// Update performance metrics
function updatePerformance() {
const now = performance.now();
frameCount++;
if (now - lastFpsUpdate > 1000) {
const fps = frameCount / ((now - lastFpsUpdate) / 1000);
fpsElement.textContent = fps.toFixed(1);
frameCount = 0;
lastFpsUpdate = now;
}
processingTimeElement.textContent = (now - lastFrameTime).toFixed(1);
lastFrameTime = now;
}
// Copy URL
copyUrlButton.addEventListener('click', () => {
const url = window.location.href;
navigator.clipboard.writeText(url).then(() => {
alert('URL copied to clipboard!');
});
});
// Event listeners
videoSourceSelect.addEventListener('change', () => {
startVideo(videoSourceSelect.value);
});
enableBgRemoval.addEventListener('change', () => {
canvasElement.style.display = enableBgRemoval.checked ? 'block' : 'none';
videoElement.style.display = !enableBgRemoval.checked ? 'block' : 'none';
});
showPreview.addEventListener('change', () => {
document.querySelector('.card').style.display = showPreview.checked ? 'block' : 'none';
});
// Process video frames
async function processFrame() {
if (enableBgRemoval.checked) {
await selfieSegmentation.send({ image: videoElement });
}
requestAnimationFrame(processFrame);
}
// Initialize
async function init() {
await initModel();
await populateVideoSources();
await startVideo();
processFrame();
}
init();
</script>
</body>
</html>