L’image se fige, le son continue : détecter un blocage que libvlc appelle encore Playing

Une image figée n’est pas une condition d’erreur. Le moteur continue d’annoncer Playing, et la seule chose qui change, c’est un delta de compteur qui tombe à zéro.

displayed +25   read +50,000   demux +48,000   audio +40   engine state: Playing
displayed  +0   read +50,000   demux +48,000   audio +40   engine state: Playing

Deux échantillons d’une seconde, l’un à la suite de l’autre. Un seul nombre a changé. Les octets arrivent toujours au même rythme, le démultiplexeur les consomme toujours, les tampons audio atteignent toujours la carte son, et le moteur annonce Playing sur les deux lignes et continuera de l’annoncer aussi longtemps que vous laisserez la chaîne ouverte. Ce qui est à l’écran, c’est la dernière image qui a atteint la sortie vidéo. Ce sont les constantes que notre jeu de test donne au détecteur, pas une capture prise sur une vraie chaîne : un flux à 4 Mbit/s lit environ 500 000 octets par seconde, un ordre de grandeur au-dessus de ces constantes, et ses nombres de lecture et de démultiplexage se suivent de bien plus près qu’un écart soutenu de 4 pour cent. C’est la forme qui se transpose. Rien dans cette paire n’est une erreur, aucun événement d’erreur ne la suit, et ces quatre deltas sont tout ce qu’un détecteur obtient comme preuves.

Tout ce qui suit est mesuré sur libvlc 3.0.21 via LibVLCSharp 3.9.7 dans notre application Windows de bureau. L’API de statistiques a bougé d’une version majeure de libvlc à l’autre, donc lisez les affirmations sur les compteurs comme des affirmations sur la 3.x.

libvlc annonce Playing pendant que l’image est figée

L’état du moteur répond à une question plus étroite que celle qu’on y lit. Playing veut dire que le thread de lecture n’a pas été arrêté, mis en pause ni terminé. Cela n’affirme rien sur le fait qu’une image ait atteint l’écran dans les quatre dernières secondes, ni sur le fait que l’image qui l’a atteint contenait quoi que ce soit.

Notre spécification de watchdog énonce la prémisse sur laquelle repose tout le sous-système : libvlc reste indéfiniment sur Playing pendant un gel comme pendant un flux noir, et EncounteredError est réservé aux échecs durs d’ouverture et de protocole, donc il ne se déclenche presque jamais pour les deux modes de défaillance que les spectateurs signalent réellement. Cette phrase est notre lecture du moteur, pas une ligne tirée de sa documentation, et c’est la raison pour laquelle aucune des règles ci-dessous ne demande au moteur comment il va.

La conséquence observable, c’est la taille de la taxonomie qu’il a fallu construire. Neuf classes sont définies : aucun octet à la couche d’accès, une fin de flux silencieuse sur une chaîne en direct, l’erreur du moteur lui-même, des octets qui arrivent pendant que le démultiplexeur ne produit rien, la vidéo bloquée avec l’audio vivant, les deux bloqués pendant que les données circulent, des images noires sur un pipeline sain, une horloge d’entrée arrêtée, et des remises en tampon répétées à l’intérieur d’une fenêtre glissante.

Trois de ces neuf s’appuient sur des événements du moteur. L’une est l’erreur du moteur. Les deux autres sont des événements ordinaires qui ne sont pas du tout des erreurs : une fin de flux ou un arrêt silencieux sur une chaîne que personne n’a arrêtée, et un compte rendu au niveau du cache que nous comptabilisons dans une boucle de remise en tampon. Six sont déduites des compteurs, même si l’horloge arrêtée parmi elles s’appuie aussi sur l’événement d’horloge du moteur. Et neuf surestime ce que le système sait dire, parce que la classe de l’horloge arrêtée n’est jamais déclarée nulle part. Elle existe pour corroborer et n’a pas de code de taxonomie à elle.

La carte d’échec jetait autrefois tout cela et affichait une seule phrase pour tous les cas, si bien qu’un spectateur dont la source refusait la connexion et un spectateur dont l’appareil ne savait pas décoder la vidéo lisaient exactement les mêmes mots. Chaque classe correspond désormais à un code, et une même classe ne veut pas dire la même chose de part et d’autre de la première image, donc la correspondance porte les deux. Aucun octet pendant le délai de grâce de connexion, c’est un flux qui n’a jamais commencé. Le même blocage dix minutes plus tard, c’est un flux qui s’est arrêté.

Détecter un gel vidéo, c’est quatre deltas de compteur dans un ordre fixe

