Das Bild friert ein, der Ton läuft weiter: einen Stillstand erkennen, den libvlc weiterhin Playing nennt
Ein stehendes Bild ist kein Fehlerzustand. Die Engine meldet weiterhin Playing, und das Einzige, was sich ändert, ist ein Zähler-Delta, das auf null geht.
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
Zwei benachbarte Messungen im Abstand von einer Sekunde. Eine Zahl hat sich geändert. Bytes kommen weiter mit derselben Rate an, der Demuxer verbraucht sie weiter, Audiopuffer erreichen weiter die Soundkarte, und die Engine meldet auf beiden Zeilen Playing und meldet es weiter, solange Sie den Sender offen lassen. Auf dem Bildschirm steht das letzte Bild, das den Videoausgang erreicht hat. Das sind die Konstanten, die unser Testaufbau dem Detektor vorsetzt, keine Aufzeichnung von einem echten Sender: Ein Stream mit 4 Mbit/s liest etwa 500.000 Bytes pro Sekunde, eine Größenordnung über diesen Konstanten, und seine Zahlen für read und demux laufen viel enger beieinander als ein dauerhafter Abstand von 4 Prozent. Was sich überträgt, ist die Form. Nichts an dem Paar ist ein Fehler, kein Fehlerereignis folgt darauf, und diese vier Deltas sind das gesamte Beweismaterial, das ein Detektor bekommt.
Alles Weitere ist gegen libvlc 3.0.21 über LibVLCSharp 3.9.7 in unserer Windows-Desktop-App gemessen. Die Statistik-API hat sich zwischen den Hauptversionen von libvlc verschoben, lesen Sie die Aussagen zu den Zählern deshalb als Aussagen über 3.x.
libvlc meldet Playing auch bei stehendem Bild
Der Zustand der Engine beantwortet eine engere Frage, als die Leute in ihn hineinlesen. Playing heißt, dass der Wiedergabe-Thread nicht gestoppt, nicht pausiert und nicht beendet wurde. Es behauptet nicht, dass in den letzten vier Sekunden ein Bild auf dem Schirm angekommen ist, und auch nicht, dass auf dem Bild, das angekommen ist, irgendetwas zu sehen war.
Unsere Watchdog-Spezifikation nennt die Prämisse, auf der das ganze Teilsystem ruht: libvlc bleibt bei einem Freeze und bei einem schwarzen Feed unbegrenzt in Playing, und EncounteredError ist harten Fehlern beim Öffnen und beim Protokoll vorbehalten, feuert also für die beiden Fehlerbilder, die Zuschauer tatsächlich melden, so gut wie nie. Dieser Satz ist unsere Lesart der Engine und keine Zeile aus ihrer Dokumentation, und er ist der Grund, warum keine der Regeln weiter unten die Engine fragt, wie es ihr geht.
Die beobachtbare Folge ist der Umfang der Taxonomie, die wir bauen mussten. Neun Klassen sind definiert: keine Bytes an der Zugriffsschicht, ein stilles Streamende auf einem Livesender, der eigene Fehler der Engine, ankommende Bytes bei einem Demuxer, der nichts produziert, Video steht bei lebendigem Ton, beides steht, während Daten fließen, schwarze Bilder auf einer gesunden Pipeline, eine stehen gebliebene Eingangsuhr und wiederholtes Nachpuffern innerhalb eines gleitenden Fensters.
Drei dieser neun hängen an Ereignissen der Engine. Eine ist der Fehler der Engine selbst. Die anderen beiden sind gewöhnliche Ereignisse und überhaupt keine Fehler: ein Streamende oder ein stiller Stopp auf einem Sender, den niemand gestoppt hat, und eine Meldung aus dem Cache, die wir auf eine Schleife aus Nachpuffern anrechnen. Sechs werden aus Zählern erschlossen, wobei die stehende Uhr darunter zusätzlich am Uhrereignis der Engine hängt. Und neun sagt mehr, als das System hergibt, denn die Klasse für die stehende Uhr wird nirgends festgestellt. Sie ist da, um zu bestätigen, und hat keinen eigenen Taxonomie-Code.
Die Fehlerkarte warf das früher alles weg und zeigte für jeden Fall denselben einen Satz, ein Zuschauer, dessen Quelle die Verbindung verweigerte, und ein Zuschauer, dessen Gerät das Video nicht dekodieren konnte, lasen also dieselben Worte. Jede Klasse bildet jetzt auf einen Code ab, und dieselbe Klasse bedeutet vor und nach dem ersten Bild etwas anderes, die Zuordnung trägt deshalb beides. Keine Bytes innerhalb der Karenzzeit beim Verbinden, das ist ein Stream, der nie begonnen hat. Derselbe Stillstand nach zehn Minuten ist einer, der aufgehört hat.
Vier Zähler-Deltas in fester Reihenfolge erkennen einen Video-Freeze
Eine Messung besteht aus elf Feldern: einem monotonen Zeitstempel und zehn kumulativen Zählern, in einem Durchgang aus der Engine gelesen. Bei acht der zehn wird die Differenz zur vorigen Messung gebildet. Bei den anderen beiden, verworfene verspätete Bilder und verworfene Audiopuffer, bildet niemand eine Differenz; sie wurden für den Fall gesammelt, dass eine Regel sie will, und keine Regel wollte sie je. Vier Deltas tragen die Urteile:
- Gelesene Bytes, an der Zugriffsschicht. Null heißt, dass aus dem Netz nichts ankommt.
- Bytes, die der Demuxer verbraucht. Ein read ungleich null bei einem demux von null heißt, es kommen Bytes an, in denen kein Programm steckt.
- Angezeigte Bilder, am Videoausgang. Das ist das wichtigste Lebenszeichen des Videos, und der nächste Abschnitt handelt davon, warum man ihm nicht auf den ersten Blick glauben darf.
- Abgespielte Audiopuffer, am Audioausgang. Daran hängt der Unterschied zwischen einem Video-Freeze und einem totalen.
Dekodiertes Video ist der einzige Hilfszähler mit Autorität: Er springt ein, wenn sich der Zähler der angezeigten Bilder als unbrauchbar erweist. Dekodierter Ton hat keine entsprechende Rolle. Er wird einmal gelesen, als Nachweis, dass es eine Tonspur gibt. Beschädigte Einheiten und Diskontinuitäten sind das übrige Paar, und ein Ausschlag bei einem von beiden schaltet für eine Weile verkürzte Freeze-Fenster scharf.
Die Reihenfolge wiegt schwerer als jede einzelne Schwelle, und der Code sagt an Ort und Stelle, warum (kurz: Freezes bei der Darstellung bleiben unterdrückt, solange Demuxer-Hunger im Verdacht steht, weil Hunger die Darstellung ebenfalls anhält und das längere Fenster von F4 die Einordnung besitzt):
// 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.
Der Zugriff wird zuerst geprüft, der Demuxer als Zweites, die Darstellung zuletzt. Eine tote Zugriffsschicht hungert alles aus, was hinter ihr liegt, ein Bild, das vier Sekunden nach den Bytes stehen bleibt, ist deshalb ein Symptom und keine Diagnose, und es als Freeze zu melden schickt die Leiter der Wiederherstellungsversuche auf der falschen Sprosse los. Unsere derzeitigen Fenster sind vier Sekunden ohne Bytes, sechs bei einem ausgehungerten Demuxer, vier bei stehender Darstellung. Das sind Einstellwerte, die sich verschieben, sobald sie jemand das nächste Mal misst; die Bedeutung trägt die Reihenfolge.
Der Verdacht wird als Zeitpunkt gespeichert, nicht als Flag. Vier Marken halten jeweils den Zeitstempel fest, an dem ihre Bedingung begann, und ein Urteil ist eine Marke, deren Alter ihr Fenster überschritten hat. Ein guter Tick setzt die Marke zurück auf null, und das Alter ist weg, das Fenster ist damit eine Subtraktion und keine Buchhaltung pro Tick. Der Ausschlag bei Korruption ist die Ausnahme, die diese Form bestätigt: Er speichert ein Ablaufdatum statt eines Beginns, nichts löscht ihn vorzeitig, und kein Urteil liest sein Alter.
Eine Anmerkung zum Geltungsbereich, bevor die Schwellen allgemeingültig aussehen. All das ist auf rohem MPEG-TS kalibriert. Wir führen die Art der Quelle als ts oder hls mit, aber nur für Telemetrie und für Schwellen je Art, die noch niemand geschrieben hat. Bei HLS holt der adaptive Demuxer weiter unten selbst nach, die Aufteilung in access und demux auf oberster Ebene bedeutet dort also nicht, was dieser Abschnitt sagt, und keine Schwelle, die wir ausliefern, ist HLS-spezifisch.
Warum sagt der Bildzähler, dass ein gesunder Sender ständig einfriert?
Manche Wege über den Videoausgang und über die Hardware-Dekodierung melden zu wenige angezeigte Bilder, und es gibt keine Möglichkeit zu fragen, auf welchem man gerade ist. Unser eigener Code-Kommentar gibt dafür dem Access-Modul die Schuld, und das kann nicht stimmen: Die Zugriffsschicht sitzt vor Demux und Dekodierung und fasst den Zähler nie an, den der Kern des Videoausgangs hochzählt. Der Kommentar ist falsch, und das Verhalten, das er beschreibt, gibt es wirklich.
In den ersten dreißig Sekunden, nachdem ein Stream zu darstellen beginnt, schaut die Maschine deshalb zu, statt zu urteilen. Am Ende des Fensters legt sie sich über fünf Zweige auf eine von drei Positionen fest: Der Zähler der angezeigten Bilder lief weiter und ist vertrauenswürdig; der Zähler der angezeigten Bilder ist tot, aber dekodiertes Video lief weiter und wird zum Ersatzsignal; oder jeder Videozähler ist tot, während die Eingangsuhr läuft und ein Videoausgang da ist, der Weg meldet also zu wenig, und die Freeze-Regeln schalten sich für diese Sitzung ab. Ein vierter Zweig fängt einen falsch etikettierten Sender ab, bei dem sich auf der Videoseite nichts bewegt hat und kein Videoausgang aufgetaucht ist, der Ton aber spielt, und dann übernimmt das Lebenszeichen des Tons. Ein fünfter glaubt dem Zähler im Zweifel.
Über all dem sitzt eine Absicherung. Ein Freeze-Urteil ist erst möglich, wenn ein Videozähler in dieser Sitzung mindestens einmal weitergelaufen ist, denn ein Zähler, der sich nie bewegt hat, ist von einem Zähler, der zu wenig meldet, nicht zu unterscheiden, und der Fall, der von Anfang an wirklich tot ist, gehört dem Klassifizierer für den Start, der andere Belege hat und an anderer Stelle in die Leiter einsteigt. Fünfundvierzig Ticks im Sekundentakt mit laufendem Ton, laufender Uhr und vorhandenem Videoausgang, jeder Videozähler flach, stellen nichts fest.
Der Grenzfall, der die Kalibrierung geformt hat, ist die Tafel mit niedriger Bildrate, eine Senderkennung oder ein Radiobild, das sich etwa mit einem halben Bild pro Sekunde erneuert. Bei einem Sampler im Sekundentakt steht dieser Sender bei jedem zweiten Tick auf null angezeigten Bildern, exakt die Signatur eines Freezes. Die Kalibrierung erkennt die Rate und verbreitert das Freeze-Fenster von vier Sekunden auf fünfzehn, und das Verbreitern ist die Hälfte, die der Intuition widerspricht: Der Reflex ist, auf einem Sender mit weniger zu zeigen früher zu urteilen, und das erzeugt auf jeder Tafel, die Sie haben, ein Fehlurteil. Ein vollständiger Stillstand der Tafel wird immer noch erwischt, auf dem breiteren Fenster.
Einen Verdacht scharfzuschalten, den keine Regel verwerten kann, kostet etwas, und wir haben bezahlt. Ein Stillstand beim Ton neben gesundem Video weckte früher auf einem Videosender Verdacht, wo ihn nie ein Urteil lesen kann, die Sitzung saß deshalb ihr ganzes Leben lang in diesem Zustand: doppelt so schnell abtasten und nie das Stabilitätssignal abgeben, das das Konto der Wiederherstellungsversuche löscht. Jeder spätere Fehler in dieser Sitzung erbte eine Leiter, deren Versuche schon verbraucht waren, und landete direkt auf der Karte, die das Aufgeben meldet. Ein Verdacht, der nicht zu einem Urteil werden kann, ist nicht vorsichtig. Er ist ein Leck.
Eine Pause sieht genauso aus wie ein Freeze
Bei einer Pause durch den Nutzer bleibt jeder Zähler in der Messung stehen, in genau dem Muster, das ein totaler Freeze erzeugt.
An den Zahlen sind die beiden nicht auseinanderzuhalten, die Pause wird deshalb gar nicht erst befristet: Sie verdeckt Urteile unbegrenzt, und zehn Minuten eingefrorene Zähler hinter einer Pause stellen nichts fest. Ein Sprung bekommt stattdessen eine begrenzte Verdeckung von fünf Sekunden, denn ein Sprung geht auf oder er geht nicht auf. Puffern setzt die Urteile über die Darstellung aus, lässt die Regel für den Zugriff aber scharf, mit der Begründung, dass null Bytes während des Pufferns eine tote Quelle sind und keine langsame Leitung, denn bei einer langsamen Leitung kommen weiter Bytes an.
Die andere Stelle, an der die Arithmetik zubeißt, sind erneute Öffnungen. Die Byte-Zähler sind 32 Bit breit und gelten für genau eine Media, sie beginnen also wieder bei null, sobald eine neue Media angewendet wird, und sie laufen in einer hinreichend langen Sitzung über. So oder so kommt das Delta negativ zurück, und ein negatives Delta bei einem der sechs Zähler, die als Lebenszeichen gelten, setzt den Bezugswert zurück und überspringt die Auswertung für diesen Tick, statt einen Stillstand der ganzen Pipeline zu melden.
Um schwarze Bilder zu erkennen, muss man auf die Pixel sehen
Ein schwarzer Feed dekodiert, stellt dar, spielt Ton ab und zieht Bytes mit der gewohnten Rate. Jede Regel bis hierher sieht einen gesunden Stream. Das ist der eine Weg im Detektor, der auf die Pixel sehen muss, und er braucht ein echtes Bild, vom Videoausgang aufgenommen mit 96 mal 54.
Eine einzelne Luma-Schwelle reicht nicht, und der Grund ist der Inhalt. Das Urteil nimmt zwei Achsen und die Wiederholung:
- Anteil schwarzer Pixel bei 0.98 oder darüber, wobei ein Pixel bei Luma 24 oder darunter als schwarz gilt.
- Standardabweichung der Luma bei 4.0 oder darunter, und das ist es, was eine dunkle, aber strukturierte Szene verschont.
- Vier schwarze Proben hintereinander, und das ist es, was eine Blende nicht erfüllen kann.
Beide Fehlschläge am Inhalt sind per Test festgenagelt und nicht behauptet. Eine Nachtszene, überwiegend fast schwarz mit vereinzelter mondbeschienener Struktur, scheitert an beiden Achsen zugleich. Inhalt mit Letterbox-Balken kommt auf weniger als halb schwarz, denn die Balken sind selbst bei einem breiten Seitenverhältnis eine Minderheit im Bild.
Das Argument mit der Blende hat einen Zweig, den ich nicht überspringen sollte. Zehn Sekunden lang, nachdem sich eine Wiederherstellung bestätigt hat, sinkt die geforderte Serie von vier Proben auf zwei, also auf etwa drei Sekunden bei der konfigurierten Untergrenze und in der Praxis auf mehr, und das kann eine langsame Blende durchhalten. Das ist ein bewusster Handel: Die Belege, die die Quelle vor Augenblicken verurteilt haben, stehen noch, eine Quelle, die kaputt zurückkommt, wird deshalb schneller wieder verurteilt, und die Absicherung gegen Blenden ist das, was wir dafür ausgeben. Auch das Intervall von 1500 ms ist eine Untergrenze und kein Abstand. Die Sonde schaltet sich daran wieder scharf und löst bei der nächsten Nachricht aus, die eintrifft, der tatsächliche Abstand ist deshalb länger: sieben Sekunden und dann neun, in dem Lauf weiter unten.
Wann die Sonde läuft, ist eine Entscheidung mit sichtbaren Kosten, und die ehrliche Beschreibung fällt enger aus, als der Name vermuten lässt. Sie läuft einmal als Durchlauf, direkt nachdem ein Stream zum ersten Mal darstellt, und noch einmal beim Übergang in den Verdacht, wenn ein Video-Stillstand ihn ausgelöst hat. Sobald der erste Durchlauf beendet ist, läuft sie nicht wieder, solange die Sitzung gesund bleibt. Ein schwarzes Bild mitten in der Wiedergabe erzeugt auf einem ansonsten gesunden Stream deshalb kein Urteil, reproduzierbar, und wir haben uns entschieden, das nicht zu reparieren: Es einzufangen kostet einen Snapshot in jeder Sitzung jedes Streams, um zu erkennen, was der Mensch davor sofort sieht, und wer wegzappt und zurückkommt, löst den Durchlauf ohnehin neu aus.
Eine zweite Einschränkung, die ich nicht mit Belegen schließen kann. Die Aufnahme geht über den eigenen Snapshot-Aufruf der Engine, und das Zurücklesen von einer hardwaredekodierten Fläche ist eine wohlbekannte Stelle, an der ein Player ein leeres Bild zurückgibt, aus Gründen, die mit dem Bild nichts zu tun haben. Den Snapshot, der nie ankommt, behandeln wir: Er ist ein Beleg für einen festgefahrenen Videoausgang, bestätigt ein Urteil aus den Zählern, stellt aber nie allein etwas fest. Den, der ankommt und fälschlich schwarz ist, behandeln wir nicht, und ich habe keine Messung dazu, wie oft das auf unserem Weg passiert.
Am unwohlsten ist mir bei dem, was die Sonde misst und dann wegwirft. Jede Probe berechnet einen 64-Bit-Durchschnitts-Hash der Luma-Ebene und eine mittlere Luma, beide reisen in der Nachricht mit, und die Regel-Engine liest keines von beidem. Ein Stream, der auf einem hellen Standbild eingefroren ist, auf einem Sender, dessen Bildzähler als nicht vertrauenswürdig kalibriert wurde, ist genau das, was zwei gleiche Hashes über zeitlich getrennte Proben hinweg erwischen würden. Wir vergleichen sie nicht, und ob der Fall häufig genug ist, um die Regel wert zu sein, ist eine Frage, die wir nicht beantworten können, aus einem Grund, zu dem die letzten beiden Abschnitte kommen. Eine Einzelheit am Hash hat mich genug überrascht, um sie hier zu behalten: Ein flaches Bild hasht auf lauter Einsen, jedes der 64 Bits gesetzt, weil jedes Zellenmittel genau auf dem globalen Mittel liegt und der Vergleich größer-oder-gleich lautet. Reines Weiß und reines Schwarz sind derselbe Hash.
Der schlimmste Fehler hier war ein Freeze, den es gar nicht gab
Die Statistik des Players zu lesen ist kein sicherer Aufruf. Jede Zeile davon dereferenziert native Handles, die die Engine freigibt, wenn der Nutzer zappt, die Seite verlässt oder ein Neuaufbau der Swapchain den Player ausmustert, und der Sampler läuft auf einer eigenen Task. Wer dieses Rennen verliert, bekommt nichts, was ein catch auffangen könnte: libvlc stürzt mit einer Zugriffsverletzung ab, und der Prozess verschwindet, ohne etwas im Log zu hinterlassen. Der Sampler läuft deshalb hinter demselben Gate, das auch der Player-Pool benutzt.
Hinter diesem Gate hat eine Caching-Entscheidung, die für sich genommen richtig aussieht, den schlimmsten Fehler erzeugt, den dieses Teilsystem je ausgeliefert hat. Der Sampler hielt den Media-Wrapper fest, statt ihn bei jedem Tick zu holen, weil es verschwenderisch schien, ihn einmal pro Sekunde zu holen. Ein Media-Wrapper von LibVLCSharp nimmt sich eine eigene native Referenz, der zwischengespeicherte Wrapper blieb deshalb gültig, nachdem die Engine ihm die Media auf einem Pool-Thread unter den Füßen weggetauscht hatte. Er antwortete weiter. Er hat nie geworfen. Er lieferte die Zähler der toten Media, und die bewegen sich nicht, und das ist nach jeder Regel in der Maschine ein totaler Stillstand der Bytes.
Das Symptom war ein gesunder Sender, gemeldet als totes Netz. Der Preis war ein Wechsel der Quelle weg von einem funktionierenden Stream, live beobachtet während eines Abnahmelaufs im Feld am 2026-07-24 um 23:24. Der Kommentar, der heute über der Reparatur steht, sagt es unverblümt (kurz: den Wrapper bei jedem Tick holen und wieder freigeben; ein zwischengespeicherter Wrapper hält eine eigene native Referenz und liefert nach einem Wechsel der Media auf einem Pool-Thread weiter die eingefrorenen Zähler der toten Media, ohne je zu werfen):
// 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.
Ein Detektor, der veraltete Eingaben liest, scheitert nicht leise. Er erzeugt dasselbe Urteil, mit derselben Überzeugung, über einen Sender, der einwandfrei läuft, und handelt dann danach. Jeder andere Fehlschlag in einem Watchdog kostet Sie einen Stillstand, den Sie nicht erwischt haben. Dieser hier kostet Sie einen Stillstand, den Sie erfunden haben, und der Zuschauer sieht die Wiederherstellung.
Das gibt ein Freeze über zwölf Sekunden tatsächlich aus
Unten steht eine gescriptete Sitzung, durch die ausgelieferte Regel-Engine auf dem vorhandenen Testrahmen gelaufen, zwölf Ticks im Sekundentakt. Sie wurde ausgeführt und nicht von Hand nachvollzogen, und der Treiber wurde danach gelöscht. Lesen Sie die Zahlen als Vorgaben aus dem Testaufbau. Jede Snapshot-Anfrage wurde 300 ms später beantwortet, so wie es ein echter Host täte, denn das Budget für einen Snapshot ist eine Sekunde.
Die Sitzung öffnet, meldet Playing und meldet einen Videoausgang. Dann:
| t | dRead | dDemux | dDisplayed | dAudio | Zustand danach | ausgegebene Effekte |
|---|---|---|---|---|---|---|
| 1 s | +50.000 | +48.000 | +25 | +40 | Starting | keine |
| 2 s | +50.000 | +48.000 | +25 | +40 | Healthy | StateChanged, SetSampleRate 1 Hz, RequestSnapshot |
| 3 s | +50.000 | +48.000 | +25 | +40 | Healthy | keine |
| 4 s | +50.000 | +48.000 | +25 | +40 | Healthy | keine |
| 5 s | +50.000 | +48.000 | +25 | +40 | Healthy | keine |
| 6 s | +50.000 | +48.000 | +25 | +40 | Healthy | keine |
| 7 s | +50.000 | +48.000 | +0 | +40 | Suspect | StateChanged, SetSampleRate 2 Hz, RequestSnapshot |
| 8 s | +50.000 | +48.000 | +0 | +40 | Suspect | keine |
| 9 s | +50.000 | +48.000 | +0 | +40 | Suspect | RequestSnapshot |
| 10 s | +50.000 | +48.000 | +0 | +40 | Suspect | keine |
| 11 s | +50.000 | +48.000 | +0 | +40 | Recovering | StateChanged, FaultDeclared VideoFreeze, Alter 4000 ms |
| 12 s | +50.000 | +48.000 | +0 | +40 | Recovering | keine |
Tick 1 erzeugt nichts, denn eine Messung ist noch kein Delta; sie wird zum Bezugswert. Tick 2 ist das erste ausgewertete Paar, der Fortschritt bei der Darstellung ist belegt, und die Maschine verlässt Starting. Sie wiederholt außerdem eine Abtastrate, die sie schon benutzte, was überflüssig und harmlos ist. Die Snapshot-Anfrage auf dieser Zeile ist der erste Durchlauf, und der Host hat sie mit echtem Inhalt beantwortet, 6 Prozent schwarze Pixel bei einer Standardabweichung von 41, der Durchlauf war damit beendet.
Die Ticks 3 bis 6 geben überhaupt nichts aus. Das ist der gewollte Dauerbetrieb: Ein gesunder Stream erzeugt keine Effekte, ein leeres Log ist also der Erfolgsfall.
Bei Tick 7 bleibt das Bild stehen. Die Marke für den Video-Stillstand wird mit 7 s gestempelt, die Maschine geht nach Suspect, der Sampler verdoppelt auf 2 Hz, und die Sonde wird scharfgeschaltet und fordert ein Bild an. Festgestellt wird nichts, denn eine Marke mit dem Alter null ist kein Urteil. Die Ticks 8 und 10 sind das laufende Fenster, und Tick 9 ist die Sonde, die in ihrem eigenen Takt erneut misst, zwei Sekunden nach der letzten Anfrage statt der eingestellten 1500 ms, denn sie löst bei der nächsten Nachricht nach dem Fälligkeitszeitpunkt aus. Bytes und Ton geraten dabei nie ins Stocken: Diese Sitzung sieht für jede Regel bis auf eine einwandfrei aus.
Tick 11 ist das Urteil. Die Marke für den Video-Stillstand erreicht vier Sekunden, der Demuxer steht nicht im Verdacht, der Ton lief während des Stillstands weiter und läuft immer noch, es ist also ein Video-Freeze und kein totaler, und das Anomalie-Alter, das an den Executor gemeldet wird, beträgt 4000 ms. Lassen Sie dasselbe Skript laufen, bei dem auch der Ton bei Tick 7 aufhört, und dieselbe Zeile stellt einen totalen Freeze fest. Lassen Sie es noch einmal laufen, wobei der Snapshot in einen Timeout läuft, statt Inhalt zu liefern, und das Urteil bleibt gleich, seine Begründung lautet aber video stalled while audio keeps playing (snapshot timeout observed) (Video steht, während der Ton weiterläuft, Snapshot-Timeout beobachtet): Der festgefahrene Ausgang hat bestätigt, entschieden hat er nicht.
Tick 12 gibt nichts aus, weil die Maschine übergeben hat. Die Playback-Suite um diesen Code herum umfasst 108 Tests, die zum Zeitpunkt dieses Textes alle bestehen.
Ein Urteil kauft fünf Sprossen, und eine davon ist nicht gebaut
Das Urteil ist eine Eingabe für eine Leiter, keine Antwort. Es gibt fünf Sprossen: ein Anstoß aus Pause und Play, den nur totale Freezes bekommen; ein vollständiges erneutes Anwenden derselben URL; ein Wechsel zum nächsten Kandidaten; ein Neuaufbau des Players selbst; und Aufgeben. Ein Fehler innerhalb von fünfzehn Sekunden nach dem Umschalten steigt beim Wechsel ein und nicht beim erneuten Anwenden, denn eine Quelle, die sich nie bewährt hat, hat keinen zweiten Versuch mit der URL verdient, die gerade gescheitert ist. Gereifte Sitzungen behalten das erneute Anwenden vorn, ein Schluckauf nach zehn Minuten kostet den Zuschauer also keinen Wechsel der Quelle. Es gibt genau einen Neustartversuch vor der Eskalation, denn die Daten aus dem Feld haben gezeigt, dass der zweite nie hilft.
Die Sprosse für den Neuaufbau ist im Moment eine ehrliche Attrappe. Sie hat keine Primitive im Host, sie ist in den ausgelieferten Einstellungen abgeschaltet, und die eine Bedingung, die zu ihr führt, ein Stopp, der in einen Timeout lief und die native Sitzung festgefahren zurückließ, beendet derzeit die Leiter im Vordergrund und landet stattdessen beim Aufgeben. Das ist vorsichtig und richtig, es ist zugleich eine Lücke, und der Code sagt das an der Stelle, an der es passiert.
Zwischen den Versuchen wächst die Wartezeit exponentiell, und der erste Versuch ist frei: Basis mal zwei hoch Versuchsindex minus eins, gedeckelt, dann in beide Richtungen um bis zu zwanzig Prozent gejittert. In der Praxis: sofort feuern, dann eine Sekunde, drei, sieben und so weiter bis zum Deckel. Der Jitter multipliziert nach dem Deckel und nicht davor, die echte Obergrenze liegt deshalb bei sechsunddreißig Sekunden und nicht bei den dreißig, die der Deckel nahelegt. Eine Kleinigkeit, aber es ist die Sorte Kleinigkeit, die um 3 Uhr morgens einen Graphen falsch aussehen lässt.
Die Pixel bleiben auf dem Rechner, der sie aufgenommen hat
Die Sonde schreibt zwei rotierende PNG-Dateien in den eigenen lokalen Datenordner der App und überschreibt sie an Ort und Stelle. Niemand lädt sie hoch, und niemand schickt sie irgendwohin; das Bild existiert lange genug, um auf zwei Zahlen und einen Hash zusammenzuschrumpfen, und dann überschreibt es das nächste. Fehlerurteile gehen in das lokale Log auf demselben Rechner, und ein Sender wird dort über die ersten sechzehn Hexzeichen eines SHA-256 bezeichnet, nie über eine URL.
Genau daran liegt es, dass ich Ihnen nicht sagen kann, wie häufig der Fall mit dem hellen eingefrorenen Bild ist. Die Formen der Telemetrie-Ereignisse sind geschrieben und typisiert, sie nennen in ihrem Doc-Kommentar einen gebündelten Uploader, und diesen Uploader gibt es nicht. Drei Record-Typen sind deklariert und werden nirgends im Code konstruiert. Jedes Urteil in diesem Artikel darüber, welche Fehlerbilder zählen, stammt aus einem einzigen Abnahmelauf im Feld, einem Prüfstand für eingespeiste Fehler und einer Testsuite, nicht aus einer Flotte. Das ist eine echte Grenze für die Sicherheit von allem, was oben steht.
Wenn Sie den Detektor lieber benutzen als über ihn lesen: Er ist in der Windows-App von My TV Player standardmäßig eingeschaltet, und Sie können sich ein kostenloses Konto anlegen, um ihn auszuprobieren. Wenn er seine Arbeit tut, sehen Sie ihn nicht: Das Bild wird vier Sekunden lang stocken und zurückkommen, und auf dem Schirm wird nichts aufgetaucht sein, was Ihnen sagt, dass je ein Urteil gefallen ist.
Was dieser Artikel gemessen hat37 Behauptungen, jeweils mit den Beweisen dahinter
| Anspruch | Beweise | Gezählt |
|---|---|---|
| Alles in diesem Artikel ist gegen libvlc 3.0.21 über LibVLCSharp 3.9.7 in der Windows-App gemessen. Die Statistik-API unterscheidet sich in libvlc 4.x, die Aussagen zu den Zählern gelten deshalb nur für 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. | Spezifikation | Nicht zutreffend |
| libvlc bleibt bei einem Freeze und bei einem schwarzen Feed unbegrenzt in Playing, und EncounteredError ist harten Fehlern beim Öffnen und beim Protokoll vorbehalten, feuert also für die beiden vorherrschenden Fehlerbilder im Livebetrieb so gut wie nie.docs/research/MTP-PlaybackWatchdog-Spec-v1.0.md:18 (within the cited 15-19 block), header checked, no confidentiality marking. | Spezifikation | Nicht zutreffend |
| Neun Fehlerklassen sind definiert, F1 bis F9, und acht davon können überhaupt je festgestellt werden. F8 ClockStall wird nirgends festgestellt und hat keinen eigenen Taxonomie-Code. | n = 9 | 22.08.2026 |
| Drei der neun Klassen hängen an Ereignissen der Engine. F3 ist der eigene Fehler der Engine, F2 wird aus EndReached oder einem stillen Stopped festgestellt, und F9 aus Buffering. Der Rest wird aus Zählern erschlossen. | n = 3 | 22.08.2026 |
| Die Fehlerkarte zeigte früher für jede Klasse denselben einen Satz, und jede Klasse bildet jetzt auf einen Taxonomie-Code ab, der außerdem davon abhängt, ob der Fehler vor oder nach dem ersten Bild eintrat. | n = 1 | 22.08.2026 |
| Eine Messung der Statistik besteht aus elf Feldern: einem monotonen Zeitstempel und zehn kumulativen Zählern. Bei acht davon wird die Differenz gebildet; bei LostPictures und LostAudioBuffers bildet keine Regel je eine. | n = 11 | 22.08.2026 |
| Nur Video hat einen Ersatzzähler. DecodedVideo springt ein, wenn DisplayedPictures unbrauchbar ist; DecodedAudio dient allein dem Nachweis, dass es eine Tonspur gibt, und keine Regel liest ihn je als Lebenszeichen. | n = 1 | 22.08.2026 |
| Die Urteile werden in fester Reihenfolge geprüft: Zugriff zuerst, Demuxer als Zweites, Darstellung zuletzt, und die Freeze-Regeln treten zurück, solange Demuxer-Hunger im Verdacht steht. | n = 1 | 22.08.2026 |
| Aktuelle Einstellung: 4 s ohne Bytes für F1, 6 s für F4, 4 s für F5 und F6, 15 s für eine Tafel mit niedriger Bildrate. | n = 1 | 22.08.2026 |
| Vier Marken, die auch null sein dürfen, halten den Zeitpunkt fest, an dem die jeweilige Bedingung begann, und ein guter Tick löscht sie. Ein fünftes Signal, der Ausschlag bei Korruption und Diskontinuitäten, arbeitet andersherum: Es speichert einen Ablaufzeitpunkt und verkürzt die Freeze-Fenster, solange es scharf ist. | n = 4 | 22.08.2026 |
| Das Zählermodell ist auf rohem MPEG-TS kalibriert. Die Art der Quelle wird als „ts“ oder „hls“ mitgeführt, für Telemetrie und für künftige Schwellen je Art, und heute ist keine Schwelle in den Optionen HLS-spezifisch. | n = 1 | 22.08.2026 |
| Die Byte-Zähler von libvlc 3.x sind 32 Bit breit und gelten je Media, sie beginnen bei einem erneuten Öffnen also wieder bei null und laufen in langen Sitzungen über. So oder so wird das Delta negativ.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. | Spezifikation | Nicht zutreffend |
| Ein negatives Delta bei einem der sechs Zähler, die als Lebenszeichen gelten, setzt den Bezugswert zurück und überspringt die Auswertung für diesen Tick. Die Deltas für beschädigte Einheiten und für Diskontinuitäten gehören nicht zu dieser Absicherung. | n = 6 | 22.08.2026 |
| Der Bildzähler wird über ein Fenster von 30 s hinweg auf Trusted, FallbackDecodedVideo oder Untrusted festgelegt, verteilt auf fünf Zweige, und ein Freeze-Urteil setzt außerdem voraus, dass ein Videozähler in dieser Sitzung mindestens einmal weitergelaufen ist. | n = 5 | 22.08.2026 |
| Die Wege, die zu wenige angezeigte Bilder melden, sind vout-Wege und Wege der Hardware-Dekodierung, keine Access-Module.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. | Spezifikation | Nicht zutreffend |
| 45 Ticks im Sekundentakt mit laufendem Ton, laufender Uhr und vorhandenem Videoausgang, aber jedem Videozähler tot, stellen keinen Fehler fest und landen auf Untrusted. | n = 45 | 22.08.2026 |
| Eine Tafel, gemessen mit etwa einem halben Bild pro Sekunde, verbreitert das Freeze-Fenster auf 15 s, statt es zu verkürzen, und ein vollständiger Stillstand der Tafel wird auch auf dem verbreiterten Fenster noch erkannt. | n = 1 | 22.08.2026 |
| Ein Stillstand beim Ton neben gesundem Video konnte Verdacht wecken, aber nie zu einem Urteil führen, eine Sitzung saß deshalb ihr ganzes Leben lang mit der schnelleren Abtastrate in SUSPECT, gab nie das Stabilitätssignal ab und ließ das Konto der Wiederherstellungsversuche ungelöscht, sodass spätere Fehler eine Leiter mit schon verbrauchten Versuchen erbten und direkt auf der Karte landeten, die das Aufgeben meldet. | n = 1 | 22.08.2026 |
| Eine Pause durch den Nutzer verdeckt eingefrorene Zähler unbegrenzt; ein Sprung verdeckt sie nur 5 s lang; Puffern setzt die Urteile über die Darstellung aus, während die Regel für null Bytes weiterläuft. | n = 3 | 22.08.2026 |
| Aktuelle Einstellung für Schwarz: Ein Pixel gilt bei Luma 24 oder darunter als schwarz, eine Probe gilt bei 98 Prozent schwarzen Pixeln mit einer Standardabweichung der Luma von 4.0 oder weniger als schwarz, und das Urteil braucht 4 Proben hintereinander, aufgenommen mit 96 mal 54. | n = 1 | 22.08.2026 |
| In den 10 s, nachdem sich ein Wiederherstellungsversuch bestätigt hat, sinkt die geforderte Zahl schwarzer Proben hintereinander auf 2. | n = 1 | 22.08.2026 |
| 1500 ms sind eine Untergrenze für das erneute Scharfschalten, kein Abstand. Die Sonde löst bei der nächsten Nachricht aus, die zum Fälligkeitszeitpunkt oder danach eintrifft, der tatsächliche Abstand ist deshalb länger: 2 s dort, wo Nachrichten mit 1 Hz eintreffen, wie in dem hier festgehaltenen Lauf. | n = 1 | 22.08.2026 |
| Eine Nachtszene scheitert an beiden Achsen für Schwarz, und Inhalt mit Letterbox-Balken kommt auf weniger als halb schwarz; beides ist per Test als nicht schwarz festgenagelt. | n = 2 | 22.08.2026 |
| Die Sonde läuft als einmaliger Durchlauf, direkt nachdem der Stream zum ersten Mal darstellt, und noch einmal beim Übergang nach SUSPECT, wenn ein Video-Stillstand den Verdacht ausgelöst hat. Sobald der erste Durchlauf beendet ist, läuft sie im gesunden Dauerbetrieb nie wieder, ein schwarzes Bild mitten in der Wiedergabe erzeugt auf einem ansonsten gesunden Stream also kein Urteil. | n = 2 | 22.08.2026 |
| Die Sonde nimmt über den eigenen Snapshot-Aufruf der Engine auf, und ein Snapshot, der von einer hardwaredekodierten Fläche zurückgelesen wird, ist eine bekannte Stelle, an der ein Player ein leeres Bild liefert, aus Gründen, die mit dem Inhalt nichts zu tun haben. Den gescheiterten Snapshot behandeln wir als bestätigendes Indiz; wie oft ein erfolgreiches Zurücklesen fälschlich schwarz zurückkommt, haben wir nicht gemessen.PlaybackEngine.Watchdog.cs:253 calls MediaPlayer.TakeSnapshot (libvlc_video_take_snapshot); the timeout path is handled at WatchdogStateMachine.cs:867-873. | Spezifikation | Nicht zutreffend |
| Bei jeder Probe der Sonde werden ein 64-Bit-Durchschnitts-Hash und eine mittlere Luma berechnet, beide reisen in der Nachricht mit, und die Regel-Engine liest keines von beidem. | n = 1 | 22.08.2026 |
| Ein flaches Bild ergibt einen Durchschnitts-Hash, in dem jedes Bit gesetzt ist, weil jede Zelle genau auf dem globalen Mittel liegt und der Vergleich größer-oder-gleich lautet. Reines Weiß und reines Schwarz sind für diesen Hash deshalb nicht zu unterscheiden. | n = 1 | 22.08.2026 |
| Ein Use-after-free innerhalb von libvlc zeigt sich als native Zugriffsverletzung, nicht als CLR-Exception, ein verwaltetes catch läuft deshalb nie.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. | Spezifikation | Nicht zutreffend |
| Ein zwischengespeicherter Media-Wrapper von LibVLCSharp hält eine eigene native Referenz, nach einem Wechsel der Media auf einem Pool-Thread liefert er deshalb weiter die eingefrorenen Zähler der toten Media, ohne je zu werfen.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. | Spezifikation | Nicht zutreffend |
| Ein zwischengespeicherter Media-Wrapper brachte den Sampler dazu, auf einem gesunden Stream die eingefrorenen Zähler einer toten Media zu melden, gelesen als Stillstand im Netz. Live beobachtet während eines Abnahmelaufs im Feld am 2026-07-24 um 23:24. Behoben, indem der Wrapper bei jedem Tick geholt und wieder freigegeben wird. | n = 1 | 24.07.2026 |
| Zwölf gescriptete Ticks im Sekundentakt erreichen HEALTHY bei 2 s, SUSPECT bei 7 s und stellen bei 11 s F5 VideoFreeze mit einem Anomalie-Alter von 4000 ms fest. Dasselbe Skript, bei dem auch der Audiozähler stehen bleibt, stellt im selben Tick F6 TotalFreeze fest, und ein dritter Lauf, dessen Snapshots in SUSPECT in einen Timeout laufen, stellt F5 fest, mit der Begründung erweitert zu "video stalled while audio keeps playing (snapshot timeout observed)". | n = 12 | 22.08.2026 |
| Die Testsuite Playback läuft mit 108 Tests und 0 Fehlschlägen. | n = 108 | 22.08.2026 |
| Fünf Sprossen: ein Anstoß aus Pause und Play, nur bei totalen Freezes, ein vollständiges erneutes Anwenden, ein Wechsel zum nächsten Kandidaten, ein Neuaufbau des Players, dann Aufgeben. Ein einziger Neustartversuch, bevor eskaliert wird, und ein Fehler innerhalb von 15 s nach dem Umschalten steigt beim Wechsel ein statt beim erneuten Anwenden. | n = 5 | 22.08.2026 |
| Die Sprosse für den Neuaufbau des Players hat noch keine Primitive im Host und ist in den ausgelieferten Einstellungen abgeschaltet, ein Vorfall mit Timeout beim Stoppen beendet die Leiter im Vordergrund derzeit also und geht zum Aufgeben über. | n = 1 | 22.08.2026 |
| Versuch k wartet min(Basis mal (2^k - 1), Deckel) mit symmetrischem Jitter, der erste Versuch feuert also sofort, und ab dem zweiten wächst es auf 1 s, 3 s, 7 s bis zum Deckel. Der Jitter kommt nach dem Deckel dazu, gedeckelte Wartezeiten streuen deshalb symmetrisch um 30 s, und die echte Obergrenze liegt bei 36 s. | n = 1 | 22.08.2026 |
| Die Sonde schreibt zwei rotierende PNG-Dateien in den eigenen lokalen Datenordner der App und überschreibt sie. Fehlerereignisse gehen in das lokale Log, in dem ein Sender über die ersten 16 Hexzeichen eines SHA-256 bezeichnet wird und nie über eine URL. | n = 1 | 22.08.2026 |
| Die Record-Typen für die Telemetrie sind deklariert und werden nie konstruiert, und den gebündelten Uploader, den ihr Doc-Kommentar nennt, gibt es nicht. | n = 3 | 22.08.2026 |