Feat : OBS presets
Some checks failed
release / build (push) Successful in 26s
release / verify-windows (push) Failing after 1m0s

This commit is contained in:
jeanotx32
2026-08-11 22:24:45 +02:00
parent 3b614c5d05
commit 03b1963150
11 changed files with 710 additions and 8 deletions

View File

@@ -49,6 +49,7 @@ export const AGENT_ACTIONS = [
'browser.close',
'capture.start',
'capture.stop',
'preset.apply',
'agent.update',
'agent.ping',
] as const;
@@ -65,6 +66,244 @@ export interface AgentActionParams {
'profile.set': { profile: string };
'collection.set': { collection: string };
'recordDirectory.set': { directory: string };
/** Sans paramètre, l'agent applique le preset configuré côté serveur. */
'preset.apply': { presetId?: string; encoder?: RecordingEncoder };
}
// ---------------------------------------------------------------------------
// Presets d'enregistrement
// ---------------------------------------------------------------------------
/**
* Un preset décrit un compromis qualité / charge CPU. Il se traduit en écritures
* dans le profil OBS courant (`SetProfileParameter`, section `SimpleOutput`) et
* en un réglage vidéo (`SetVideoSettings`).
*
* Deux limites d'OBS qu'aucun contournement propre ne lève :
* - les paramètres de profil sont relus à chaque démarrage d'enregistrement,
* donc un changement de qualité prend effet au prochain enregistrement ;
* - l'objet encodeur, lui, n'est instancié qu'au lancement d'OBS : changer
* `RecEncoder` exige un redémarrage d'OBS pour être réellement pris en compte.
*/
export interface RecordingEncoderInfo {
/** Valeur écrite dans `SimpleOutput/RecEncoder`. */
obsValue: string;
label: string;
/** Clé de profil pilotant le compromis vitesse/qualité ; null si l'encodeur n'en expose pas. */
speedKey: string | null;
/** Jeu de valeurs accepté par cette clé. */
speedFamily: SpeedFamily | null;
hint: string;
}
export type SpeedFamily = 'x264' | 'nvenc' | 'qsv' | 'amd';
/**
* Pas d'auto-détection : obs-websocket ne publie pas la liste des encodeurs
* disponibles, et le matériel diffère d'une VM à l'autre. C'est l'opérateur qui
* choisit, VM par VM.
*/
export const RECORDING_ENCODERS = {
x264: {
obsValue: 'x264',
label: 'x264 — logiciel (CPU)',
speedKey: 'Preset',
speedFamily: 'x264',
hint: 'Toujours disponible. Le seul choix sur un VPS sans GPU.',
},
x264_lowcpu: {
obsValue: 'x264_lowcpu',
label: 'x264 économe — logiciel (CPU)',
speedKey: null,
speedFamily: null,
hint: "Force le réglage le plus rapide de x264 : CPU au plancher, fichiers plus gros. Ignore la vitesse du preset.",
},
nvenc: {
obsValue: 'nvenc',
label: 'NVENC H.264 — NVIDIA',
speedKey: 'NVENCPreset2',
speedFamily: 'nvenc',
hint: 'Encodage déporté sur le GPU : CPU quasi nul. Exige une carte NVIDIA.',
},
nvenc_hevc: {
obsValue: 'nvenc_hevc',
label: 'NVENC HEVC — NVIDIA',
speedKey: 'NVENCPreset2',
speedFamily: 'nvenc',
hint: 'Environ 30 % de poids en moins que H.264, moins universellement lisible.',
},
qsv: {
obsValue: 'qsv',
label: 'Quick Sync H.264 — Intel',
speedKey: 'QSVPreset',
speedFamily: 'qsv',
hint: 'iGPU Intel. Disponible sur beaucoup de VM bureautiques.',
},
amd: {
obsValue: 'amd',
label: 'AMF H.264 — AMD',
speedKey: 'AMDPreset',
speedFamily: 'amd',
hint: 'GPU AMD.',
},
apple_h264: {
obsValue: 'apple_h264',
label: 'VideoToolbox H.264 — macOS',
speedKey: null,
speedFamily: null,
hint: 'macOS uniquement.',
},
} as const satisfies Record<string, RecordingEncoderInfo>;
export type RecordingEncoder = keyof typeof RECORDING_ENCODERS;
export const DEFAULT_RECORDING_ENCODER: RecordingEncoder = 'x264';
export function isRecordingEncoder(value: unknown): value is RecordingEncoder {
return typeof value === 'string' && Object.hasOwn(RECORDING_ENCODERS, value);
}
export interface RecordingPreset {
id: string;
label: string;
summary: string;
/** Charge CPU relative, 1 = la plus légère. Sert à ordonner l'interface. */
cpuCost: 1 | 2 | 3 | 4;
/** Hauteur de sortie ; `null` conserve la résolution de la scène. Jamais d'agrandissement. */
height: number | null;
/**
* Images par seconde, toujours explicite. Un « ne pas toucher » rendrait le
* résultat dépendant du preset appliqué juste avant : passer d'Économe à
* Qualité maximale garderait les 30 fps du premier, sans que rien ne le dise.
*/
fps: number;
/**
* `SimpleOutput/RecQuality` : OBS en déduit un CRF (x264) ou un CQP (matériel).
* On évite volontairement le mode à débit fixe, qui gâche des bits sur les
* plans statiques et sature sur les plans animés.
*/
quality: 'Small' | 'HQ' | 'Lossless';
/** `mkv` survit à un plantage ; `hybrid_mp4` est lisible partout. */
format: 'mkv' | 'hybrid_mp4';
audioBitrateKbps: number;
/** Valeur de vitesse à écrire, selon la famille d'encodeur retenue. */
speed: Record<SpeedFamily, string>;
}
/**
* Catalogue ordonné du plus léger au plus lourd.
*
* Le levier de qualité est le CRF (`quality`), celui de charge CPU est la
* vitesse d'encodage (`speed`) puis la définition. Un preset lourd sur une VM
* sous-dimensionnée fait chuter les images par seconde : la qualité perçue
* baisse alors, malgré un CRF meilleur. Surveille « Frames perdues » sur la
* fiche de l'agent après un changement.
*/
export const RECORDING_PRESETS: RecordingPreset[] = [
{
id: 'light',
label: 'Économe',
summary:
'720p 30 fps, encodage rapide. Pour les VM à petit CPU ou plusieurs captures en parallèle.',
cpuCost: 1,
height: 720,
fps: 30,
quality: 'Small',
format: 'hybrid_mp4',
audioBitrateKbps: 128,
speed: { x264: 'superfast', nvenc: 'p3', qsv: 'speed', amd: 'speed' },
},
{
id: 'balanced',
label: 'Équilibré',
summary: "1080p 30 fps, qualité élevée. Le compromis par défaut : bonne image sans saturer un CPU modeste.",
cpuCost: 2,
height: 1080,
fps: 30,
quality: 'HQ',
format: 'hybrid_mp4',
audioBitrateKbps: 160,
speed: { x264: 'veryfast', nvenc: 'p5', qsv: 'balanced', amd: 'balanced' },
},
{
id: 'archive',
label: 'Qualité maximale',
summary:
"Définition de la scène, 60 fps, encodage lent. Exige un CPU confortable ou un encodeur matériel.",
cpuCost: 3,
height: null,
fps: 60,
quality: 'HQ',
format: 'mkv',
audioBitrateKbps: 192,
speed: { x264: 'medium', nvenc: 'p6', qsv: 'quality', amd: 'quality' },
},
{
id: 'lossless',
label: 'Sans perte',
summary:
'Aucune compression destructrice, 60 fps. Des dizaines de Go par heure : à réserver aux captures courtes.',
cpuCost: 4,
height: null,
fps: 60,
quality: 'Lossless',
format: 'mkv',
audioBitrateKbps: 320,
// OBS impose ultrafast en sans perte ; les autres valeurs sont là pour la forme.
speed: { x264: 'ultrafast', nvenc: 'p1', qsv: 'speed', amd: 'speed' },
},
];
export const DEFAULT_RECORDING_PRESET_ID = 'balanced';
export function findRecordingPreset(id: string): RecordingPreset | null {
return RECORDING_PRESETS.find((preset) => preset.id === id) ?? null;
}
/** Preset choisi pour un agent. Réglable indépendamment sur chaque VM. */
export interface RecordingSettings {
/** Tant que c'est faux, l'agent ne touche jamais aux réglages d'OBS. */
enabled: boolean;
presetId: string;
encoder: RecordingEncoder;
}
export const DEFAULT_RECORDING_SETTINGS: RecordingSettings = {
enabled: false,
presetId: DEFAULT_RECORDING_PRESET_ID,
encoder: DEFAULT_RECORDING_ENCODER,
};
export function normalizeRecordingSettings(raw: unknown): RecordingSettings {
const input = (raw ?? {}) as Partial<RecordingSettings>;
const base = DEFAULT_RECORDING_SETTINGS;
return {
enabled: input.enabled === true,
presetId:
typeof input.presetId === 'string' && findRecordingPreset(input.presetId)
? input.presetId
: base.presetId,
encoder: isRecordingEncoder(input.encoder) ? input.encoder : base.encoder,
};
}
/** Compte rendu détaillé d'une application de preset, remonté au dashboard. */
export interface PresetApplyResult {
presetId: string;
presetLabel: string;
encoder: RecordingEncoder;
/** Paramètres écrits, sous la forme `clé=valeur`. */
applied: string[];
/** Paramètres refusés par OBS, avec le motif. */
skipped: string[];
/** Résumé lisible du réglage vidéo obtenu (définition et fluidité de sortie). */
video: string;
/**
* Vrai si l'encodeur configuré diffère de celui qu'OBS a chargé au démarrage :
* la nouvelle valeur est écrite, mais OBS continuera d'utiliser l'ancienne
* jusqu'à son redémarrage.
*/
restartRequired: boolean;
}
// ---------------------------------------------------------------------------
@@ -341,6 +580,7 @@ export interface WelcomeMessage {
autoConnectObs: boolean;
watch: WatchSettings;
browser: BrowserSettings;
recording: RecordingSettings;
}
export interface CommandMessage {
@@ -356,6 +596,7 @@ export interface ConfigMessage {
autoConnectObs: boolean;
watch: WatchSettings;
browser: BrowserSettings;
recording: RecordingSettings;
}
export interface PingMessage {
@@ -382,6 +623,7 @@ export interface AgentView {
autoConnectObs: boolean;
watch: WatchSettings;
browser: BrowserSettings;
recording: RecordingSettings;
notes: string | null;
status: AgentStatus;
}