Un échantillon compte onze champs : un horodatage monotone et dix compteurs cumulatifs, lus sur le moteur en une seule passe. Huit des dix donnent un delta contre l’échantillon précédent. Les deux autres, les images en retard abandonnées et les tampons audio abandonnés, n’en donnent à personne ; ils ont été collectés au cas où une règle en voudrait, et aucune règle n’en a jamais voulu. Quatre deltas portent les verdicts :

  • Les octets lus, à la couche d’accès. Zéro veut dire que rien n’arrive du réseau.
  • Les octets consommés par le démultiplexeur. Une lecture non nulle avec un démultiplexage nul veut dire que les octets qui arrivent ne contiennent aucun programme.
  • Les images affichées, à la sortie vidéo. C’est le signal de vivacité vidéo principal, et la section suivante explique pourquoi on ne peut pas le croire sur parole.
  • Les tampons audio joués, à la sortie audio. C’est ce qui sépare un gel vidéo d’un gel total.

La vidéo décodée est le seul compteur d’appoint qui fasse autorité : il prend le relais quand le compteur d’images affichées se révèle inutilisable. L’audio décodé n’a pas de rôle équivalent. Il est lu une fois, pour prouver qu’un flux audio existe. Les unités corrompues et les discontinuités forment la paire restante, et un pic sur l’une ou l’autre arme des fenêtres de gel raccourcies pendant un moment.

L’ordre compte plus que n’importe quel seuil pris isolément, et le code en donne la raison sur place :

// F5 / F6 presentation freezes. Suppressed while demux starvation is suspected: starvation
// stalls presentation too, and F4's longer window owns the classification (its ladder entry
// differs). F1 needs no such guard; it shares the shorter window and is checked first.

L’accès est testé en premier, le démultiplexeur en deuxième, la présentation en dernier. Une couche d’accès morte affame tout ce qui se trouve en aval, donc une image qui s’arrête quatre secondes après les octets est un symptôme et non un diagnostic, et la signaler comme un gel envoie l’échelle de récupération au mauvais barreau. Nos fenêtres actuelles sont de quatre secondes d’octets à zéro, six pour un démultiplexeur affamé, quatre pour une présentation bloquée. Ce sont des valeurs de réglage qui bougeront la prochaine fois que quelqu’un les mesurera ; c’est l’ordre qui porte le sens.

Le soupçon est mémorisé comme un instant, pas comme un drapeau. Quatre marqueurs portent chacun l’horodatage auquel leur condition a commencé, et un verdict, c’est un marqueur dont l’âge a dépassé sa fenêtre. Un seul tick sain remet le marqueur à null et l’âge disparaît : la fenêtre est donc une soustraction et non une comptabilité tenue à chaque tick. Le pic de corruption est l’exception qui confirme la forme : il mémorise une expiration au lieu d’un début, rien ne l’efface par avance, et aucun verdict ne lit son âge.

Une note de portée, avant que les seuils ne se mettent à avoir l’air universels. Tout ceci est étalonné sur du MPEG-TS brut. Nous transportons le type de source sous la forme ts ou hls, mais uniquement pour la télémétrie et pour des seuils par type que personne n’a encore écrits. En HLS, le démultiplexeur adaptatif lance ses propres récupérations en aval, donc la séparation de haut niveau entre accès et démultiplexage ne veut pas dire ce que cette section dit qu’elle veut dire, et aucun seuil que nous livrons n’est spécifique à HLS.

Pourquoi le compteur d’images dit-il qu’une chaîne saine n’arrête pas de geler ?

Certains chemins de sortie vidéo et de décodage matériel sous-comptent les images affichées, et il n’existe aucun moyen de demander sur lequel on se trouve. Notre propre commentaire de code en accuse le module d’accès, ce qui ne peut pas être juste : la couche d’accès se situe en amont du démultiplexage et du décodage, et elle ne touche jamais le compteur qu’incrémente le cœur de la sortie vidéo. Le commentaire est faux, et le comportement qu’il décrit est réel.

Alors, pendant les trente premières secondes qui suivent la première présentation d’un flux, la machine observe au lieu de juger. À la fin de la fenêtre, elle s’engage sur l’une de trois positions réparties en cinq branches : le compteur d’images affichées a avancé et on lui fait confiance ; le compteur d’images affichées est mort mais la vidéo décodée a avancé et devient le signal de repli ; ou bien tous les compteurs vidéo sont morts alors que l’horloge d’entrée avance et qu’une sortie vidéo existe, donc le chemin sous-compte et les règles de gel s’éteignent pour la session. Une quatrième branche attrape une chaîne mal étiquetée, où rien n’a bougé côté vidéo et où aucune sortie vidéo n’est apparue mais où le son joue, et c’est la vivacité audio qui prend le relais. Une cinquième, par défaut, fait confiance au compteur.

