logitech-brio-zoom-control
Version:
Cross-platform native library for controlling Logitech MX Brio camera zoom with 4K video support in Electron applications
639 lines (566 loc) • 24.4 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Logitech MX Brio Zoom Control - Electron Example</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
padding: 30px;
}
h1 {
color: #333;
text-align: center;
margin-bottom: 30px;
}
.section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 6px;
background: #fafafa;
}
.section h2 {
margin-top: 0;
color: #444;
}
button {
background: #007acc;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
margin-bottom: 10px;
font-size: 14px;
}
button:hover {
background: #005999;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
input[type="range"] {
width: 100%;
margin: 10px 0;
}
input[type="number"] {
width: 80px;
padding: 5px;
border: 1px solid #ccc;
border-radius: 3px;
}
.status {
padding: 10px;
border-radius: 4px;
margin: 10px 0;
font-weight: bold;
}
.status.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.status.info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.device-list {
background: white;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
margin: 10px 0;
}
.device-item {
padding: 8px;
border-bottom: 1px solid #eee;
}
.device-item:last-child {
border-bottom: none;
}
.zoom-controls {
display: flex;
align-items: center;
gap: 10px;
margin: 15px 0;
}
.zoom-display {
font-size: 18px;
font-weight: bold;
color: #007acc;
min-width: 60px;
}
.capabilities {
background: white;
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
margin: 10px 0;
}
.capabilities-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
}
.capability-item {
display: flex;
justify-content: space-between;
padding: 5px 0;
border-bottom: 1px solid #eee;
}
</style>
</head>
<body>
<div class="container">
<h1>🎥 Logitech MX Brio Zoom Control</h1>
<div id="status" class="status info">
Ready to discover cameras...
</div>
<!-- Live Video Preview Section -->
<div class="section">
<h2>📹 Live Video Preview</h2>
<div style="text-align: center; margin-bottom: 15px;">
<video id="video-preview" width="800" height="600" autoplay style="background: #000; border-radius: 8px; border: 2px solid #ddd; max-width: 100%;"></video>
</div>
<div style="text-align: center;">
<button id="start-video-btn">Start Video Preview</button>
<button id="stop-video-btn" disabled>Stop Video Preview</button>
<select id="video-quality">
<option value="3840x2160">4K UHD (3840x2160)</option>
<option value="2560x1440">1440p (2560x1440)</option>
<option value="1920x1080" selected>1080p (1920x1080)</option>
<option value="1280x720">720p (1280x720)</option>
<option value="640x480">480p (640x480)</option>
</select>
</div>
</div>
<!-- Zoom Control Section -->
<div class="section">
<h2>🎛️ Zoom Control</h2>
<div class="zoom-controls">
<label>Current Zoom:</label>
<span id="current-zoom" class="zoom-display">--</span>
<button id="refresh-zoom-btn" disabled>Refresh</button>
</div>
<div class="zoom-controls">
<label>Zoom Slider:</label>
<input type="range" id="zoom-slider" min="100" max="500" value="100" disabled>
<input type="number" id="zoom-input" min="100" max="500" value="100" disabled>
<button id="set-zoom-btn" disabled>Set</button>
</div>
<div class="zoom-controls">
<button id="zoom-in-btn" disabled>Zoom In (+10)</button>
<button id="zoom-out-btn" disabled>Zoom Out (-10)</button>
<button id="reset-zoom-btn" disabled>Reset Zoom</button>
<button id="max-zoom-btn" disabled>Max Zoom</button>
</div>
<div class="zoom-controls">
<label>Custom Relative:</label>
<input type="number" id="relative-input" value="10" style="width: 60px;">
<button id="relative-plus-btn" disabled>+</button>
<button id="relative-minus-btn" disabled>-</button>
</div>
</div>
<!-- Library Info Section -->
<div class="section">
<h2>📋 Library Information</h2>
<div id="library-info">Loading...</div>
</div>
<!-- Device Discovery Section -->
<div class="section">
<h2>🔍 Device Discovery</h2>
<button id="discover-btn">Discover Devices</button>
<button id="init-first-btn" disabled>Initialize First Device</button>
<button id="release-btn" disabled>Release Device</button>
<div id="device-list" class="device-list" style="display: none;"></div>
</div>
<!-- Zoom Capabilities Section -->
<div class="section">
<h2>📏 Zoom Capabilities</h2>
<button id="get-capabilities-btn" disabled>Get Capabilities</button>
<div id="capabilities" class="capabilities" style="display: none;"></div>
</div>
</div>
<script>
let isDeviceInitialized = false;
let zoomCapabilities = null;
let videoStream = null;
// DOM elements
const statusEl = document.getElementById('status');
const libraryInfoEl = document.getElementById('library-info');
const deviceListEl = document.getElementById('device-list');
const capabilitiesEl = document.getElementById('capabilities');
const currentZoomEl = document.getElementById('current-zoom');
const zoomSlider = document.getElementById('zoom-slider');
const zoomInput = document.getElementById('zoom-input');
const videoPreview = document.getElementById('video-preview');
const startVideoBtn = document.getElementById('start-video-btn');
const stopVideoBtn = document.getElementById('stop-video-btn');
const videoQualitySelect = document.getElementById('video-quality');
// Buttons
const discoverBtn = document.getElementById('discover-btn');
const initFirstBtn = document.getElementById('init-first-btn');
const releaseBtn = document.getElementById('release-btn');
const getCapabilitiesBtn = document.getElementById('get-capabilities-btn');
const refreshZoomBtn = document.getElementById('refresh-zoom-btn');
const setZoomBtn = document.getElementById('set-zoom-btn');
const zoomInBtn = document.getElementById('zoom-in-btn');
const zoomOutBtn = document.getElementById('zoom-out-btn');
const resetZoomBtn = document.getElementById('reset-zoom-btn');
const maxZoomBtn = document.getElementById('max-zoom-btn');
const relativePlusBtn = document.getElementById('relative-plus-btn');
const relativeMinusBtn = document.getElementById('relative-minus-btn');
const relativeInput = document.getElementById('relative-input');
// Utility functions
function showStatus(message, type = 'info') {
statusEl.textContent = message;
statusEl.className = `status ${type}`;
}
function updateDeviceButtons(enabled) {
isDeviceInitialized = enabled;
releaseBtn.disabled = !enabled;
getCapabilitiesBtn.disabled = !enabled;
refreshZoomBtn.disabled = !enabled;
setZoomBtn.disabled = !enabled;
zoomInBtn.disabled = !enabled;
zoomOutBtn.disabled = !enabled;
resetZoomBtn.disabled = !enabled;
maxZoomBtn.disabled = !enabled;
relativePlusBtn.disabled = !enabled;
relativeMinusBtn.disabled = !enabled;
zoomSlider.disabled = !enabled;
zoomInput.disabled = !enabled;
}
async function refreshCurrentZoom() {
if (!isDeviceInitialized) return;
try {
const zoomValue = await window.cameraAPI.getZoomValue();
if (zoomValue !== null) {
currentZoomEl.textContent = zoomValue;
zoomSlider.value = zoomValue;
zoomInput.value = zoomValue;
}
} catch (error) {
console.error('Error refreshing zoom:', error);
}
}
// Video preview functions
async function startVideoPreview() {
try {
const quality = videoQualitySelect.value.split('x');
const constraints = {
video: {
width: { ideal: parseInt(quality[0]) },
height: { ideal: parseInt(quality[1]) },
// Try to match Logitech BRIO specifically
deviceId: { exact: undefined }, // Will be set if we can detect the camera
facingMode: 'user'
}
};
// Get all video devices to try to find our Logitech camera
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter(device => device.kind === 'videoinput');
// Look for Logitech device
const logitechDevice = videoDevices.find(device =>
device.label.toLowerCase().includes('logitech') ||
device.label.toLowerCase().includes('brio')
);
if (logitechDevice) {
constraints.video.deviceId = { exact: logitechDevice.deviceId };
showStatus(`Using camera: ${logitechDevice.label}`, 'info');
} else {
showStatus('Using default camera (Logitech camera not found in browser)', 'info');
}
videoStream = await navigator.mediaDevices.getUserMedia(constraints);
videoPreview.srcObject = videoStream;
startVideoBtn.disabled = true;
stopVideoBtn.disabled = false;
showStatus('Video preview started', 'success');
} catch (error) {
console.error('Error starting video preview:', error);
showStatus(`Failed to start video: ${error.message}`, 'error');
}
}
async function stopVideoPreview() {
try {
if (videoStream) {
videoStream.getTracks().forEach(track => track.stop());
videoStream = null;
}
videoPreview.srcObject = null;
startVideoBtn.disabled = false;
stopVideoBtn.disabled = true;
showStatus('Video preview stopped', 'info');
} catch (error) {
console.error('Error stopping video preview:', error);
showStatus(`Failed to stop video: ${error.message}`, 'error');
}
}
// Load library info
async function loadLibraryInfo() {
try {
const info = await window.cameraAPI.getLibraryInfo();
libraryInfoEl.innerHTML = `
<strong>${info.name}</strong> v${info.version}<br>
Platform: ${info.platform} (${info.arch})<br>
Node.js: ${info.nodeVersion}
`;
} catch (error) {
libraryInfoEl.textContent = 'Error loading library info';
}
}
// Event handlers
discoverBtn.addEventListener('click', async () => {
showStatus('Discovering devices...', 'info');
try {
const devices = await window.cameraAPI.discoverDevices();
if (devices.length === 0) {
showStatus('No Logitech BRIO devices found', 'error');
deviceListEl.style.display = 'none';
initFirstBtn.disabled = true;
} else {
showStatus(`Found ${devices.length} device(s)`, 'success');
deviceListEl.innerHTML = devices.map((device, index) => `
<div class="device-item">
<strong>${device.name}</strong><br>
ID: ${device.id}<br>
Vendor: 0x${device.vendorId.toString(16)}, Product: 0x${device.productId.toString(16)}
</div>
`).join('');
deviceListEl.style.display = 'block';
initFirstBtn.disabled = false;
}
} catch (error) {
showStatus(`Error discovering devices: ${error.message}`, 'error');
}
});
initFirstBtn.addEventListener('click', async () => {
showStatus('Initializing first device...', 'info');
try {
const success = await window.cameraAPI.initializeFirstDevice();
if (success) {
showStatus('Device initialized successfully', 'success');
updateDeviceButtons(true);
await refreshCurrentZoom();
} else {
showStatus('Failed to initialize device', 'error');
}
} catch (error) {
showStatus(`Error initializing device: ${error.message}`, 'error');
}
});
releaseBtn.addEventListener('click', async () => {
try {
await window.cameraAPI.releaseDevice();
showStatus('Device released', 'info');
updateDeviceButtons(false);
currentZoomEl.textContent = '--';
capabilitiesEl.style.display = 'none';
zoomCapabilities = null;
} catch (error) {
showStatus(`Error releasing device: ${error.message}`, 'error');
}
});
getCapabilitiesBtn.addEventListener('click', async () => {
try {
const caps = await window.cameraAPI.getZoomCapabilities();
if (caps) {
zoomCapabilities = caps;
// Update slider limits
zoomSlider.min = caps.min;
zoomSlider.max = caps.max;
zoomSlider.step = caps.step;
zoomInput.min = caps.min;
zoomInput.max = caps.max;
zoomInput.step = caps.step;
capabilitiesEl.innerHTML = `
<div class="capabilities-grid">
<div class="capability-item">
<span>Min Zoom:</span>
<strong>${caps.min}</strong>
</div>
<div class="capability-item">
<span>Max Zoom:</span>
<strong>${caps.max}</strong>
</div>
<div class="capability-item">
<span>Step Size:</span>
<strong>${caps.step}</strong>
</div>
<div class="capability-item">
<span>Current:</span>
<strong>${caps.current}</strong>
</div>
<div class="capability-item">
<span>Absolute Support:</span>
<strong>${caps.supportsAbsolute ? '✅' : '❌'}</strong>
</div>
<div class="capability-item">
<span>Relative Support:</span>
<strong>${caps.supportsRelative ? '✅' : '❌'}</strong>
</div>
</div>
`;
capabilitiesEl.style.display = 'block';
await refreshCurrentZoom();
} else {
showStatus('Failed to get zoom capabilities', 'error');
}
} catch (error) {
showStatus(`Error getting capabilities: ${error.message}`, 'error');
}
});
refreshZoomBtn.addEventListener('click', refreshCurrentZoom);
setZoomBtn.addEventListener('click', async () => {
const zoomValue = parseInt(zoomInput.value);
try {
const success = await window.cameraAPI.setZoomAbsolute(zoomValue);
if (success) {
showStatus(`Zoom set to ${zoomValue}`, 'success');
await refreshCurrentZoom();
} else {
showStatus('Failed to set zoom', 'error');
}
} catch (error) {
showStatus(`Error setting zoom: ${error.message}`, 'error');
}
});
zoomSlider.addEventListener('input', async () => {
const zoomValue = parseInt(zoomSlider.value);
zoomInput.value = zoomValue;
// Automatically set zoom when slider moves
if (isDeviceInitialized) {
try {
const success = await window.cameraAPI.setZoomAbsolute(zoomValue);
if (success) {
await refreshCurrentZoom();
}
} catch (error) {
console.error('Error setting zoom from slider:', error);
}
}
});
zoomInput.addEventListener('input', () => {
zoomSlider.value = zoomInput.value;
});
zoomInBtn.addEventListener('click', async () => {
try {
const success = await window.cameraAPI.zoomIn(10);
if (success) {
showStatus('Zoomed in', 'success');
await refreshCurrentZoom();
} else {
showStatus('Failed to zoom in', 'error');
}
} catch (error) {
showStatus(`Error zooming in: ${error.message}`, 'error');
}
});
zoomOutBtn.addEventListener('click', async () => {
try {
const success = await window.cameraAPI.zoomOut(10);
if (success) {
showStatus('Zoomed out', 'success');
await refreshCurrentZoom();
} else {
showStatus('Failed to zoom out', 'error');
}
} catch (error) {
showStatus(`Error zooming out: ${error.message}`, 'error');
}
});
resetZoomBtn.addEventListener('click', async () => {
try {
const success = await window.cameraAPI.resetZoom();
if (success) {
showStatus('Zoom reset to minimum', 'success');
await refreshCurrentZoom();
} else {
showStatus('Failed to reset zoom', 'error');
}
} catch (error) {
showStatus(`Error resetting zoom: ${error.message}`, 'error');
}
});
maxZoomBtn.addEventListener('click', async () => {
try {
const success = await window.cameraAPI.maxZoom();
if (success) {
showStatus('Zoom set to maximum', 'success');
await refreshCurrentZoom();
} else {
showStatus('Failed to set max zoom', 'error');
}
} catch (error) {
showStatus(`Error setting max zoom: ${error.message}`, 'error');
}
});
relativePlusBtn.addEventListener('click', async () => {
const amount = parseInt(relativeInput.value) || 10;
try {
const success = await window.cameraAPI.setZoomRelative(amount);
if (success) {
showStatus(`Zoomed in by ${amount}`, 'success');
await refreshCurrentZoom();
} else {
showStatus('Failed to zoom relatively', 'error');
}
} catch (error) {
showStatus(`Error zooming relatively: ${error.message}`, 'error');
}
});
relativeMinusBtn.addEventListener('click', async () => {
const amount = parseInt(relativeInput.value) || 10;
try {
const success = await window.cameraAPI.setZoomRelative(-amount);
if (success) {
showStatus(`Zoomed out by ${amount}`, 'success');
await refreshCurrentZoom();
} else {
showStatus('Failed to zoom relatively', 'error');
}
} catch (error) {
showStatus(`Error zooming relatively: ${error.message}`, 'error');
}
});
// Video preview event listeners
startVideoBtn.addEventListener('click', startVideoPreview);
stopVideoBtn.addEventListener('click', stopVideoPreview);
videoQualitySelect.addEventListener('change', async () => {
if (videoStream) {
// Restart video with new quality if already running
await stopVideoPreview();
await startVideoPreview();
}
});
// Cleanup video on page unload
window.addEventListener('beforeunload', () => {
if (videoStream) {
videoStream.getTracks().forEach(track => track.stop());
}
});
// Load library info on startup
loadLibraryInfo();
</script>
</body>
</html>