ProtoFlix

Documentation · Vos fichiers

La décision de lecture

Lire tel quel, remuxer ou convertir : les entrées, l’algorithme et les codes de raison.

Statut : lots 3a, 4a, 4b, sous-titres et chaîne matérielle du Pixel 6 implémentés (2026-11-06). internal/playback.Decide calcule 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 porte blocked (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, action convert (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, raison subtitle_burn_in_unavailable, paramètre cause (no_encoder, hdr_video, no_video). ASS : conversion avec perte de style, pas d'incrustation pour l'instant.
  • Le conteneur webm n'est pas distingué de matroska par 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éeSourceFiabilité
Description technique de la version et de ses pistesAnalyse 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éesProfil connu + déclaration du client (§3)Non fiable : validée et bornée
Préférences de la demandePiste audio, sous-titres, qualité max, position de départValidées (identifiants de pistes existants, bornes)
Politique du compteDébit et résolution max (dont qualité max à distance), transcodage autorisé, limite d’âge (ADR 0009)Côté serveur, fait foi
Contexte réseauLocal / distant (adresse source après résolution des proxys de confiance), limite configuréeEstimation ; jamais une mesure garantie
Capacités serveurEncodeurs, décodeurs, filtres au niveau validated ou experimental autorisé ; disponibilité instantanée du Resource ManagerIssues 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

  1. 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, experimental ou untested, issu de la matrice de compatibilité. Par défaut, le moteur n'utilise que validated. experimental est activable par l'administrateur. untested ne sert jamais à une décision.
  2. 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.
  3. 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é.
  4. 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 raison client_profile_fallback.
  5. 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.

mermaid
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 --> P

Détails :

  1. 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).
  2. 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.
  3. Sous-titres. Voir §5. L'incrustation force le transcodage vidéo.
  4. Conteneur. Direct Play seulement si le conteneur d'origine est livrable tel quel au client et que toutes les pistes retenues sont en copy et que les pistes non retenues ne posent pas de problème de lecture connu. Sinon remux en HLS fMP4.
  5. Mode global. direct_play si tout est copy dans le conteneur d'origine ; remux si tout est copy mais que le conteneur change ; transcode si au moins une piste est réencodée. Le plan précise la granularité (partial: true si la vidéo est copiée).
  6. Ressources. Pour transcode, le moteur consulte un instantané de disponibilité fourni en entrée. S'il n'y a pas de capacité : refus resources_exhausted, ou dégradation (résolution inférieure) seulement si la politique l'autorise. Ce choix est inscrit dans le plan.
  7. 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

SituationActionCoûtRaison
Aucun sous-titre ou sous-titre désactivériennul—
Piste texte (SRT, WebVTT)extract + conversion en WebVTT servi à partfaible, sans réencodage vidéosubtitle_converted (si le format change)
Piste texte stylée (ASS/SSA), client qui ne rend pas ASSconversion en WebVTT avec perte de style et option d'incrustationfaible ou élevésubtitle_style_lost ou subtitle_burn_in_required
Piste bitmap (PGS, VobSub, DVB) et client sans rendu bitmapburn-inélevé : force le transcodage vidéosubtitle_burn_in_required
Sous-titre externe (fichier à côté)même logique, selon le format——

Ordre de préférence :

  1. rendu par le client ;
  2. extraction ou conversion sans toucher à la vidéo ;
  3. 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_vision avec 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.
  • Tone mapping HDR → SDR (raison hdr_tonemap_required) : uniquement avec un couple (filtre, backend) au niveau validated pour ce format source. Sinon refus explicite hdr_tonemap_unavailable plutô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 (raison audio_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.

CodeSensParamètres
container_unsupportedLe conteneur d'origine n'est pas livrable tel quelcontainer
video_codec_unsupportedCodec vidéo non décodable par le clientcodec
video_profile_unsupportedProfil, niveau, profondeur ou chroma hors capacitéprofile, level, bit_depth, chroma
video_resolution_exceeds_limitRésolution au-dessus de la limite effectivewidth, height, limit
video_framerate_unsupportedFréquence d'image non acceptéefps
bitrate_exceeds_limitDébit au-dessus de la limite (réseau, politique ou qualité choisie)bitrate, limit, limit_source
hdr_unsupported / hdr_tonemap_requiredFormat HDR non géré, tone mapping requishdr_format, dv_profile
hdr_tonemap_unavailableAucune chaîne de tone mapping validéehdr_format
hdr_dynamic_metadata_droppedMétadonnées dynamiques perdueshdr_format
audio_codec_unsupportedCodec audio non décodablecodec
audio_channels_unsupportedDownmix nécessairechannels, max_channels
audio_object_metadata_droppedMétadonnées objet (Atmos) perdues—
subtitle_convertedSous-titre converti de format, sans perte connuefrom, to
subtitle_style_lostConversion avec perte de stylefrom, to
subtitle_burn_in_requiredIncrustation, cause de transcodage vidéoformat
user_policy_limitLimite imposée par la politique de l'utilisateur ou du profilpolicy
user_requested_qualityQualité inférieure demandée explicitementquality
client_profile_fallbackProfil client inconnu ou invalide : profil prudentrequested_profile
server_capability_degradedDégradation à cause des ressources serveurresource
video_interlacedImage entrelacée : la copie provoque des erreurs de décodage aléatoires dans Chromium, désentrelacement requis—
audio_track_selectionPiste 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électionnerstream
hdr_tone_mappedHDR converti en SDR par la chaîne matériellehdr_format
subtitle_burn_in_unavailablePiste bitmap choisie, incrustation impossible ici ; lecture sans elleformat, cause
subtitle_not_yet_supportedSous-titre texte que FFmpeg ne convertit pas, ou serveur sans FFmpeg ; l'image et le son ne changent pasformat

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

json
{
  "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)

json
{
  "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)

json
{
  "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)

json
{
  "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/quick ou fuzzing Go) :
    • un plan direct_play ne contient que des copy ;
    • une raison accompagne toujours un transcode ;
    • une déclaration client ne peut pas élargir le profil ;
    • le résultat est déterministe.
  • 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.