Un garde-fou surplombe tout cela. Aucun verdict de gel n’est possible tant qu’un compteur vidéo n’a pas avancé au moins une fois pendant la session, parce qu’un compteur qui n’a jamais bougé est indiscernable d’un compteur qui sous-compte, et le cas du vraiment-mort-depuis-le-début appartient au classificateur de démarrage, qui dispose d’autres preuves et d’une autre entrée dans l’échelle. Quarante-cinq ticks d’une seconde avec du son qui joue, l’horloge qui avance et une sortie vidéo présente, tous les compteurs vidéo plats, ne déclarent rien.

Le cas limite qui a façonné l’étalonnage, c’est la mire à faible cadence d’images, un carton d’identification de chaîne ou un visuel de radio qui se rafraîchit à environ une demi-image par seconde. Sur un échantillonneur d’une seconde, cette chaîne passe un tick sur deux à zéro image affichée, la signature exacte d’un gel. L’étalonnage détecte la cadence et élargit la fenêtre de gel de quatre secondes à quinze, et c’est l’élargissement qui est la moitié contre-intuitive : l’instinct pousse à juger plus vite sur une chaîne qui a moins à montrer, et cela produit un faux verdict sur toutes les mires que vous possédez. Un arrêt complet de la mire reste attrapé, sur la fenêtre élargie.

Armer un soupçon qu’aucune règle ne peut consommer a un coût, et nous l’avons payé. Un blocage audio à côté d’une vidéo saine levait autrefois un soupçon sur une chaîne vidéo, là où aucun verdict ne peut jamais le lire, si bien que la session restait dans cet état toute sa vie : à échantillonner deux fois plus vite et à n’émettre jamais le signal de stabilité qui purge le registre de récupération. Chaque panne ultérieure de cette session héritait d’une échelle dont les tentatives étaient déjà dépensées et allait droit à la carte d’abandon. Un soupçon qui ne peut pas devenir un verdict n’est pas prudent. C’est une fuite.

Une pause ressemble exactement à un gel

Tous les compteurs de l’échantillon s’arrêtent sur une pause utilisateur, exactement selon le motif que produit un gel total.

Il n’y a aucun moyen de distinguer les deux à partir des nombres, alors la pause n’est pas chronométrée du tout : elle masque les verdicts indéfiniment, et dix minutes de compteurs figés derrière une pause ne déclarent rien. Un saut dans le flux reçoit plutôt un masque borné de cinq secondes, parce qu’un saut aboutit ou n’aboutit pas. La mise en tampon suspend les verdicts de présentation mais laisse la règle d’accès armée, au motif que zéro octet pendant une mise en tampon, c’est une source morte plutôt qu’un lien lent, puisqu’un lien lent montre quand même des octets qui arrivent.

Les réouvertures sont l’autre endroit où l’arithmétique mord. Les compteurs d’octets sont sur 32 bits et rattachés à un seul média, donc ils repartent de zéro quand un nouveau média est appliqué et ils débordent sur une session assez longue. Dans les deux cas, le delta revient négatif, et un delta négatif sur l’un quelconque des six compteurs de vivacité réinitialise la référence et saute l’évaluation pour ce tick, au lieu de signaler un blocage de tout le pipeline.

Détecter une image noire oblige à regarder les pixels

Un flux noir décode, affiche, joue le son et tire des octets au rythme habituel. Toutes les règles vues jusqu’ici voient un flux sain. C’est le seul chemin du détecteur qui doive regarder les pixels, et il lui faut une vraie image capturée sur la sortie vidéo en 96 par 54.

Un seul seuil de luma ne suffit pas, et la raison, c’est le contenu. Le verdict prend deux axes et de la répétition :

  • La proportion de pixels noirs à 0.98 ou au-dessus, un pixel comptant comme noir à une luma de 24 ou moins.
  • L’écart-type de luma à 4.0 ou en dessous, c’est ce qui épargne une scène sombre mais structurée.
  • Quatre échantillons noirs consécutifs, c’est ce qu’un fondu ne peut pas satisfaire.

Les deux échecs liés au contenu sont figés par des tests plutôt qu’argumentés. Une scène de nuit, presque entièrement proche du noir avec quelques structures éclairées par la lune, échoue sur les deux axes à la fois. Un contenu à bandes noires ressort à moins de la moitié de noir, parce que les bandes sont minoritaires dans l’image, même sur un format large.

L’argument du fondu a une branche que je ne devrais pas sauter. Pendant les dix secondes qui suivent la confirmation d’une récupération, la série exigée tombe de quatre échantillons à deux, environ trois secondes au plancher configuré, et davantage en pratique, ce qu’un fondu lent peut tenir. C’est un compromis délibéré : les preuves qui ont condamné la source il y a un instant tiennent toujours, donc une source qui revient cassée est recondamnée plus vite, et le garde-fou du fondu est ce que nous dépensons pour l’acheter. L’intervalle de 1500 ms est lui aussi un plancher plutôt qu’un espacement. La sonde se réarme dessus et se déclenche au message suivant qui arrive, donc l’espacement réel est plus long : sept secondes, puis neuf, dans l’exécution ci-dessous.

