176 lines
7.1 KiB
JavaScript
176 lines
7.1 KiB
JavaScript
/* Réglages matériels de webcam : curseurs générés selon ce que V4L2 expose. */
|
||
(() => {
|
||
'use strict';
|
||
const pane = document.getElementById('audioSettingsPanel');
|
||
const tabAudio = document.getElementById('audioTab');
|
||
const tabCamera = document.getElementById('cameraTab');
|
||
const sectionAudio = document.getElementById('audioSettingsSection');
|
||
const sectionCamera = document.getElementById('cameraSettingsSection');
|
||
const items = document.getElementById('cameraSettingsItems');
|
||
const message = document.getElementById('cameraSettingsMessage');
|
||
const reload = document.getElementById('cameraSettingsReload');
|
||
const reset = document.getElementById('cameraSettingsReset');
|
||
if (!pane || !tabAudio || !tabCamera || !items) return;
|
||
|
||
const labels = {
|
||
brightness: 'Luminosité', contrast: 'Contraste', saturation: 'Saturation',
|
||
hue: 'Teinte', gamma: 'Gamma', gain: 'Gain vidéo', sharpness: 'Netteté',
|
||
backlight_compensation: 'Compensation contre-jour',
|
||
power_line_frequency: 'Fréquence anti-scintillement',
|
||
white_balance_automatic: 'Balance des blancs automatique',
|
||
white_balance_temperature: 'Température des blancs (K)',
|
||
auto_exposure: 'Exposition', exposure_time_absolute: 'Temps d’exposition',
|
||
exposure_dynamic_framerate: 'Fréquence d’image dynamique'
|
||
};
|
||
const groups = [
|
||
{name: 'Image', keys: ['brightness','contrast','saturation','sharpness','hue','gamma','gain','backlight_compensation']},
|
||
{name: 'Éclairage et couleurs', keys: ['white_balance_automatic','white_balance_temperature','power_line_frequency']},
|
||
{name: 'Exposition', keys: ['auto_exposure','exposure_time_absolute','exposure_dynamic_framerate']}
|
||
];
|
||
const exposureChoices = {1: 'Manuelle', 3: 'Automatique'};
|
||
const powerChoices = {0: 'Désactivé', 1: '50 Hz', 2: '60 Hz'};
|
||
let working = false;
|
||
|
||
function showTab(which) {
|
||
const camera = which === 'camera';
|
||
tabCamera.setAttribute('aria-selected', String(camera));
|
||
tabAudio.setAttribute('aria-selected', String(!camera));
|
||
sectionCamera.hidden = !camera;
|
||
sectionAudio.hidden = camera;
|
||
if (camera) load();
|
||
}
|
||
tabAudio.addEventListener('click', () => showTab('audio'));
|
||
tabCamera.addEventListener('click', () => showTab('camera'));
|
||
document.getElementById('audioSettingsButton')?.addEventListener('click', () => {
|
||
if (!pane.hidden) showTab('audio');
|
||
});
|
||
|
||
async function api(path, payload) {
|
||
const opts = {cache: 'no-store'};
|
||
if (payload !== undefined) {
|
||
opts.method = 'POST';
|
||
opts.headers = {'Content-Type':'application/json', 'X-Alfred-Request':'1'};
|
||
opts.body = JSON.stringify(payload);
|
||
}
|
||
const res = await fetch(path, opts);
|
||
const data = await res.json();
|
||
if (!res.ok) throw new Error(data.error || `Erreur HTTP ${res.status}`);
|
||
return data;
|
||
}
|
||
|
||
function elt(tag, klass, text) {
|
||
const e = document.createElement(tag);
|
||
if (klass) e.className = klass;
|
||
if (text !== undefined) e.textContent = text;
|
||
return e;
|
||
}
|
||
|
||
function controlRow(name, meta) {
|
||
const row = elt('div', 'camera-control');
|
||
if (meta.inactive) row.classList.add('is-inactive');
|
||
const header = elt('div','setting-row');
|
||
const label = elt('label', null, labels[name] || name);
|
||
const output = elt('output','camera-control-value',String(meta.value ?? '—'));
|
||
header.append(label, output); row.appendChild(header);
|
||
let input;
|
||
if (meta.kind === 'bool' || meta.kind === 'menu') {
|
||
input = elt('select','camera-select');
|
||
let opts;
|
||
if (meta.kind === 'bool') opts = [{value:1,label:'Activé'},{value:0,label:'Désactivé'}];
|
||
else if (name === 'auto_exposure') opts = [{value:3,label:'Automatique'},{value:1,label:'Manuelle'}];
|
||
else if (name === 'power_line_frequency') opts = Object.entries(powerChoices).map(([value,label])=>({value:Number(value),label}));
|
||
else opts = meta.menu || [];
|
||
const allowed = meta.menu?.length && meta.kind === 'menu' ? new Set(meta.menu.map(i=>i.value)) : null;
|
||
for (const opt of opts) {
|
||
if (allowed && !allowed.has(opt.value)) continue;
|
||
const option = elt('option',null,opt.label);
|
||
option.value = String(opt.value);
|
||
input.appendChild(option);
|
||
}
|
||
input.value = String(meta.value);
|
||
const updateLabel = () => {
|
||
output.textContent = input.options[input.selectedIndex]?.text || String(input.value);
|
||
};
|
||
updateLabel();
|
||
input.addEventListener('change', () => save(name, Number(input.value)));
|
||
} else {
|
||
input = elt('input','camera-range');
|
||
input.type = 'range';
|
||
input.min = String(meta.min); input.max = String(meta.max);
|
||
input.step = String(meta.step || 1);
|
||
input.value = String(meta.value);
|
||
input.addEventListener('input', () => { output.textContent = input.value; });
|
||
// Appliquer au relâchement : évite des centaines de commandes shell.
|
||
input.addEventListener('change', () => save(name, Number(input.value)));
|
||
}
|
||
input.id = `v4l-${name}`;
|
||
label.htmlFor = input.id;
|
||
input.disabled = Boolean(meta.inactive || working);
|
||
row.appendChild(input);
|
||
if (meta.inactive) row.appendChild(elt('div','camera-disabled-hint', 'Désactive le mode automatique pour régler cette valeur.'));
|
||
return row;
|
||
}
|
||
|
||
function render(data) {
|
||
items.replaceChildren();
|
||
const available = data.controls || {};
|
||
for (const group of groups) {
|
||
const found = group.keys.filter(name => available[name]);
|
||
if (!found.length) continue;
|
||
items.appendChild(elt('h3','camera-group-title',group.name));
|
||
for (const name of found) items.appendChild(controlRow(name,available[name]));
|
||
}
|
||
if (!items.children.length) items.appendChild(elt('p','camera-settings-note','Aucun réglage V4L2 disponible.'));
|
||
}
|
||
|
||
async function load() {
|
||
if (working) return;
|
||
message.textContent = 'Lecture des réglages caméra…';
|
||
try {
|
||
render(await api('/api/camera/settings'));
|
||
message.textContent = '';
|
||
} catch (err) {
|
||
message.textContent = err.message;
|
||
items.replaceChildren();
|
||
}
|
||
}
|
||
|
||
async function save(control, value) {
|
||
if (working) return;
|
||
working = true;
|
||
message.textContent = 'Application…';
|
||
try {
|
||
const result = await api('/api/camera/settings', {control, value});
|
||
working = false;
|
||
render(result);
|
||
message.textContent = 'Réglage appliqué et mémorisé.';
|
||
} catch (err) {
|
||
message.textContent = err.message;
|
||
working = false;
|
||
await load();
|
||
return;
|
||
}
|
||
working = false;
|
||
}
|
||
|
||
reload.addEventListener('click', load);
|
||
reset.addEventListener('click', async () => {
|
||
if (working || !window.confirm('Rétablir tous les réglages caméra d’origine ?')) return;
|
||
working = true;
|
||
message.textContent = 'Restauration en cours…';
|
||
try {
|
||
const result = await api('/api/camera/reset', {});
|
||
working = false;
|
||
render(result);
|
||
message.textContent = 'Valeurs d’usine restaurées.';
|
||
} catch (err) {
|
||
message.textContent = err.message;
|
||
} finally {
|
||
working = false;
|
||
}
|
||
});
|
||
window.alfredSocket?.on('camera_settings', data => {
|
||
if (!sectionCamera.hidden && !working) render(data);
|
||
});
|
||
})();
|