Add camera/webcam capture engine with engine-aware display picker
- New CameraEngine using OpenCV VideoCapture for webcam capture - HAS_OWN_DISPLAYS class attribute on CaptureEngine base to distinguish engines with their own device lists from desktop monitor engines - Display picker renders device list for cameras/scrcpy, spatial layout for desktop monitors - Engine-aware display label formatting (camera name vs monitor index) - Stream modal properly loads engine-specific displays on template change, edit, and clone - Camera backend config rendered as dropdown (auto/dshow/msmf/v4l2) - Remove offline label from device cards (healthcheck indicator suffices) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
import {
|
||||
_cachedDisplays, _displayPickerCallback, _displayPickerSelectedIndex,
|
||||
set_displayPickerCallback, set_displayPickerSelectedIndex, displaysCache,
|
||||
availableEngines,
|
||||
} from '../core/state.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { fetchWithAuth } from '../core/api.js';
|
||||
@@ -14,6 +15,10 @@ import { showToast } from '../core/ui.js';
|
||||
/** Currently active engine type for the picker (null = desktop monitors). */
|
||||
let _pickerEngineType = null;
|
||||
|
||||
/** Check if an engine type has its own device list (for inline onclick use). */
|
||||
window._engineHasOwnDisplays = (engineType) =>
|
||||
!!(engineType && availableEngines.find(e => e.type === engineType)?.has_own_displays);
|
||||
|
||||
export function openDisplayPicker(callback, selectedIndex, engineType = null) {
|
||||
set_displayPickerCallback(callback);
|
||||
set_displayPickerSelectedIndex((selectedIndex !== undefined && selectedIndex !== null && selectedIndex !== '') ? Number(selectedIndex) : null);
|
||||
@@ -57,8 +62,10 @@ async function _fetchAndRenderEngineDisplays(engineType) {
|
||||
|
||||
if (displays.length > 0) {
|
||||
renderDisplayPickerLayout(displays, engineType);
|
||||
} else {
|
||||
} else if (engineType === 'scrcpy') {
|
||||
_renderEmptyAndroidPicker(canvas);
|
||||
} else {
|
||||
canvas.innerHTML = `<div class="loading">${t('displays.none')}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching engine displays:', error);
|
||||
@@ -143,6 +150,34 @@ export function renderDisplayPickerLayout(displays, engineType = null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Engines with own displays (camera, scrcpy) → device list layout
|
||||
if (engineType) {
|
||||
const items = displays.map(display => {
|
||||
const isSelected = _displayPickerSelectedIndex !== null && display.index === _displayPickerSelectedIndex;
|
||||
return `
|
||||
<div class="device-picker-item layout-display-pickable${isSelected ? ' selected' : ''}"
|
||||
onclick="selectDisplay(${display.index})"
|
||||
title="${t('displays.picker.click_to_select')}">
|
||||
<div class="layout-index-label">#${display.index}</div>
|
||||
<div class="layout-display-label">
|
||||
<strong>${display.name}</strong>
|
||||
<small>${display.width}×${display.height} · ${display.refresh_rate} FPS</small>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
let html = `<div class="device-picker-list">${items}</div>`;
|
||||
|
||||
if (engineType === 'scrcpy') {
|
||||
html += _buildAdbConnectHtml();
|
||||
}
|
||||
|
||||
canvas.innerHTML = html;
|
||||
return;
|
||||
}
|
||||
|
||||
// Desktop monitors → positioned rectangle layout
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
displays.forEach(display => {
|
||||
minX = Math.min(minX, display.x);
|
||||
@@ -178,22 +213,18 @@ export function renderDisplayPickerLayout(displays, engineType = null) {
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
let html = `
|
||||
canvas.innerHTML = `
|
||||
<div class="layout-container" style="width: 100%; padding-bottom: ${aspect * 100}%; position: relative;">
|
||||
${displayElements}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Show ADB connect form below devices for scrcpy engine
|
||||
if (engineType === 'scrcpy') {
|
||||
html += _buildAdbConnectHtml();
|
||||
}
|
||||
|
||||
canvas.innerHTML = html;
|
||||
}
|
||||
|
||||
export function formatDisplayLabel(displayIndex, display) {
|
||||
export function formatDisplayLabel(displayIndex, display, engineType = null) {
|
||||
if (display) {
|
||||
if (engineType && window._engineHasOwnDisplays(engineType)) {
|
||||
return `${display.name} (${display.width}×${display.height})`;
|
||||
}
|
||||
return `#${display.index}: ${display.width}×${display.height}${display.is_primary ? ' (' + t('displays.badge.primary') + ')' : ''}`;
|
||||
}
|
||||
return `Display ${displayIndex}`;
|
||||
|
||||
Reference in New Issue
Block a user