Le moment où la sonde tourne est une décision qui a un coût visible, et la description honnête est plus étroite que ce que le nom laisse croire. Elle tourne une fois, en balayage, juste après la première présentation d’un flux, et de nouveau à la transition vers le soupçon quand c’est un blocage vidéo qui l’a levé. Une fois le balayage d’ouverture refermé, elle ne tourne plus tant que la session reste saine. Un noir en cours de lecture sur un flux par ailleurs sain ne produit donc aucun verdict, de façon reproductible, et nous avons choisi de ne pas le corriger : l’attraper coûte une capture sur chaque session de chaque flux, pour détecter ce que la personne qui regarde voit instantanément, et zapper ailleurs puis revenir relance le balayage de toute façon.

Une deuxième limite, que je ne peux pas refermer avec des preuves. La capture passe par l’appel de capture du moteur lui-même, et la relecture depuis une surface décodée en matériel est un endroit bien connu où un lecteur rend une image vide pour des raisons sans rapport avec ce qui est à l’écran. La capture qui n’arrive jamais, nous la gérons : c’est la preuve d’une sortie vidéo coincée, qui corrobore un verdict de compteur mais ne déclare jamais toute seule. Celle qui arrive et qui est noire à tort, non, et je n’ai aucune mesure de la fréquence à laquelle cela se produit sur notre chemin.

Ce avec quoi je suis le moins à l’aise, c’est ce que la sonde mesure puis jette. Chaque échantillon calcule un hachage moyen sur 64 bits du plan de luma ainsi qu’une luma moyenne, les deux voyagent dans le message, et le moteur de règles ne lit ni l’un ni l’autre. Un flux figé sur une image fixe lumineuse, sur une chaîne dont le compteur d’images s’est étalonné comme non fiable, c’est précisément ce que deux hachages égaux sur des échantillons espacés attraperaient. Nous ne les comparons pas, et savoir si le cas est assez fréquent pour valoir la règle est une question à laquelle nous ne pouvons pas répondre, pour une raison à laquelle arrivent les deux dernières sections. Un détail du hachage m’a assez surpris pour que je le garde : une image uniforme se hache en tout à un, chacun des 64 bits mis à un, parce que la moyenne de chaque cellule tombe exactement sur la moyenne globale et que la comparaison est un supérieur ou égal. Le blanc uni et le noir uni donnent le même hachage.

Le pire bug ici, c’était un gel qui n’avait pas lieu

Lire les statistiques du lecteur n’est pas un appel sûr. Chacune de ses lignes déréférence des handles natifs que le moteur libère quand l’utilisateur zappe, quitte la page, ou quand la reconstruction d’une swapchain met le lecteur à la retraite, et l’échantillonneur tourne sur sa propre tâche. Perdre cette course ne lève rien qu’un catch puisse absorber : libvlc plante sur une violation d’accès et le processus disparaît sans rien laisser dans le journal. L’échantillonneur tourne donc derrière le même verrou que celui qu’utilise le pool de lecteurs.

Derrière ce verrou, une décision de mise en cache qui a l’air correcte prise isolément a produit le pire bug que ce sous-système ait livré. L’échantillonneur gardait le wrapper de média au lieu de le récupérer à chaque tick, parce que le récupérer une fois par seconde semblait du gaspillage. Un wrapper de média LibVLCSharp prend sa propre référence native, donc le wrapper mis en cache est resté valide après que le moteur eut échangé le média sous lui, sur un thread du pool. Il a continué de répondre. Il n’a jamais levé d’exception. Il renvoyait les compteurs du média mort, qui ne bougent pas, ce qui est un blocage total des octets pour toutes les règles de la machine.

Le symptôme, c’était une chaîne saine signalée comme un réseau mort. Le coût, c’était un changement de source qui abandonnait un flux qui fonctionnait, observé en direct pendant une recette sur le terrain, le 2026-07-24 à 23:24. Le commentaire qui surplombe désormais le correctif le dit sans détour :

// Acquire and dispose the wrapper on every tick. A cached wrapper holds its own native
// retain, so after a pool-thread media swap it would keep returning the DEAD media's
// frozen counters without ever throwing, which reads as a net stall on a healthy stream
// (observed live: [...] 2026-07-24 23:24). One retain/release per second is negligible.

Un détecteur qui lit une entrée périmée n’échoue pas discrètement. Il produit le même verdict, avec la même assurance, à propos d’une chaîne qui joue parfaitement, et ensuite il agit dessus. Toutes les autres défaillances d’un watchdog vous coûtent un blocage que vous n’avez pas attrapé. Celle-ci vous coûte un blocage que vous avez inventé, et le spectateur voit la récupération.

