Statut : lots 3a, 4a, 4b, sous-titres et chaîne matérielle du Pixel 6 implémentés (2026-11-06).
internal/playback.Decidecalcule le plan complet, avec toutes les raisons, et tout ce qu'il décide est exécuté : lecture directe, remux, conversion audio, encodage vidéo logiciel. Un plan impossible porteblocked(hdr_tonemap_unavailable,no_compatible_plan,transcoder_unavailable). Écarts provisoires :
- HDR : un film HDR que le client ne peut pas afficher est refusé (
hdr_tonemap_unavailable) : pas de mappage de tons avant sa validation (phase 5, §6).- Les profils sont en Go (
internal/playback/profiles.go), pas encore dans un format déclaratif.- Toutes leurs règles sont
experimental, car le banc de compatibilité n'existe pas. Elles décident par défaut (PROTOFLIX_PLAYBACK_EXPERIMENTAL=true), sinon rien ne serait lisible. Le plan l'indique (support: experimental).- Sous-titres texte (2026-11-06) : intégrés au fichier, servis en WebVTT à part de l'image (
sidecar_webvtt), toutes les pistes étant disponibles sans nouvelle session. L'image et le son ne changent pas. Bitmap, formats que FFmpeg ne convertit pas, ou serveur sans FFmpeg :subtitle_not_yet_supported(nouveau code), lecture inchangée. Fichiers posés à côté du film : mêmes pistes, convertis par le serveur sans FFmpeg, actionconvert(naming.md§8). Incrustation (2026-11-06) : une piste bitmap choisie (PGS, VobSub, DVB) force le réencodage de la vidéo (burn_in,subtitle_burn_in_required), seulement si elle est demandée. Sans encodeur ou sur une vidéo HDR (pas de mappage de tons) : lecture sans elle, raisonsubtitle_burn_in_unavailable, paramètrecause(no_encoder,hdr_video,no_video). ASS : conversion avec perte de style, pas d'incrustation pour l'instant.- Le conteneur
webmn'est pas distingué dematroskapar l'analyse : un fichier WebM part donc en remux.- La décision par fréquence d'image reste à faire. Les capacités serveur se résument à deux drapeaux :
CanConvert(FFmpeg trouvé),CanEncodeVideo(libx264 a passé son test de démarrage). Les ressources sont gérées à l'exécution :resources_exhausted, sans dégradation automatique.
Décisions liées : ADR 0010 (moteur FFmpeg), ADR 0011 (protocoles de livraison). Le contrat HTTP de négociation est décrit dans api/openapi.yaml ; ce document décrit la logique qui produit sa réponse.
1. Rôle
Le moteur reçoit une demande de lecture et produit un Playback Plan : un objet typé, déterministe, inspectable et testable sans FFmpeg ni base de données. Il ne lance rien. Le Session Manager exécute ensuite le plan : il sert le fichier tel quel ou confie un job au Transcoding Engine.
Règles :
- Fonction pure. Mêmes entrées, même plan. Aucune lecture d'horloge, de disque ou de réseau dans le moteur ; ces données arrivent en entrée.
- Pas de conditions dans les handlers HTTP. Le handler valide la requête, construit les entrées et appelle le moteur.
- Chaque compromis a une raison, sous forme de code stable (§6). Un plan sans raison est un Direct Play intégral.
- La qualité d'origine est le défaut. Disposer d'un transcodeur n'est jamais une raison de transcoder.
2. Entrées
| Entrée | Source | Fiabilité |
|---|---|---|
| Description technique de la version et de ses pistes | Analyse ffprobe stockée (media_files, media_streams) | Fiable si l'analyse est à jour (version de l'analyseur et empreinte du fichier) |
| Capacités client normalisées | Profil connu + déclaration du client (§3) | Non fiable : validée et bornée |
| Préférences de la demande | Piste audio, sous-titres, qualité max, position de départ | Validées (identifiants de pistes existants, bornes) |
| Politique du compte | Débit et résolution max (dont qualité max à distance), transcodage autorisé, limite d’âge (ADR 0009) | Côté serveur, fait foi |
| Contexte réseau | Local / distant (adresse source après résolution des proxys de confiance), limite configurée | Estimation ; jamais une mesure garantie |
| Capacités serveur | Encodeurs, décodeurs, filtres au niveau validated ou experimental autorisé ; disponibilité instantanée du Resource Manager | Issues des tests de capacité, pas de la simple détection |
Le moteur ne reçoit jamais de chemin de fichier : il manipule des identifiants de fichier et de piste. La résolution vers un chemin a lieu dans storage, au moment de l'exécution.
3. Représentation des capacités client
3.1 Le problème
Une matrice « appareil × codec × profil × niveau × profondeur × conteneur × protocole » est ingérable si chaque appareil est une ligne écrite à la main, et dangereuse si l'on fait aveuglément confiance à ce que le client déclare.
3.2 Modèle retenu
- Profils nommés et versionnés maintenus dans le dépôt (
server/internal/device/profiles/, format déclaratif), par famille de client et non par modèle d'appareil :web-chromium@1,web-firefox@1,web-safari@1,conservative@1. Chaque profil liste des règles de capacité :- conteneurs de livraison acceptés (
mp4,webm,hls-fmp4…) ; - par codec vidéo : profils, niveau max, profondeur de bits, sous-échantillonnage chroma, résolution et fréquence max, formats HDR ;
- par codec audio : canaux max, fréquences d'échantillonnage ;
- formats de sous-titres rendus par le client (
webvtt, et éventuellement d'autres) ; - débit max conseillé. Chaque règle porte un niveau
validated,experimentalouuntested, issu de la matrice de compatibilité. Par défaut, le moteur n'utilise quevalidated.experimentalest activable par l'administrateur.untestedne sert jamais à une décision.
- conteneurs de livraison acceptés (
- Déclaration du client. Le client envoie le nom de profil qu'il revendique et, en option, le résultat de sondes locales (sur le web :
MediaSource.isTypeSupported,MediaCapabilities.decodingInfo,canPlayType). Le serveur valide le schéma, borne les valeurs et ignore les clés inconnues. - Intersection prudente. Capacité effective = profil connu ∩ déclaration du client. Une déclaration peut retirer une capacité (le navigateur dit ne pas décoder HEVC), jamais en ajouter une absente du profil validé. Exception : un mode « profil personnalisé » réservé à l'administrateur, journalisé.
- Repli conservateur. Profil absent, inconnu ou déclaration invalide :
conservative@1(H.264 High ≤ niveau 4.1, 8 bits, 4:2:0, ≤ 1080p ; AAC-LC stéréo ; HLS fMP4 ; sous-titres WebVTT ou incrustés). Le plan porte alors la raisonclient_profile_fallback. - Versionnement. Le plan enregistre le profil et la version utilisés (
web-chromium@1) pour que le diagnostic d'une session passée reste interprétable après une mise à jour des profils.
3.3 Pourquoi cela reste gérable
- Le nombre de profils croît avec les familles de moteurs de décodage, pas avec les appareils.
- Les variantes fines (profil, niveau, profondeur) sont des attributs de règle, pas de nouvelles lignes.
- Les sondes du client corrigent localement (pilote, OS, réglage) sans élargir ce qui a été validé.
- Le banc de test produit les niveaux de support. Un profil ne change qu'en même temps que les résultats de test qui le justifient.
4. Algorithme
Ordre de préférence global : Direct Play > Remux (Direct Stream) > transcodage partiel (audio et/ou sous-titres seulement) > transcodage vidéo. La décision est prise par piste, puis le mode global découle de la combinaison.
flowchart TD
A[Demande de lecture] --> B[Valider la demande<br/>et les pistes choisies]
B --> C[Capacités effectives =<br/>profil connu ∩ déclaration]
C --> D[Appliquer la politique<br/>utilisateur et réseau]
D --> E{Vidéo : codec, profil,<br/>niveau, profondeur, HDR,<br/>résolution, débit OK ?}
E -- oui --> EV[Vidéo : copy]
E -- non --> ET[Vidéo : transcode<br/>+ raisons]
EV --> F{Audio choisi décodable ?}
ET --> F
F -- oui --> FA[Audio : copy]
F -- non --> FT[Audio : transcode<br/>+ raisons]
FA --> G{Sous-titre choisi}
FT --> G
G -- aucun / rendu client possible --> GS[Sous-titre : copy ou extract]
G -- conversion texte possible --> GC[Sous-titre : convert]
G -- bitmap non rendu client --> GB[Sous-titre : burn-in<br/>⇒ vidéo : transcode]
GS --> H{Conteneur source<br/>livrable tel quel ?}
GC --> H
GB --> H
H -- oui, toutes pistes copy --> DP[Mode : direct_play<br/>HTTP Range]
H -- non, toutes pistes copy --> RX[Mode : remux<br/>HLS fMP4]
H -- au moins une piste transcode --> TC[Mode : transcode<br/>HLS fMP4]
TC --> R{Ressources serveur<br/>disponibles ?}
R -- non --> X[Refus : resources_exhausted<br/>ou dégradation autorisée]
R -- oui --> P[Playback Plan]
DP --> P
RX --> PDétails :
- Vidéo. Copier si toutes les conditions sont vraies : codec, profil, niveau, profondeur, chroma, format HDR et résolution acceptés par les capacités effectives ; débit de la piste sous la limite effective (min de la politique utilisateur, de la limite réseau configurée et de la qualité demandée). Sinon, transcoder, avec une raison par condition violée (et non seulement la première).
- Audio. Copier si codec, canaux et fréquence sont acceptés. Sinon transcoder cette piste seulement, en conservant le nombre de canaux si la cible le permet (par exemple AAC multicanal), sinon downmix avec raison
audio_channels_unsupported. - Sous-titres. Voir §5. L'incrustation force le transcodage vidéo.
- Conteneur. Direct Play seulement si le conteneur d'origine est livrable tel quel au client et que toutes les pistes retenues sont en
copyet que les pistes non retenues ne posent pas de problème de lecture connu. Sinon remux en HLS fMP4. - Mode global.
direct_playsi tout estcopydans le conteneur d'origine ;remuxsi tout estcopymais que le conteneur change ;transcodesi au moins une piste est réencodée. Le plan précise la granularité (partial: truesi la vidéo est copiée). - Ressources. Pour
transcode, le moteur consulte un instantané de disponibilité fourni en entrée. S'il n'y a pas de capacité : refusresources_exhausted, ou dégradation (résolution inférieure) seulement si la politique l'autorise. Ce choix est inscrit dans le plan. - Aucune boucle silencieuse. Si aucun plan n'est possible (codec non décodable par FFmpeg, piste corrompue), refus avec un code d'erreur stable (
no_compatible_plan,media_unreadable).
5. Sous-titres
| Situation | Action | Coût | Raison |
|---|---|---|---|
| Aucun sous-titre ou sous-titre désactivé | rien | nul | — |
| Piste texte (SRT, WebVTT) | extract + conversion en WebVTT servi à part | faible, sans réencodage vidéo | subtitle_converted (si le format change) |
| Piste texte stylée (ASS/SSA), client qui ne rend pas ASS | conversion en WebVTT avec perte de style et option d'incrustation | faible ou élevé | subtitle_style_lost ou subtitle_burn_in_required |
| Piste bitmap (PGS, VobSub, DVB) et client sans rendu bitmap | burn-in | élevé : force le transcodage vidéo | subtitle_burn_in_required |
| Sous-titre externe (fichier à côté) | même logique, selon le format | — | — |
Ordre de préférence :
- rendu par le client ;
- extraction ou conversion sans toucher à la vidéo ;
- incrustation seulement en dernier recours.
L'incrustation est toujours visible dans le diagnostic comme cause de transcodage. Pour l'ASS, le choix entre perte de style et incrustation est une préférence utilisateur (défaut proposé : conversion avec perte de style, pour préserver la vidéo). À valider avec le design du lecteur.
Le web rend WebVTT via <track> ou via le moteur de rendu du lecteur ; le rendu ASS ou PGS dans le navigateur (bibliothèques JavaScript tierces) est une option future, non prévue en V1.
6. HDR et couleur
- Les formats sont distincts :
hdr10,hdr10plus,hlg,dolby_visionavec profil (5, 7, 8.1, 8.2, 8.4…) et compatibilité de couche de base. L'analyse stocke primaires, transfert, matrice et plage pour chaque piste vidéo. - Direct Play HDR seulement si la capacité effective du client inclut ce format et ce profil.
- Dolby Vision :
- profil 8.1 : la couche de base est HDR10. Si le client ne gère pas DV mais gère HDR10, le flux peut être servi comme HDR10, avec perte des métadonnées dynamiques (raison
hdr_dynamic_metadata_dropped). Faisabilité par copie (sans réencodage) à vérifier sur le banc. - profil 5 : pas de couche de base compatible. Sans support client, tone mapping obligatoire, et son résultat doit être validé par test avant d'être proposé.
- profil 7 (double couche) : non supporté en V1 hors Direct Play vers un client qui le gère.
- Pas de conversion DV générique.
- profil 8.1 : la couche de base est HDR10. Si le client ne gère pas DV mais gère HDR10, le flux peut être servi comme HDR10, avec perte des métadonnées dynamiques (raison
- Tone mapping HDR → SDR (raison
hdr_tonemap_required) : uniquement avec un couple (filtre, backend) au niveauvalidatedpour ce format source. Sinon refus explicitehdr_tonemap_unavailableplutôt qu'une image aux couleurs fausses. - Le transcodage SDR → SDR conserve primaires, transfert, matrice et plage d'origine ; une erreur de signalisation est un bug bloquant.
7. Audio
- Ne jamais réencoder une piste que le client décode.
- Le nombre de canaux est préservé si la cible le permet ; un downmix porte la raison
audio_channels_unsupported. - Passthrough (AC-3, E-AC-3, DTS, TrueHD, Atmos) : depuis un navigateur, le serveur ne promet aucun passthrough. Le navigateur décode ou refuse ; la sortie bitstream vers un amplificateur dépend de l'OS et du matériel, hors de contrôle du serveur. Les clients natifs pourront déclarer une capacité de passthrough, à tester par appareil.
- Atmos : métadonnées d'objet conservées seulement en copie (
copy) ; tout réencodage les perd (raisonaudio_object_metadata_dropped).
8. Codes de raison (stables)
Les codes sont un contrat : ajoutés, jamais renommés. Le texte lisible est produit par le client à partir du code (i18n), avec des paramètres structurés.
| Code | Sens | Paramètres |
|---|---|---|
container_unsupported | Le conteneur d'origine n'est pas livrable tel quel | container |
video_codec_unsupported | Codec vidéo non décodable par le client | codec |
video_profile_unsupported | Profil, niveau, profondeur ou chroma hors capacité | profile, level, bit_depth, chroma |
video_resolution_exceeds_limit | Résolution au-dessus de la limite effective | width, height, limit |
video_framerate_unsupported | Fréquence d'image non acceptée | fps |
bitrate_exceeds_limit | Débit au-dessus de la limite (réseau, politique ou qualité choisie) | bitrate, limit, limit_source |
hdr_unsupported / hdr_tonemap_required | Format HDR non géré, tone mapping requis | hdr_format, dv_profile |
hdr_tonemap_unavailable | Aucune chaîne de tone mapping validée | hdr_format |
hdr_dynamic_metadata_dropped | Métadonnées dynamiques perdues | hdr_format |
audio_codec_unsupported | Codec audio non décodable | codec |
audio_channels_unsupported | Downmix nécessaire | channels, max_channels |
audio_object_metadata_dropped | Métadonnées objet (Atmos) perdues | — |
subtitle_converted | Sous-titre converti de format, sans perte connue | from, to |
subtitle_style_lost | Conversion avec perte de style | from, to |
subtitle_burn_in_required | Incrustation, cause de transcodage vidéo | format |
user_policy_limit | Limite imposée par la politique de l'utilisateur ou du profil | policy |
user_requested_quality | Qualité inférieure demandée explicitement | quality |
client_profile_fallback | Profil client inconnu ou invalide : profil prudent | requested_profile |
server_capability_degraded | Dégradation à cause des ressources serveur | resource |
video_interlaced | Image entrelacée : la copie provoque des erreurs de décodage aléatoires dans Chromium, désentrelacement requis | — |
audio_track_selection | Piste audio autre que celle par défaut : un lecteur à qui l'on donne le fichier joue la piste par défaut, le serveur doit la sélectionner | stream |
hdr_tone_mapped | HDR converti en SDR par la chaîne matérielle | hdr_format |
subtitle_burn_in_unavailable | Piste bitmap choisie, incrustation impossible ici ; lecture sans elle | format, cause |
subtitle_not_yet_supported | Sous-titre texte que FFmpeg ne convertit pas, ou serveur sans FFmpeg ; l'image et le son ne changent pas | format |
Les erreurs de refus (no_compatible_plan, resources_exhausted, media_unreadable, storage_offline…) relèvent du contrat d'erreurs de l'API, pas des raisons.
9. Forme du Playback Plan
Les exemples suivent la forme cible. L'API publique n'en expose qu'une vue réduite aux non-administrateurs : pas d'identifiant de job, pas d'encodeur, pas de chemin.
9.1 Direct Play
{
"plan_version": 1,
"mode": "direct_play",
"partial": false,
"delivery": { "protocol": "http_range", "container": "mp4" },
"client_profile": "web-chromium@1",
"tracks": [
{ "kind": "video", "stream": "str_01JD…", "action": "copy",
"source": { "codec": "h264", "profile": "high", "level": 4.1, "width": 1920, "height": 1080, "bit_depth": 8 } },
{ "kind": "audio", "stream": "str_01JE…", "action": "copy",
"source": { "codec": "aac", "channels": 2 } },
{ "kind": "subtitle", "stream": "str_01JF…", "action": "extract",
"source": { "format": "subrip" }, "output": { "format": "webvtt" } }
],
"reasons": [ { "code": "subtitle_converted", "params": { "from": "subrip", "to": "webvtt" } } ]
}La conversion d'un sous-titre externe ne casse pas le Direct Play de l'audio et de la vidéo.
9.2 Remux (conteneur MKV, pistes compatibles)
{
"plan_version": 1,
"mode": "remux",
"partial": false,
"delivery": { "protocol": "hls", "segment_container": "fmp4" },
"client_profile": "web-firefox@1",
"tracks": [
{ "kind": "video", "stream": "str_…", "action": "copy", "source": { "codec": "h264" } },
{ "kind": "audio", "stream": "str_…", "action": "copy", "source": { "codec": "aac", "channels": 6 } }
],
"reasons": [ { "code": "container_unsupported", "params": { "container": "matroska" } } ]
}9.3 Transcodage partiel (audio seulement)
{
"plan_version": 1,
"mode": "transcode",
"partial": true,
"delivery": { "protocol": "hls", "segment_container": "fmp4" },
"client_profile": "web-chromium@1",
"tracks": [
{ "kind": "video", "stream": "str_…", "action": "copy",
"source": { "codec": "hevc", "profile": "main10", "hdr": "hdr10" } },
{ "kind": "audio", "stream": "str_…", "action": "transcode",
"source": { "codec": "truehd", "channels": 8 },
"output": { "codec": "aac", "channels": 6, "bitrate": 384000 } }
],
"reasons": [
{ "code": "container_unsupported", "params": { "container": "matroska" } },
{ "code": "audio_codec_unsupported", "params": { "codec": "truehd" } },
{ "code": "audio_channels_unsupported", "params": { "channels": 8, "max_channels": 6 } },
{ "code": "audio_object_metadata_dropped", "params": {} }
]
}Cet exemple suppose que le profil client valide HEVC Main10 HDR10. Ce n'est pas acquis : voir la matrice.
9.4 Transcodage vidéo (incrustation PGS + tone mapping)
{
"plan_version": 1,
"mode": "transcode",
"partial": false,
"delivery": { "protocol": "hls", "segment_container": "fmp4" },
"client_profile": "conservative@1",
"tracks": [
{ "kind": "video", "stream": "str_…", "action": "transcode",
"source": { "codec": "hevc", "profile": "main10", "width": 3840, "height": 2160, "hdr": "hdr10" },
"output": { "codec": "h264", "profile": "high", "width": 1920, "height": 1080, "bit_depth": 8, "max_bitrate": 12000000 },
"filters": ["tonemap", "scale", "subtitle_overlay"] },
{ "kind": "audio", "stream": "str_…", "action": "copy", "source": { "codec": "aac", "channels": 2 } },
{ "kind": "subtitle", "stream": "str_…", "action": "burn_in", "source": { "format": "hdmv_pgs_subtitle" } }
],
"reasons": [
{ "code": "client_profile_fallback", "params": { "requested_profile": "unknown-tv@3" } },
{ "code": "video_codec_unsupported", "params": { "codec": "hevc" } },
{ "code": "hdr_tonemap_required", "params": { "hdr_format": "hdr10" } },
{ "code": "video_resolution_exceeds_limit", "params": { "width": 3840, "height": 2160, "limit": "1920x1080" } },
{ "code": "subtitle_burn_in_required", "params": { "format": "hdmv_pgs_subtitle" } }
],
"admin": { "engine": "ffmpeg", "backend": "software", "encoder": "libx264", "job": "job_…" }
}Le bloc admin n'est renvoyé qu'avec la permission d'administration.
10. Tests attendus
- Table de cas (fixtures de description technique, pas de vrais fichiers) : un cas par code de raison, plus les combinaisons (bitmap + HDR, débit + résolution).
- Propriétés à vérifier par tests génératifs (
testing/quickou fuzzing Go) :- un plan
direct_playne contient que descopy; - une raison accompagne toujours un
transcode; - une déclaration client ne peut pas élargir le profil ;
- le résultat est déterministe.
- un plan
- Tests de non-régression : tout changement de profil versionné relance la table complète.
Source : docs/media/playback-decision.md, dans le dépôt de ProtoFlix.