up
This commit is contained in:
+83
-5
@@ -19,6 +19,7 @@ import { Readable } from 'node:stream'
|
||||
import { DiagnosticLog } from './diagnostics.js'
|
||||
import { Controller, parseCommand } from './controller.js'
|
||||
import { WindowSettings, fitControlBounds, preferredDisplayId } from './window-settings.js'
|
||||
import { AudioOutput } from './audio-output.js'
|
||||
import { parseRange } from './range.js'
|
||||
import { Online } from './online.js'
|
||||
import type { OnlineState } from '../src/types/online.js'
|
||||
@@ -40,6 +41,8 @@ protocol.registerSchemesAsPrivileged([
|
||||
])
|
||||
let control: BrowserWindow | null = null
|
||||
let output: BrowserWindow | null = null
|
||||
let audioDevicesWindow: BrowserWindow | null = null
|
||||
let audioOutput: AudioOutput
|
||||
let closingWindow: BrowserWindow | null = null
|
||||
let openingOutput = false
|
||||
let outputEpoch = 0
|
||||
@@ -70,6 +73,9 @@ const rendererRoot = () => join(app.getAppPath(), 'dist')
|
||||
function host(): HostInfo {
|
||||
const primary = screen.getPrimaryDisplay().id
|
||||
return {
|
||||
audioOutputs: audioOutput?.devices ?? [],
|
||||
audioOutputId: audioOutput?.selected ?? '',
|
||||
audioOutputNotice: audioOutput?.notice ?? '',
|
||||
notice: hostNotice,
|
||||
sleepBlocked: sleepBlocker !== null && powerSaveBlocker.isStarted(sleepBlocker),
|
||||
preferredDisplayId: preferredDisplayId(
|
||||
@@ -90,7 +96,7 @@ function windowOptions(isOutput = false) {
|
||||
return {
|
||||
backgroundColor: '#111111',
|
||||
webPreferences: {
|
||||
...(isOutput ? { partition: 'screen-output' } : {}),
|
||||
...(isOutput ? { partition: 'persist:screen-output' } : {}),
|
||||
preload: join(__dirname, 'preload.js'),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
@@ -98,6 +104,15 @@ function windowOptions(isOutput = false) {
|
||||
},
|
||||
}
|
||||
}
|
||||
async function setAudioSink(window: BrowserWindow, id: string) {
|
||||
await window.webContents.executeJavaScript(`(async () => {
|
||||
const results = await Promise.allSettled(
|
||||
Array.from(document.querySelectorAll('audio,video')).map(media => media.setSinkId(${JSON.stringify(id)}))
|
||||
);
|
||||
const failed = results.find(result => result.status === 'rejected');
|
||||
if (failed) throw failed.reason;
|
||||
})()`)
|
||||
}
|
||||
function secure(window: BrowserWindow) {
|
||||
window.webContents.setWindowOpenHandler(() => ({ action: 'deny' }))
|
||||
window.webContents.on('will-navigate', (event) => event.preventDefault())
|
||||
@@ -462,12 +477,60 @@ app
|
||||
return new Response(null, { status: 404 })
|
||||
}
|
||||
}
|
||||
// Разные сессии изолируют renderer показа от пульта, протокол и запреты одинаковы.
|
||||
for (const target of [session.defaultSession, session.fromPartition('screen-output')]) {
|
||||
target.setPermissionRequestHandler((_contents, _permission, callback) => callback(false))
|
||||
target.setPermissionCheckHandler(() => false)
|
||||
// Выбор выхода разрешён только renderer показа; камера и микрофон остаются запрещены.
|
||||
for (const target of [session.defaultSession, session.fromPartition('persist:screen-output')]) {
|
||||
const allowAudioOutput = (contents: Electron.WebContents | null, permission: string) =>
|
||||
permission === 'speaker-selection' &&
|
||||
!!contents &&
|
||||
[audioDevicesWindow?.webContents, output?.webContents].includes(contents) &&
|
||||
contents.getURL().startsWith('screen-app://app/index.html?')
|
||||
target.setPermissionRequestHandler((contents, permission, callback, details) =>
|
||||
callback(details.isMainFrame && allowAudioOutput(contents, permission)),
|
||||
)
|
||||
target.setPermissionCheckHandler((contents, permission, origin, details) =>
|
||||
details.isMainFrame === true &&
|
||||
(origin === 'screen-app://app' || origin === 'screen-app://app/') &&
|
||||
allowAudioOutput(contents, permission),
|
||||
)
|
||||
target.protocol.handle('screen-app', handleAppRequest)
|
||||
}
|
||||
audioDevicesWindow = new BrowserWindow({ ...windowOptions(true), show: false })
|
||||
secure(audioDevicesWindow)
|
||||
await audioDevicesWindow.loadURL('screen-app://app/index.html?audio-devices=1')
|
||||
await audioDevicesWindow.webContents.executeJavaScript(
|
||||
"document.body.appendChild(document.createElement('audio'))",
|
||||
)
|
||||
audioOutput = new AudioOutput(
|
||||
settings.value.audioOutput ?? '',
|
||||
() => audioDevicesWindow!.webContents.executeJavaScript(`navigator.mediaDevices.enumerateDevices().then(
|
||||
devices => devices.filter(d => d.kind === 'audiooutput' && d.deviceId && d.deviceId !== 'default' && d.deviceId !== 'communications')
|
||||
.map((d, index) => ({ id: d.deviceId, label: d.label || 'Устройство звука ' + (index + 1) }))
|
||||
)`),
|
||||
async (id) => {
|
||||
await setAudioSink(audioDevicesWindow!, id)
|
||||
const current = output
|
||||
if (current && !current.isDestroyed() && !current.webContents.isLoadingMainFrame())
|
||||
await setAudioSink(current, id)
|
||||
},
|
||||
(id) => {
|
||||
const previous = settings.value.audioOutput
|
||||
settings.value.audioOutput = id
|
||||
try {
|
||||
settings.save()
|
||||
} catch (error) {
|
||||
settings.value.audioOutput = previous
|
||||
throw error
|
||||
}
|
||||
},
|
||||
)
|
||||
const refreshAudioOutputs = () => audioOutput.refresh().catch(() => {
|
||||
audioOutput.notice = 'Не удалось обновить список устройств звука.'
|
||||
})
|
||||
ipcMain.on('audio-devices-changed', (event) => {
|
||||
if (event.sender === audioDevicesWindow?.webContents && event.senderFrame === event.sender.mainFrame)
|
||||
void refreshAudioOutputs()
|
||||
})
|
||||
await refreshAudioOutputs()
|
||||
control = new BrowserWindow({
|
||||
...windowOptions(),
|
||||
...fitControlBounds(settings.value.control?.bounds, screen.getPrimaryDisplay().workArea),
|
||||
@@ -490,6 +553,21 @@ app
|
||||
control.on('unmaximize', scheduleSave)
|
||||
control.on('close', saveWindowSettings)
|
||||
secure(control)
|
||||
ipcMain.handle('select-audio-output', (event, id: unknown) => {
|
||||
requireControl(event)
|
||||
return audioOutput.select(id)
|
||||
})
|
||||
ipcMain.handle('prepare-audio-output', async (event) => {
|
||||
const current = output
|
||||
if (!current || event.sender !== current.webContents || event.senderFrame !== current.webContents.mainFrame)
|
||||
throw new Error('Недоступно')
|
||||
try {
|
||||
await audioOutput.prepare((id) => setAudioSink(current, id))
|
||||
} catch (error) {
|
||||
if (output === current) closeOutput('Не удалось выбрать устройство звука')
|
||||
throw error
|
||||
}
|
||||
})
|
||||
ipcMain.handle('state', (event) => {
|
||||
if (
|
||||
![control?.webContents, output?.webContents].includes(event.sender) ||
|
||||
|
||||
Reference in New Issue
Block a user