Voici ce que douze secondes de gel émettent réellement

Voici une session scriptée passée dans le moteur de règles livré, sur le harnais de test existant, douze ticks d’une seconde. Elle a été exécutée plutôt que déroulée à la main, et le pilote a été supprimé ensuite. Lisez les nombres comme des valeurs de test. Chaque demande de capture a reçu une réponse 300 ms plus tard, comme le ferait un vrai hôte, puisque le budget de capture est d’une seconde.

La session s’ouvre, annonce Playing, et signale une sortie vidéo. Ensuite :

tdReaddDemuxdDisplayeddAudioétat aprèseffets émis
1 s+50 000+48 000+25+40Startingaucun
2 s+50 000+48 000+25+40HealthyStateChanged, SetSampleRate 1 Hz, RequestSnapshot
3 s+50 000+48 000+25+40Healthyaucun
4 s+50 000+48 000+25+40Healthyaucun
5 s+50 000+48 000+25+40Healthyaucun
6 s+50 000+48 000+25+40Healthyaucun
7 s+50 000+48 000+0+40SuspectStateChanged, SetSampleRate 2 Hz, RequestSnapshot
8 s+50 000+48 000+0+40Suspectaucun
9 s+50 000+48 000+0+40SuspectRequestSnapshot
10 s+50 000+48 000+0+40Suspectaucun
11 s+50 000+48 000+0+40RecoveringStateChanged, FaultDeclared VideoFreeze, âge 4000 ms
12 s+50 000+48 000+0+40Recoveringaucun

Le tick 1 ne produit rien, parce qu’un seul échantillon n’est pas un delta ; il devient la référence. Le tick 2 est la première paire évaluée, la progression de la présentation est prouvée, et la machine quitte Starting. Elle réaffirme au passage une cadence d’échantillonnage qu’elle utilisait déjà, ce qui est redondant et sans conséquence. La demande de capture sur cette ligne, c’est le balayage d’ouverture, et l’hôte y a répondu avec du vrai contenu, 6 pour cent de pixels noirs pour un écart-type de 41, donc le balayage s’est refermé.

Les ticks 3 à 6 n’émettent absolument rien. C’est le régime établi voulu : un flux sain ne produit aucun effet, donc un journal vide est le cas de réussite.

Le tick 7, c’est là que l’image s’arrête. Le marqueur de blocage vidéo est estampillé à 7 s, la machine passe à Suspect, l’échantillonneur double à 2 Hz, et la sonde est armée et demande une image. Rien n’est déclaré, parce qu’un marqueur d’âge zéro n’est pas un verdict. Les ticks 8 et 10 sont la fenêtre qui s’écoule, et le tick 9 est la sonde qui rééchantillonne à sa propre cadence, deux secondes après la dernière demande plutôt que les 1500 ms configurées, parce qu’elle se déclenche au message suivant passé l’instant dû. Les octets et le son ne faiblissent jamais pendant tout ça : cette session a l’air parfaite pour toutes les règles sauf une.

Le tick 11, c’est le verdict. Le marqueur de blocage vidéo atteint quatre secondes, le démultiplexeur n’est pas soupçonné, le son a avancé pendant le blocage et avance encore, donc c’est un gel vidéo et non un gel total, et l’âge d’anomalie transmis à l’exécuteur est de 4000 ms. Rejouez le même script avec le son qui s’arrête lui aussi au tick 7 et la même ligne déclare un gel total. Rejouez-le encore avec la capture qui expire au lieu de renvoyer du contenu et le verdict ne change pas, mais sa chaîne de motif devient video stalled while audio keeps playing (snapshot timeout observed) : la sortie coincée a corroboré, elle n’a pas décidé.

Le tick 12 n’émet rien parce que la machine a passé la main. La suite Playback autour de ce code compte 108 tests, tous au vert à l’heure où j’écris.

Un verdict achète cinq barreaux, et l’un d’eux n’est pas construit

Le verdict est une entrée d’échelle, pas une réponse. Il existe cinq barreaux : une relance par pause puis lecture, que seuls les gels totaux obtiennent ; une réapplication complète de la même URL ; un passage au candidat suivant ; une reconstruction du lecteur lui-même ; et l’abandon. Une panne survenant dans les quinze secondes qui suivent le lancement d’une chaîne entre au barreau du changement plutôt qu’à celui de la réapplication, parce qu’une source qui ne s’est jamais montrée à la hauteur ne mérite pas qu’on réessaie l’URL qui vient d’échouer. Les sessions mûres gardent la réapplication en premier, si bien qu’un hoquet dix minutes plus tard ne coûte pas un changement de source au spectateur. Il y a exactement une tentative de redémarrage avant l’escalade, parce que les données du terrain ont montré que la deuxième n’aide jamais.

