Feat : OBS presets
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user