V1
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
/* 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);
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user