Le barreau de reconstruction est pour l’instant une fiction assumée. Il n’a pas de primitive hôte, il est désactivé dans les réglages livrés, et la seule condition qui y mène, un arrêt qui a expiré en laissant la session native coincée, termine aujourd’hui l’échelle de premier plan et atterrit sur l’abandon à la place. C’est prudent et c’est correct, c’est aussi une lacune, et le code le dit à l’endroit où cela se produit.

Entre deux tentatives, l’attente est exponentielle avec la première tentative gratuite : la base multipliée par deux à la puissance de l’indice de tentative, moins un, plafonnée, puis affectée d’une gigue pouvant aller jusqu’à vingt pour cent dans un sens ou dans l’autre. En pratique : partir immédiatement, puis une seconde, trois, sept, et ainsi de suite jusqu’au plafond. La gigue multiplie après le plafond et non avant, donc le vrai maximum est de trente-six secondes, pas les trente que le plafond laisse croire. Petit détail, mais c’est le genre de petit détail qui fait qu’un graphe a l’air faux à 3 h du matin.

Les pixels restent sur la machine qui les a capturés

La sonde écrit deux fichiers PNG en rotation dans le dossier de données local de l’application et les écrase sur place. Rien ne les téléverse et rien ne les envoie où que ce soit ; l’image existe juste assez longtemps pour être réduite à deux nombres et un hachage, puis elle est écrasée par la suivante. Les verdicts de panne partent dans le journal local, sur la même machine, et une chaîne y est identifiée par les seize premiers caractères hexadécimaux d’un SHA-256, jamais par une URL.

Ce dernier point est la raison pour laquelle je ne peux pas vous dire à quel point le cas de l’image figée lumineuse est répandu. Les formes d’événements de télémétrie sont écrites et typées, elles nomment un téléverseur par lots dans leur commentaire de documentation, et ce téléverseur n’existe pas. Trois types d’enregistrement sont déclarés et ne sont jamais construits nulle part dans le code. Tout jugement porté dans cet article sur les modes de défaillance qui comptent vient d’une seule recette sur le terrain, d’un banc d’injection de fautes et d’une suite de tests, pas d’un parc. C’est une vraie limite à la confiance qu’on peut accorder à tout ce qui précède.

Si vous préférez vous servir du détecteur plutôt que d’en lire la description, il est activé par défaut dans l’application Windows My TV Player, et vous pouvez créer un compte gratuit pour l’essayer. Quand il fait son travail, vous ne le verrez pas : l’image saccadera pendant quatre secondes puis reviendra, et rien ne sera apparu à l’écran pour vous dire qu’un verdict a été rendu.

