UNPKG

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
<!DOCTYPE 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>