fix: window capture list not showing in video sources
Root cause: PowerShell command was wrapped in exec() with double-quote
escaping that silently failed. Rewrote using execFile() with args array
which requires no manual escaping.
Changes:
- electron/devices.js: use execFile('powershell.exe', [...args]) for
window enumeration — eliminates quoting issues entirely
- Window list now returned INSIDE getDevices() video array (type='window')
so they arrive together with cameras/screens in one IPC call
- StreamSettings: windows grouped from devices.video directly, with 🪟
emoji prefix for visual distinction
- Added inline "↻ Обновить" button in video source field header to
refresh devices+windows on demand without switching tabs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -204,6 +204,7 @@ export default function App() {
|
||||
onStart={() => startStream(stream.id)}
|
||||
onStop={() => stopStream(stream.id)}
|
||||
onRemove={() => removeStream(stream.id)}
|
||||
onRefreshDevices={refreshDevices}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ const STATUS_CLASS = {
|
||||
error: 'status--error'
|
||||
}
|
||||
|
||||
export default function StreamCard({ stream, devices, windows, devicesLoaded, onChange, onStart, onStop, onRemove }) {
|
||||
export default function StreamCard({ stream, devices, windows, devicesLoaded, onChange, onStart, onStop, onRemove, onRefreshDevices }) {
|
||||
const [expanded, setExpanded] = useState(true)
|
||||
const [editingName, setEditingName] = useState(false)
|
||||
const [nameValue, setNameValue] = useState(stream.name)
|
||||
@@ -120,6 +120,7 @@ export default function StreamCard({ stream, devices, windows, devicesLoaded, on
|
||||
devicesLoaded={devicesLoaded}
|
||||
disabled={isActive}
|
||||
onChange={onChange}
|
||||
onRefreshDevices={onRefreshDevices}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -95,7 +95,7 @@ function detectProfile(res, fps, vbr) {
|
||||
}
|
||||
|
||||
// ─── main component ───────────────────────────────────────────────────────────
|
||||
export default function StreamSettings({ stream, devices, windows, devicesLoaded, disabled, onChange }) {
|
||||
export default function StreamSettings({ stream, devices, windows, devicesLoaded, disabled, onChange, onRefreshDevices }) {
|
||||
const [activeSection, setActiveSection] = useState('source')
|
||||
|
||||
const updateVideo = changes => onChange({ videoSource: { ...stream.videoSource, ...changes } })
|
||||
@@ -104,13 +104,16 @@ export default function StreamSettings({ stream, devices, windows, devicesLoaded
|
||||
const videoDevices = devices.video || []
|
||||
const audioDevices = devices.audio || []
|
||||
|
||||
// Build grouped video options
|
||||
// Build grouped video options — windows now come from devices.video directly
|
||||
const noneOpts = videoDevices.filter(d => d.type === 'none')
|
||||
const screenOpts = videoDevices.filter(d => d.type === 'desktop')
|
||||
const cameraOpts = videoDevices.filter(d => d.type === 'device')
|
||||
const windowOpts = (windows || []).map(w => ({
|
||||
name: w.title, deviceName: w.title, type: 'window', windowTitle: w.title
|
||||
}))
|
||||
// Windows from devices.video (already enumerated server-side) + any extra from prop
|
||||
const windowsFromDevices = videoDevices.filter(d => d.type === 'window')
|
||||
const windowsFromProp = (windows || [])
|
||||
.filter(w => !windowsFromDevices.find(d => d.deviceName === w.title))
|
||||
.map(w => ({ name: w.title, deviceName: w.title, type: 'window', windowTitle: w.title }))
|
||||
const windowOpts = [...windowsFromDevices, ...windowsFromProp]
|
||||
|
||||
const videoOptions = [
|
||||
...noneOpts,
|
||||
@@ -178,7 +181,17 @@ export default function StreamSettings({ stream, devices, windows, devicesLoaded
|
||||
<div className="settings-group">
|
||||
<div className="group-title">Видеоисточник</div>
|
||||
<div className="field">
|
||||
<label className="field-label">Устройство / Источник</label>
|
||||
<div className="field-label-row">
|
||||
<label className="field-label">Устройство / Источник</label>
|
||||
{onRefreshDevices && (
|
||||
<button
|
||||
className="btn-inline-refresh"
|
||||
onClick={onRefreshDevices}
|
||||
disabled={disabled || !devicesLoaded}
|
||||
title="Обновить список устройств и окон"
|
||||
>↻ Обновить</button>
|
||||
)}
|
||||
</div>
|
||||
{devicesLoaded ? (
|
||||
<select
|
||||
className="field-select"
|
||||
@@ -193,12 +206,12 @@ export default function StreamSettings({ stream, devices, windows, devicesLoaded
|
||||
{dev.name}
|
||||
</option>
|
||||
: <option key={dev.deviceName || i} value={dev.deviceName || dev.name}>
|
||||
{dev.name}
|
||||
{dev.type === 'window' ? `🪟 ${dev.name}` : dev.name}
|
||||
</option>
|
||||
)}
|
||||
</select>
|
||||
) : (
|
||||
<div className="field-loading">Загрузка устройств…</div>
|
||||
<div className="field-loading">Загрузка устройств и окон…</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -485,6 +485,32 @@ html, body, #root {
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.field-label-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.field-label-row .field-label { margin-bottom: 0; }
|
||||
|
||||
.btn-inline-refresh {
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn-inline-refresh:hover:not(:disabled) {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
background: var(--accent-dim);
|
||||
}
|
||||
.btn-inline-refresh:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
|
||||
.fields-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
Reference in New Issue
Block a user