Ce que cet article a mesuré37 affirmations, chacune avec les preuves derrière elles
RéclamationPreuveCompté
Tout ce que contient cet article est mesuré sur libvlc 3.0.21 via LibVLCSharp 3.9.7 dans l’application Windows. L’API de statistiques diffère dans libvlc 4.x, donc les affirmations sur les compteurs ne valent que pour la 3.x.clients/windows/docs/PLAYBACK-WATCHDOG-PLAN.md:7 pins the head to LibVLCSharp 3.9.7 / libvlc 3.0.21; docs/research/MTP-PlaybackWatchdog-Spec-v1.0.md:5 scopes the spec to libvlc 3.x with forward-compatible notes for 4.x.SpécificationSans objet
libvlc reste indéfiniment sur Playing pendant un gel comme pendant un flux noir, et EncounteredError est réservé aux échecs durs d’ouverture et de protocole, donc il ne se déclenche presque jamais pour les deux modes de défaillance dominants en direct.docs/research/MTP-PlaybackWatchdog-Spec-v1.0.md:18 (within the cited 15-19 block), header checked, no confidentiality marking.SpécificationSans objet
Neuf classes de panne sont définies, F1 à F9, et huit d’entre elles seulement peuvent être déclarées un jour. F8 ClockStall n’est déclarée nulle part et n’a pas de code de taxonomie à elle.n = 922 août 2026
Trois des neuf classes s’appuient sur des événements du moteur. F3 est l’erreur du moteur lui-même, F2 est déclarée à partir de EndReached ou d’un Stopped silencieux, et F9 est déclarée à partir de Buffering. Les autres sont déduites des compteurs.n = 322 août 2026
La carte d’échec affichait autrefois une seule phrase pour toutes les classes, et chaque classe correspond désormais à un code de taxonomie qui dépend aussi du fait que la panne soit tombée avant ou après la première image.n = 122 août 2026
Un échantillon de statistiques compte onze champs : un horodatage monotone et dix compteurs cumulatifs. Huit donnent un delta ; aucune règle ne tire jamais de delta de LostPictures ni de LostAudioBuffers.n = 1122 août 2026
Seule la vidéo dispose d’un compteur de repli. DecodedVideo prend le relais quand DisplayedPictures est inutilisable ; DecodedAudio ne sert qu’à prouver qu’un flux audio existe, et aucune règle ne le lit jamais comme un signal de vivacité.n = 122 août 2026
Les verdicts sont examinés dans cet ordre : l’accès d’abord, le démultiplexeur ensuite, la présentation en dernier, et les règles de gel se taisent tant qu’une famine du démultiplexeur est soupçonnée.n = 122 août 2026
Réglage actuel : 4 s d’octets à zéro pour F1, 6 s pour F4, 4 s pour F5 et F6, 15 s pour une mire à faible cadence d’images.n = 122 août 2026
Quatre marqueurs nullables retiennent l’instant où chaque condition a commencé, et un seul tick sain les efface. Un cinquième signal, le pic de corruption et de discontinuités, fonctionne à l’envers : il mémorise un instant d’expiration et raccourcit les fenêtres de gel tant qu’il est armé.n = 422 août 2026
Le modèle de compteurs est étalonné sur du MPEG-TS brut. Le type de source est transporté sous la forme “ts” ou “hls” pour la télémétrie et pour de futurs seuils par type, et aucun seuil des options n’est spécifique à HLS aujourd’hui.n = 122 août 2026
Les compteurs d’octets de libvlc 3.x sont sur 32 bits et propres à un média, donc ils repartent de zéro à une réouverture et débordent sur les longues sessions. Dans les deux cas, le delta devient négatif.WatchdogStateMachine.cs:274-275 states both causes in place ("libvlc zeroes stats on reopen and int counters can wrap"). libvlc 3.x declares i_read_bytes and i_demux_read_bytes as int in libvlc_media_stats_t.SpécificationSans objet
Un delta négatif sur l’un quelconque des six compteurs de vivacité réinitialise la référence et saute l’évaluation pour ce tick. Les deltas d’unités corrompues et de discontinuités ne font pas partie de ce garde-fou.n = 622 août 2026
Le compteur d’images est étalonné sur une fenêtre de 30 s vers Trusted, FallbackDecodedVideo ou Untrusted à travers cinq branches, et les verdicts de gel exigent en plus qu’un compteur vidéo ait avancé au moins une fois pendant la session.n = 522 août 2026
Les chemins qui sous-comptent les images affichées sont des chemins de sortie vidéo et de décodage matériel, pas des modules d’accès.WatchdogStateMachine.cs:441 says "this access/vout combo underreports". The access module sits upstream of demux and decode and cannot influence i_displayed_pictures, which the vout core increments.SpécificationSans objet
45 ticks d’une seconde avec du son qui joue, l’horloge qui avance et une sortie vidéo présente, mais tous les compteurs vidéo morts, ne déclarent aucune panne et aboutissent à Untrusted.n = 4522 août 2026
Une mire mesurée à environ une demi-image par seconde élargit la fenêtre de gel à 15 s au lieu de la raccourcir, et un arrêt complet de la mire reste détecté sur la fenêtre élargie.n = 122 août 2026
Un blocage audio à côté d’une vidéo saine pouvait lever un soupçon sans jamais pouvoir atteindre un verdict, si bien qu’une session restait en SUSPECT toute sa vie à la cadence d’échantillonnage accélérée, n’émettait jamais le signal de stabilité et laissait le registre de récupération non purgé, de sorte que les pannes suivantes héritaient d’une échelle déjà épuisée et allaient droit à la carte d’abandon.n = 122 août 2026
Une pause utilisateur masque indéfiniment des compteurs figés ; un saut dans le flux ne les masque que 5 s ; la mise en tampon suspend les verdicts de présentation pendant que la règle des octets à zéro continue de tourner.n = 322 août 2026
Réglage actuel du noir : un pixel compte comme noir à une luma de 24 ou moins, un échantillon est noir à 98 pour cent de pixels noirs avec un écart-type de luma de 4.0 ou moins, et le verdict demande 4 échantillons consécutifs, capturés en 96 par 54.n = 122 août 2026
Pendant 10 s après la confirmation d’une tentative de récupération, le nombre d’échantillons noirs consécutifs exigés tombe à 2.n = 122 août 2026
1500 ms est un plancher de réarmement, pas un espacement. La sonde se déclenche au premier message qui arrive à l’instant dû ou après, donc l’espacement réel est plus long : 2 s là où les messages arrivent à 1 Hz, comme dans l’exécution consignée ici.n = 122 août 2026
Une scène de nuit échoue sur les deux axes du noir et un contenu à bandes noires est noir à moins de la moitié ; les deux sont figés comme non noirs par des tests.n = 222 août 2026
La sonde s’exécute en un balayage unique juste après la première présentation du flux, puis de nouveau à la transition vers SUSPECT quand c’est un blocage vidéo qui a levé le soupçon. Une fois le balayage d’ouverture refermé, elle ne tourne plus jamais en régime établi sain, si bien qu’un noir en cours de lecture sur un flux par ailleurs sain ne produit aucun verdict.n = 222 août 2026
La sonde passe par l’appel de capture du moteur lui-même, et la relecture d’une capture depuis une surface décodée en matériel est un endroit connu où un lecteur peut renvoyer une image vide pour des raisons sans rapport avec le contenu. Nous traitons la capture qui échoue comme un élément de corroboration ; nous n’avons aucune mesure de la fréquence à laquelle une relecture réussie revient noire à tort.PlaybackEngine.Watchdog.cs:253 calls MediaPlayer.TakeSnapshot (libvlc_video_take_snapshot); the timeout path is handled at WatchdogStateMachine.cs:867-873.SpécificationSans objet
Un hachage moyen sur 64 bits et une luma moyenne sont calculés sur chaque échantillon de la sonde et voyagent tous deux dans le message, et le moteur de règles ne lit ni l’un ni l’autre.n = 122 août 2026
Une image uniforme produit un hachage moyen dont tous les bits sont à un, parce que chaque cellule tombe sur la moyenne globale et que la comparaison est un supérieur ou égal. Le blanc uni et le noir uni sont donc indiscernables pour ce hachage.n = 122 août 2026
Un use-after-free à l’intérieur de libvlc se manifeste par une violation d’accès native, pas par une exception CLR, donc aucun catch managé ne s’exécute.clients/windows/src/MyTvPlayer.Shared/Media/LiveTvPreviewMediaHelper.LibVlc.cs:165-180, reproduced 2026-08-19, 0xC0000005 in libvlc_media_player_release with a concurrent stats read.SpécificationSans objet
Un wrapper Media LibVLCSharp mis en cache détient sa propre référence native, donc après un échange de média sur un thread du pool il continue de renvoyer les compteurs figés du média mort sans jamais lever d’exception.libvlc 3.x doxygen for libvlc_media_player_get_media: the returned media is reference counted and the caller must release it, which is why a cached LibVLCSharp Media keeps the old media alive and keeps answering. Recorded verbatim at clients/windows/src/MyTvPlayer.Shared/Media/Watchdog/PlaybackWatchdog.cs:322-325.SpécificationSans objet
Un wrapper de média mis en cache a fait rapporter à l’échantillonneur les compteurs figés d’un média mort sur un flux sain, lus comme un blocage réseau. Observé en direct pendant une recette sur le terrain, le 2026-07-24 à 23:24. Corrigé en acquérant et en libérant le wrapper à chaque tick.n = 124 juil. 2026
Douze ticks scriptés d’une seconde atteignent HEALTHY à 2 s, SUSPECT à 7 s et déclarent F5 VideoFreeze à 11 s avec un âge d’anomalie de 4000 ms. Le même script, avec le compteur audio qui s’arrête lui aussi, déclare F6 TotalFreeze au même tick, et une troisième exécution dont les captures en SUSPECT expirent déclare F5 avec la chaîne de motif étendue en “video stalled while audio keeps playing (snapshot timeout observed)”.n = 1222 août 2026
La suite de tests Playback exécute 108 tests avec 0 échec.n = 10822 août 2026
Cinq barreaux : une relance par pause puis lecture, réservée aux gels totaux, une réapplication complète, un passage au candidat suivant, une reconstruction du lecteur, puis l’abandon. Une seule tentative de redémarrage avant l’escalade, et une panne survenant dans les 15 s après le lancement entre au barreau du changement plutôt qu’à celui de la réapplication.n = 522 août 2026
Le barreau de reconstruction du lecteur n’a pas encore de primitive hôte et il est désactivé dans les réglages livrés, donc un épisode d’arrêt en expiration termine aujourd’hui l’échelle de premier plan et va à l’abandon.n = 122 août 2026
La tentative k attend min(base fois (2^k - 1), plafond) avec une gigue symétrique, donc la première tentative part immédiatement et la croissance à partir de la deuxième est de 1 s, 3 s, 7 s jusqu’au plafond. La gigue s’applique après le plafond, donc les attentes au plafond se dispersent symétriquement autour de 30 s et le vrai maximum est de 36 s.n = 122 août 2026
La sonde écrit deux fichiers PNG en rotation dans le dossier de données local de l’application et les écrase. Les événements de panne partent dans le journal local, où une chaîne est identifiée par les 16 premiers caractères hexadécimaux d’un SHA-256 et jamais par une URL.n = 122 août 2026
Les types d’enregistrement de télémétrie sont déclarés et jamais construits, et le téléverseur par lots nommé dans leur commentaire de documentation n’existe pas.n = 322 août 2026