Add ADB-based Android screen capture engine with display picker integration
New scrcpy/ADB capture engine that captures Android device screens over ADB using screencap polling. Supports USB and WiFi ADB connections with device auto-discovery. Engine-aware display picker shows Android devices when scrcpy engine is selected, with inline ADB connect form for WiFi devices. Key changes: - New scrcpy_engine.py using adb screencap polling (~1-2 FPS over WiFi) - Engine-aware GET /config/displays?engine_type= API - ADB connect/disconnect API endpoints (POST /adb/connect, /adb/disconnect) - Display picker supports engine-specific device lists - Stream/test modals pass engine type to display picker - Test template handler changed to sync def to prevent event loop blocking - Restart script merges registry PATH for newly-installed tools - All engines (including unavailable) shown in engine list with status flag Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,7 @@ async function loadCaptureTemplates() {
|
||||
}
|
||||
|
||||
function getEngineIcon(engineType) {
|
||||
if (engineType === 'scrcpy') return '📱';
|
||||
return '🚀';
|
||||
}
|
||||
|
||||
@@ -270,8 +271,15 @@ function collectEngineConfig() {
|
||||
|
||||
async function loadDisplaysForTest() {
|
||||
try {
|
||||
if (!_cachedDisplays) {
|
||||
const response = await fetchWithAuth('/config/displays');
|
||||
// Use engine-specific display list when testing a scrcpy template
|
||||
const engineType = window.currentTestingTemplate?.engine_type;
|
||||
const url = engineType === 'scrcpy'
|
||||
? `/config/displays?engine_type=scrcpy`
|
||||
: '/config/displays';
|
||||
|
||||
// Always refetch for scrcpy (devices may change); use cache for desktop
|
||||
if (!_cachedDisplays || engineType === 'scrcpy') {
|
||||
const response = await fetchWithAuth(url);
|
||||
if (!response.ok) throw new Error(`Failed to load displays: ${response.status}`);
|
||||
const displaysData = await response.json();
|
||||
set_cachedDisplays(displaysData.displays || []);
|
||||
@@ -800,6 +808,9 @@ export async function editStream(streamId) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Track which engine type the stream-modal displays were loaded for. */
|
||||
let _streamModalDisplaysEngine = null;
|
||||
|
||||
async function populateStreamModalDropdowns() {
|
||||
const [displaysRes, captureTemplatesRes, streamsRes, ppTemplatesRes] = await Promise.all([
|
||||
fetch(`${API_BASE}/config/displays`, { headers: getHeaders() }),
|
||||
@@ -812,6 +823,7 @@ async function populateStreamModalDropdowns() {
|
||||
const displaysData = await displaysRes.json();
|
||||
set_cachedDisplays(displaysData.displays || []);
|
||||
}
|
||||
_streamModalDisplaysEngine = null; // desktop displays loaded
|
||||
|
||||
if (!document.getElementById('stream-display-index').value && _cachedDisplays && _cachedDisplays.length > 0) {
|
||||
const primary = _cachedDisplays.find(d => d.is_primary) || _cachedDisplays[0];
|
||||
@@ -826,11 +838,15 @@ async function populateStreamModalDropdowns() {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = tmpl.id;
|
||||
opt.dataset.name = tmpl.name;
|
||||
opt.dataset.engineType = tmpl.engine_type;
|
||||
opt.textContent = `${getEngineIcon(tmpl.engine_type)} ${tmpl.name}`;
|
||||
templateSelect.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
// When template changes, refresh displays if engine type switched
|
||||
templateSelect.addEventListener('change', _onCaptureTemplateChanged);
|
||||
|
||||
const sourceSelect = document.getElementById('stream-source');
|
||||
sourceSelect.innerHTML = '';
|
||||
if (streamsRes.ok) {
|
||||
@@ -863,6 +879,47 @@ async function populateStreamModalDropdowns() {
|
||||
}
|
||||
|
||||
_autoGenerateStreamName();
|
||||
|
||||
// If the first template is an scrcpy engine, reload displays immediately
|
||||
const firstOpt = templateSelect.selectedOptions[0];
|
||||
if (firstOpt?.dataset?.engineType === 'scrcpy') {
|
||||
await _refreshStreamDisplaysForEngine('scrcpy');
|
||||
}
|
||||
}
|
||||
|
||||
async function _onCaptureTemplateChanged() {
|
||||
const templateSelect = document.getElementById('stream-capture-template');
|
||||
const engineType = templateSelect.selectedOptions[0]?.dataset?.engineType || null;
|
||||
const needsEngineDisplays = engineType === 'scrcpy';
|
||||
const currentEngine = needsEngineDisplays ? engineType : null;
|
||||
|
||||
// Only refetch if the engine category actually changed
|
||||
if (currentEngine !== _streamModalDisplaysEngine) {
|
||||
await _refreshStreamDisplaysForEngine(currentEngine);
|
||||
}
|
||||
_autoGenerateStreamName();
|
||||
}
|
||||
|
||||
async function _refreshStreamDisplaysForEngine(engineType) {
|
||||
_streamModalDisplaysEngine = engineType;
|
||||
const url = engineType ? `/config/displays?engine_type=${engineType}` : '/config/displays';
|
||||
try {
|
||||
const resp = await fetchWithAuth(url);
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
set_cachedDisplays(data.displays || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refreshing displays for engine:', error);
|
||||
}
|
||||
|
||||
// Reset display selection and pick the first available
|
||||
document.getElementById('stream-display-index').value = '';
|
||||
document.getElementById('stream-display-picker-label').textContent = t('displays.picker.select');
|
||||
if (_cachedDisplays && _cachedDisplays.length > 0) {
|
||||
const primary = _cachedDisplays.find(d => d.is_primary) || _cachedDisplays[0];
|
||||
onStreamDisplaySelected(primary.index, primary);
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveStream() {
|
||||
|
||||
Reference in New Issue
Block a user