Картинка замирает, звук идёт дальше: как поймать остановку, которую libvlc по-прежнему называет Playing
Замершая картинка это не ошибка. Движок продолжает сообщать Playing, и меняется ровно одно: дельта счётчика уходит в ноль.
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
Два соседних односекундных отсчёта. Изменилось одно число. Байты по-прежнему приходят с той же скоростью, демультиплексор по-прежнему их разбирает, аудиобуферы по-прежнему доходят до звуковой карты, а движок в обеих строках сообщает Playing и будет сообщать это ровно столько, сколько вы держите канал открытым. На экране висит последний кадр, добравшийся до видеовыхода. Это константы, которыми наш тестовый стенд кормит детектор, а не запись с настоящего канала: поток на 4 Мбит/с вычитывает около 500000 байт в секунду, на порядок больше этих констант, а его числа чтения и демультиплексирования держатся куда ближе друг к другу, чем устойчивый разрыв в 4 процента. Переносится форма, а не числа. Ошибки в этой паре нет, никакого события ошибки за ней не последует, и эти четыре дельты и есть весь корпус улик, который получает детектор.
Всё, что ниже, измерено на libvlc 3.0.21 через LibVLCSharp 3.9.7 в нашем настольном приложении для Windows. API статистики между мажорными версиями libvlc переехал, поэтому утверждения про счётчики читайте как утверждения про 3.x.
libvlc сообщает Playing и при замершей картинке
Состояние движка отвечает на более узкий вопрос, чем в него вчитывают. Playing означает, что поток воспроизведения не остановлен, не поставлен на паузу и не завершён. Оно ничего не говорит о том, дошёл ли кадр до экрана за последние четыре секунды, и ничего не говорит о том, было ли хоть что-нибудь на том кадре, который дошёл.
Спецификация нашего сторожевого механизма формулирует посылку, на которой стоит вся подсистема: libvlc остаётся в Playing сколь угодно долго и при замирании, и при чёрном фиде, а EncounteredError приберегается для жёстких отказов открытия и протокола, поэтому на двух режимах отказа, о которых зрители сообщают на самом деле, он почти никогда не срабатывает. Эта фраза наше собственное прочтение движка, а не строчка, взятая из его документации, и именно поэтому ни одно правило ниже не спрашивает у движка, как у него дела.
Наблюдаемое следствие это размер таксономии, которую нам пришлось построить. Классов определено девять: ноль байт на слое доступа, тихий конец потока на живом канале, собственная ошибка движка, байты приходят, а демультиплексор не выдаёт ничего, видео встало при живом звуке, встало и то и другое, а данные идут, чёрные кадры на здоровом конвейере, остановившиеся входные часы и повторная перебуферизация внутри скользящего окна.
Три из этих девяти едут на событиях движка. Одно это ошибка самого движка. Два других обычные события, которые ошибками не являются вовсе: конец потока или тихая остановка на канале, который никто не останавливал, и отчёт уровня кеша, который мы засчитываем в цикл перебуферизации. Шесть выводятся из счётчиков, хотя остановившиеся часы среди них опираются ещё и на событие часов от движка. А девять это преувеличение того, что система способна сказать, потому что класс остановившихся часов не объявляется нигде. Он существует, чтобы подтверждать, и своего кода таксономии не имеет.
Карточка сбоя раньше выбрасывала всё это и показывала одну и ту же фразу на любой случай, так что зритель, чей источник отказал в соединении, и зритель, чьё устройство не смогло декодировать видео, читали одинаковые слова. Теперь каждый класс отображается в код, и один и тот же класс значит разное по разные стороны от первого кадра, поэтому отображение несёт и то и другое. Ноль байт внутри льготного окна подключения это поток, который так и не начался. Точно такая же остановка через десять минут это поток, который прекратился.
Обнаружение замирания видео это четыре дельты счётчиков в фиксированном порядке
Один отсчёт это одиннадцать полей: одна монотонная метка времени и десять накопительных счётчиков, снятых с движка за один проход. Восемь из десяти считаются как разность с предыдущим отсчётом. Два оставшихся, брошенные опоздавшие кадры и брошенные аудиобуферы, не вычитаются нигде: их собрали на случай, если какому-нибудь правилу они понадобятся, и ни одно правило их так и не запросило. Вердикты несут четыре дельты:
- Прочитано байт, на слое доступа. Ноль значит, что из сети ничего не приходит.
- Байты, съеденные демультиплексором. Ненулевое чтение при нулевом демультиплексировании значит, что приходят байты, в которых нет программы.
- Показано кадров, на видеовыходе. Это главный признак активности видео, и следующий раздел о том, почему верить ему на слово нельзя.
- Проиграно аудиобуферов, на звуковом выходе. Именно это отделяет замирание видео от полного.
Декодированное видео это единственный вспомогательный счётчик, у которого есть полномочия: он подменяет счётчик показанных кадров, когда тот оказывается непригоден. У декодированного звука такой роли нет. Его читают один раз, чтобы доказать, что аудиопоток существует. Оставшаяся пара это повреждённые единицы и разрывы, и всплеск любого из них на время взводит укороченные окна замирания.
Порядок важнее любого отдельно взятого порога, и код объясняет почему прямо на месте:
// 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.
(Замирания показа F5 и F6 подавляются, пока подозревается голодание демультиплексора: голодание тоже останавливает показ, а классификацию забирает себе более длинное окно F4, потому что вход в лестницу у него другой. F1 в такой защите не нуждается: он делит короткое окно и проверяется первым.)
Доступ проверяется первым, демультиплексор вторым, показ последним. Мёртвый слой доступа морит голодом всё, что ниже по течению, поэтому картинка, вставшая через четыре секунды после того, как кончились байты, это симптом, а не диагноз, и если сообщить о ней как о замирании, лестница восстановления зайдёт не с той ступени. Сейчас окна у нас такие: четыре секунды нулевых байт, шесть на голодающем демультиплексоре, четыре на остановившемся показе. Это настроечные значения, и они сдвинутся, как только их кто-нибудь снова померяет; смысл несёт порядок.
Подозрение хранится моментом времени, а не флагом. Четыре метки хранят каждая свою отметку времени, когда началось её условие, и вердикт это метка, чей возраст перешагнул своё окно. Один здоровый тик возвращает метку в null, и возраста больше нет, так что окно это вычитание, а не бухгалтерия на каждом тике. Всплеск повреждений это исключение, которое подтверждает форму: он хранит момент истечения вместо момента начала, ничто не снимает его досрочно, и ни один вердикт не читает его возраст.
Одна оговорка о границах, пока пороги не начали выглядеть всеобщими. Всё это откалибровано на сыром MPEG-TS. Вид источника мы переносим как ts или hls, но только ради телеметрии и ради порогов под каждый вид, которых пока никто не написал. На HLS адаптивный демультиплексор сам ходит за данными ниже по течению, поэтому разделение верхнего уровня на доступ и демультиплексирование значит не то, что говорит этот раздел, и ни один выпущенный нами порог не заточен под HLS.
Почему счётчик кадров говорит, что здоровый канал всё время замирает?
Некоторые пути видеовыхода и аппаратного декодирования недоучитывают показанные кадры, и спросить, на каком из них вы оказались, не у кого. Наш собственный комментарий в коде винит в этом модуль доступа, и это не может быть правдой: слой доступа стоит выше демультиплексирования и декодирования и вообще не трогает счётчик, который увеличивает ядро видеовыхода. Комментарий неверен, а поведение, которое он описывает, настоящее.
Поэтому первые тридцать секунд после того, как поток начал показывать картинку, автомат смотрит, а не судит. В конце окна он занимает одну из трёх позиций по пяти веткам: счётчик показанных кадров двигался, и ему верят; счётчик показанных мёртв, но двигалось декодированное видео, и оно становится запасным признаком; или все видеосчётчики мертвы, при этом входные часы идут, а видеовыход есть, значит путь недоучитывает, и правила замирания на эту сессию выключаются. Четвёртая ветка ловит канал с неправильной пометкой, где со стороны видео не двинулось ничто и видеовыход не появился, зато звук играет, и признак активности берётся со звука. Пятая по умолчанию счётчику верит.
Надо всем этим стоит одна защита. Никакой вердикт замирания невозможен, пока хотя бы один видеосчётчик не сдвинулся за эту сессию, потому что счётчик, который не двигался никогда, неотличим от счётчика, который недоучитывает, а случай «мертво с самого начала» принадлежит классификатору запуска, у которого другие улики и другой вход в лестницу. Сорок пять односекундных тиков, где звук идёт, часы двигаются, видеовыход есть, а все видеосчётчики стоят, не объявляют ничего.
Краевой случай, который и слепил калибровку, это заставка с низкой частотой кадров: карточка с логотипом станции или картинка радиоканала с частотой около полукадра в секунду. На односекундном сборщике такой канал каждый второй тик стоит на нуле показанных кадров, то есть даёт в точности подпись замирания. Калибровка определяет частоту и расширяет окно замирания с четырёх секунд до пятнадцати, и расширение здесь как раз та половина, что идёт против интуиции: инстинкт велит судить быстрее на канале, которому меньше что показывать, а это даёт ложный вердикт на каждой вашей заставке. Полная остановка заставки всё равно ловится, по расширенному окну.
У взведённого подозрения, которое ни одно правило не может потребить, есть цена, и мы её заплатили. Остановка звука рядом со здоровым видео раньше поднимала подозрение на видеоканале, где прочитать его не может ни один вердикт, поэтому сессия сидела в этом состоянии всю свою жизнь: снимала отсчёты вдвое чаще и ни разу не выдавала сигнал стабильности, который очищает реестр восстановления. Каждый следующий сбой в этой сессии наследовал лестницу с уже израсходованными попытками и сразу выходил на карточку «мы сдались». Подозрение, которое не может стать вердиктом, это не осторожность. Это течь.
Пауза выглядит в точности как замирание
На паузе, поставленной пользователем, в отсчёте останавливаются все счётчики, ровно тем узором, какой даёт полное замирание.
Отличить одно от другого по числам невозможно, поэтому паузу вообще не ограничивают по времени: она маскирует вердикты сколь угодно долго, и десять минут замерших счётчиков за паузой не объявляют ничего. Перемотке вместо этого достаётся ограниченная маска в пять секунд, потому что перемотка либо завершается, либо нет. Буферизация приостанавливает вердикты показа, но оставляет правило доступа взведённым, исходя из того, что ноль байт во время буферизации это мёртвый источник, а не медленный канал связи: на медленном канале байты всё-таки видно.
Второе место, где кусается арифметика, это повторные открытия. Счётчики байт 32-битные и живут внутри одного медиа, поэтому при подаче нового медиа они начинаются с нуля, а на достаточно длинной сессии переполняются. И в том и в другом случае дельта возвращается отрицательной, и отрицательная дельта на любом из шести счётчиков активности сбрасывает базовый отсчёт и пропускает оценку на этом тике, вместо того чтобы сообщить об остановке всего конвейера.
Обнаружение чёрного кадра обязано смотреть на пиксели
Чёрный фид декодируется, показывается, играет звук и тянет байты с обычной скоростью. Каждое правило, о котором шла речь выше, видит здоровый поток. Это единственный путь в детекторе, который обязан смотреть на пиксели, и ему нужен настоящий кадр, снятый с видеовыхода в 96 на 54.
Одного порога яркости мало, и причина в содержимом. Вердикт берёт две оси и повторяемость:
- Доля чёрных пикселей 0.98 и выше, где пиксель считается чёрным при яркости 24 и ниже.
- Стандартное отклонение яркости 4.0 и ниже, и именно это щадит тёмную, но структурную сцену.
- Четыре чёрных отсчёта подряд, и вот этого затемнение выполнить не может.
Оба провала по содержимому зафиксированы тестами, а не доказаны словами. Ночная сцена, почти целиком близкая к чёрному, с редкой структурой в лунном свете, проваливается сразу по обеим осям. Картинка с чёрными полосами выходит чёрной меньше чем наполовину, потому что полосы это меньшая часть кадра даже на широком формате.
У довода про затемнение есть ветка, которую я пропускать не стану. В течение десяти секунд после того, как восстановление подтвердилось, требуемая серия падает с четырёх отсчётов до двух, это около трёх секунд по настроенной нижней границе и дольше на практике, а столько медленное затемнение продержаться может. Это осознанный размен: улики, которые только что осудили источник, никуда не делись, поэтому источник, вернувшийся сломанным, осуждается быстрее, а платим мы за это защитой от затемнения. Интервал 1500 мс тоже нижняя граница, а не шаг. Пробник по нему перевзводится и срабатывает на первом же пришедшем сообщении, поэтому фактический шаг выходит длиннее: семь секунд, а потом девять, в прогоне ниже.
Когда именно работает пробник, это решение с видимой ценой, и честное описание уже, чем обещает название. Он проходит один раз, сразу после того, как поток впервые дал картинку, и ещё раз на переходе в подозрение, когда подозрение подняла остановка видео. Как только начальный проход закрыт, пока сессия остаётся здоровой, он больше не запускается. Поэтому чернота посреди воспроизведения на потоке, здоровом во всём остальном, никакого вердикта не даёт, воспроизводится это стабильно, и чинить мы это не стали: ловить её значит платить снимком за каждую сессию каждого потока, чтобы обнаружить то, что смотрящий человек видит мгновенно, а если переключиться на другой канал и вернуться, проход всё равно повторится.
Второе ограничение, которое я не могу закрыть уликами. Кадр снимается через собственный вызов снимка у движка, а обратное чтение с поверхности аппаратного декодирования это хорошо известное место, где плеер отдаёт пустой кадр по причинам, никак не связанным с тем, что на экране. Снимок, который так и не пришёл, мы обрабатываем: это улика заклинившего видеовыхода, она подтверждает вердикт по счётчикам, но никогда не объявляет сама. А снимок, который пришёл и ошибочно оказался чёрным, мы не обрабатываем никак, и насколько часто такое случается на нашем пути, я не измерял.
Меньше всего мне нравится то, что пробник измеряет, а потом выбрасывает. На каждом отсчёте считаются 64-битный средний хеш плоскости яркости и средняя яркость, оба едут в сообщении, и движок правил не читает ни того, ни другого. Поток, замерший на ярком статичном кадре, на канале, чей счётчик кадров откалибровался как ненадёжный, это как раз то, что поймали бы два одинаковых хеша на разнесённых отсчётах. Мы их не сравниваем, а насколько такой случай част, чтобы правило окупилось, ответить мы не можем, и причина в двух последних разделах. Одна деталь этого хеша удивила меня достаточно, чтобы её сохранить: ровный кадр хешируется в одни единицы, все 64 бита выставлены, потому что среднее каждой ячейки сидит ровно на общем среднем, а сравнение идёт «больше или равно». Сплошной белый и сплошной чёрный дают один и тот же хеш.
Самый скверный баг здесь это замирание, которого не было
Чтение статистики плеера это небезопасный вызов. Каждая его строчка разыменовывает нативные дескрипторы, которые движок освобождает, когда пользователь переключает канал, уходит со страницы или пересборка цепочки буферов отправляет плеер на покой, а сборщик работает в собственной задаче. Проигрыш в этой гонке не поднимает ничего такого, что мог бы впитать catch: libvlc падает с нарушением доступа, и процесс исчезает, не оставив в логе ни строки. Поэтому сборщик работает за тем же шлюзом, которым пользуется пул плееров.
Внутри этого шлюза решение о кешировании, которое по отдельности выглядит правильным, породило самый скверный баг, какой эта подсистема успела выпустить. Сборщик держал обёртку медиа у себя вместо того, чтобы брать её на каждом тике, потому что брать её раз в секунду казалось расточительством. Обёртка медиа в LibVLCSharp берёт собственную нативную ссылку, поэтому закешированная обёртка осталась годной и после того, как движок в потоке из пула подменил медиа у неё под ногами. Она продолжала отвечать. Она ни разу не бросила исключение. Она возвращала счётчики мёртвого медиа, а они не двигаются, а это по любому правилу автомата полная остановка байтов.
Симптом: здоровый канал, о котором сообщали как о мёртвой сети. Цена: уход с работающего потока на другой источник, увиденный вживую в полевом приёмочном прогоне 2026-07-24 в 23:24. Комментарий, который теперь стоит над исправлением, говорит это прямо:
// 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.
(Брать и освобождать обёртку на каждом тике: закешированная обёртка держит собственную нативную ссылку, поэтому после подмены медиа в потоке из пула она продолжала бы отдавать замершие счётчики мёртвого медиа и ни разу не бросила бы исключение, а это читается как остановка сети на здоровом потоке; одно удержание и одно освобождение в секунду это пренебрежимо мало.)
Детектор, читающий несвежий вход, сбоит не тихо. Он выдаёт тот же вердикт, с той же уверенностью, про канал, который играет безупречно, и дальше по этому вердикту действует. Любой другой сбой сторожевого механизма стоит вам остановки, которую вы не поймали. Этот стоит вам остановки, которую вы выдумали, и зритель видит восстановление.
Вот что двенадцать секунд замирания выдают на самом деле
Ниже сценарная сессия, прогнанная через выпущенный движок правил на существующем тестовом стенде, двенадцать односекундных тиков. Её выполнили, а не расписали на бумаге, и драйвер после прогона удалили. Числа читайте как значения стенда. На каждый запрос снимка ответ приходил через 300 мс, как ответил бы настоящий хост, потому что бюджет снимка одна секунда.
Сессия открывается, сообщает Playing и сообщает об одном видеовыходе. Дальше:
| t | dRead | dDemux | dDisplayed | dAudio | состояние после | выданные эффекты |
|---|---|---|---|---|---|---|
| 1 с | +50000 | +48000 | +25 | +40 | Starting | нет |
| 2 с | +50000 | +48000 | +25 | +40 | Healthy | StateChanged, SetSampleRate 1 Гц, RequestSnapshot |
| 3 с | +50000 | +48000 | +25 | +40 | Healthy | нет |
| 4 с | +50000 | +48000 | +25 | +40 | Healthy | нет |
| 5 с | +50000 | +48000 | +25 | +40 | Healthy | нет |
| 6 с | +50000 | +48000 | +25 | +40 | Healthy | нет |
| 7 с | +50000 | +48000 | +0 | +40 | Suspect | StateChanged, SetSampleRate 2 Гц, RequestSnapshot |
| 8 с | +50000 | +48000 | +0 | +40 | Suspect | нет |
| 9 с | +50000 | +48000 | +0 | +40 | Suspect | RequestSnapshot |
| 10 с | +50000 | +48000 | +0 | +40 | Suspect | нет |
| 11 с | +50000 | +48000 | +0 | +40 | Recovering | StateChanged, FaultDeclared VideoFreeze, возраст 4000 мс |
| 12 с | +50000 | +48000 | +0 | +40 | Recovering | нет |
Тик 1 не даёт ничего, потому что один отсчёт это не дельта; он становится базовым. Тик 2 это первая оценённая пара, продвижение показа доказано, и автомат уходит из Starting. Заодно он повторно объявляет ту частоту отсчётов, которой уже пользовался, и это избыточно и безвредно. Запрос снимка на этой строке и есть начальный проход, а хост ответил на него настоящим содержимым, 6 процентов чёрных пикселей при стандартном отклонении 41, поэтому проход закрылся.
Тики с 3 по 6 не выдают вообще ничего. Это и есть задуманный установившийся режим: здоровый поток не порождает эффектов, так что пустой лог это случай успеха.
Тик 7 это место, где встаёт картинка. Метка остановки видео проставляется на 7 с, автомат переходит в Suspect, сборщик удваивает частоту до 2 Гц, пробник взводится и просит кадр. Не объявляется ничего, потому что метка нулевого возраста это не вердикт. Тики 8 и 10 это работающее окно, а тик 9 это пробник, снимающий заново в своём ритме, через две секунды после прошлого запроса, а не через заданные 1500 мс, потому что срабатывает он на первом сообщении после назначенного момента. Байты и звук за всё это время ни разу не сбились: любому правилу, кроме одного, эта сессия кажется безупречной.
Тик 11 это вердикт. Метка остановки видео дотягивает до четырёх секунд, демультиплексор под подозрением не находится, звук за время остановки двигался и двигается до сих пор, значит это замирание видео, а не полное, и возраст аномалии, сообщённый исполнителю, равен 4000 мс. Прогоните тот же сценарий, добавив остановку звука на тике 7, и та же строка объявит полное замирание. Прогоните ещё раз, чтобы снимок уходил в таймаут вместо того, чтобы вернуть содержимое, и вердикт не изменится, но строка его причины станет video stalled while audio keeps playing (snapshot timeout observed): заклинивший вывод подтвердил, но не решил.
Тик 12 не выдаёт ничего, потому что автомат уже передал управление. Набор тестов Playback вокруг этого кода это 108 тестов, и на момент написания проходят все.
Вердикт покупает пять ступеней, и одна из них не построена
Вердикт это вход в лестницу, а не ответ. Ступеней пять: пинок паузой и воспроизведением, который достаётся только полным замираниям; полное повторное применение того же адреса; переход на следующего кандидата; пересборка самого плеера; и сдаться. Сбой в первые пятнадцать секунд после переключения на канал заходит сразу на переходе, а не на повторном применении, потому что источник, который так себя и не проявил, не заслуживает повторной попытки по адресу, который только что отказал. Зрелые сессии оставляют повторное применение первым, чтобы заминка через десять минут не стоила зрителю смены источника. Попытка перезапуска ровно одна, дальше эскалация, потому что полевые данные показали: вторая не помогает никогда.
Ступень пересборки сейчас честная выдумка. Примитива на стороне хоста у неё нет, в выпущенных настройках она отключена, а единственное условие, которое на неё ведёт, остановка, ушедшая в таймаут и оставившая нативную сессию заклинившей, сейчас обрывает лестницу на переднем плане и приземляется на «сдаться». Это осторожно и правильно, это ещё и пробел, и код говорит об этом прямо там, где это происходит.
Между попытками выдержка растёт экспоненциально, и первая попытка бесплатна: база, умноженная на два в степени номера попытки минус один, обрезанная потолком, а потом сдвинутая джиттером на величину до двадцати процентов в любую сторону. На практике: сработать сразу, потом одна секунда, три, семь и так далее до потолка. Джиттер умножается после потолка, а не до него, поэтому настоящий предел это тридцать шесть секунд, а не тридцать, которые обещает потолок. Мелочь, но ровно та мелочь, из-за которой график выглядит неправильным в 3 часа ночи.
Пиксели остаются на той машине, которая их сняла
Пробник пишет два PNG-файла по кругу в собственную локальную папку данных приложения и перезаписывает их на месте. Ничто их не выгружает и ничто никуда их не отправляет; кадр живёт ровно столько, чтобы свестись к двум числам и хешу, а потом его перезаписывает следующий. Вердикты по сбоям уходят в локальный лог на той же машине, и канал в нём опознаётся по первым шестнадцати шестнадцатеричным символам SHA-256, никогда по адресу.
Именно из-за последней части я не могу сказать вам, насколько част случай с ярким замершим кадром. Формы событий телеметрии написаны и типизированы, в их doc-комментарии назван пакетный отправщик, и этого отправщика не существует. Три типа записей объявлены и нигде в коде не создаются. Всякое суждение в этой статье о том, какие режимы отказа важны, взято из одного полевого приёмочного прогона, стенда для внесения неисправностей и набора тестов, а не с парка устройств. Это настоящее ограничение на уверенность во всём, что выше.
Если вам ближе пользоваться детектором, чем читать о нём: в приложении My TV Player для Windows он включён по умолчанию, а чтобы попробовать, можно завести бесплатный аккаунт. Когда он делает свою работу, вы его не увидите: картинка дёрнется на четыре секунды и вернётся, и на экране не появится ничего, что сказало бы вам, что вердикт вообще был вынесен.
Что измеряется в этой статье37 заявлений, каждое из которых имеет под собой доказательства
| Претензия | Доказательства | Подсчитано |
|---|---|---|
| Всё в этой статье измерено на libvlc 3.0.21 через LibVLCSharp 3.9.7 на головном приложении для Windows. В libvlc 4.x API статистики другой, поэтому утверждения про счётчики относятся только к 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. | Спецификация | Не применимо |
| libvlc остаётся в Playing сколь угодно долго и при замирании, и при чёрном фиде, а EncounteredError приберегается для жёстких отказов открытия и протокола, поэтому на двух главных режимах отказа живого эфира он почти никогда не срабатывает.docs/research/MTP-PlaybackWatchdog-Spec-v1.0.md:18 (within the cited 15-19 block), header checked, no confidentiality marking. | Спецификация | Не применимо |
| Определено девять классов сбоя, от F1 до F9, и объявлен когда-либо может быть только восемь из них. F8 ClockStall не объявляется нигде и своего кода таксономии не имеет. | n = 9 | 22 авг. 2026 г. |
| Три класса из девяти едут на событиях движка. F3 это собственная ошибка движка, F2 объявляется по EndReached или тихому Stopped, а F9 объявляется по Buffering. Остальные выводятся из счётчиков. | n = 3 | 22 авг. 2026 г. |
| Карточка сбоя раньше показывала одну и ту же фразу для каждого класса, а теперь каждый класс отображается в код таксономии, который вдобавок зависит от того, случился сбой до первого кадра или после. | n = 1 | 22 авг. 2026 г. |
| Один отсчёт статистики это одиннадцать полей: одна монотонная метка времени и десять накопительных счётчиков. По восьми считается разность; LostPictures и LostAudioBuffers не участвуют в разности ни в одном правиле. | n = 11 | 22 авг. 2026 г. |
| Запасной счётчик есть только у видео. DecodedVideo подменяет DisplayedPictures, когда тот непригоден; DecodedAudio используется только чтобы доказать, что аудиопоток существует, и ни одно правило не читает его как признак активности. | n = 1 | 22 авг. 2026 г. |
| Вердикты проверяются в таком порядке: доступ первым, демультиплексор вторым, показ последним, и правила замирания отступают, пока подозревается голодание демультиплексора. | n = 1 | 22 авг. 2026 г. |
| Текущая настройка: 4 с нулевых байт для F1, 6 с для F4, 4 с для F5 и F6, 15 с для заставки с низкой частотой кадров. | n = 1 | 22 авг. 2026 г. |
| Четыре обнуляемые метки хранят момент, когда началось каждое из условий, и один здоровый тик их снимает. Пятый сигнал, всплеск повреждений и разрывов, работает наоборот: он хранит момент истечения и, пока взведён, укорачивает окна замирания. | n = 4 | 22 авг. 2026 г. |
| Модель счётчиков откалибрована на сыром MPEG-TS. Вид источника переносится как «ts» или «hls» ради телеметрии и будущих порогов под каждый вид, и сегодня ни один порог в опциях не заточен под HLS. | n = 1 | 22 авг. 2026 г. |
| Счётчики байт в libvlc 3.x 32-битные и живут внутри одного медиа, поэтому при повторном открытии они начинаются с нуля, а на долгих сессиях переполняются. И в том и в другом случае дельта уходит в минус.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. | Спецификация | Не применимо |
| Отрицательная дельта на любом из шести счётчиков активности сбрасывает базовый отсчёт и пропускает оценку на этом тике. Дельты повреждённых единиц и разрывов в эту защиту не входят. | n = 6 | 22 авг. 2026 г. |
| Счётчик кадров калибруется на окне в 30 с и приводит к Trusted, FallbackDecodedVideo или Untrusted по пяти веткам, а вердикты замирания вдобавок требуют, чтобы хотя бы один раз за сессию видеосчётчик сдвинулся. | n = 5 | 22 авг. 2026 г. |
| Недоучитывают показанные кадры пути видеовыхода и аппаратного декодирования, а не модули доступа.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. | Спецификация | Не применимо |
| 45 односекундных тиков, где звук идёт, часы двигаются и видеовыход есть, но все видеосчётчики мертвы, не объявляют никакого сбоя и приводят к Untrusted. | n = 45 | 22 авг. 2026 г. |
| Заставка, измеренная примерно в полкадра в секунду, расширяет окно замирания до 15 с, а не сужает его, и полная остановка заставки по расширенному окну всё равно обнаруживается. | n = 1 | 22 авг. 2026 г. |
| Остановка звука рядом со здоровым видео могла поднять подозрение, но никогда не могла дойти до вердикта, поэтому сессия всю свою жизнь сидела в SUSPECT на учащённой частоте отсчётов, ни разу не выдавала сигнал стабильности и оставляла реестр восстановления неочищенным, так что более поздние сбои наследовали израсходованную лестницу и сразу выходили на карточку «мы сдались». | n = 1 | 22 авг. 2026 г. |
| Пауза, поставленная пользователем, маскирует замершие счётчики сколь угодно долго; перемотка маскирует их только 5 с; буферизация приостанавливает вердикты показа, а правило нулевых байт продолжает работать. | n = 3 | 22 авг. 2026 г. |
| Текущая настройка черноты: пиксель считается чёрным при яркости 24 и ниже, отсчёт считается чёрным при 98 процентах чёрных пикселей и стандартном отклонении яркости 4.0 и меньше, а вердикту нужны 4 отсчёта подряд, снятые в 96 на 54. | n = 1 | 22 авг. 2026 г. |
| В течение 10 с после того, как попытка восстановления подтвердилась, требуемое число чёрных отсчётов подряд падает до 2. | n = 1 | 22 авг. 2026 г. |
| 1500 мс это нижняя граница перевзвода, а не шаг. Пробник срабатывает на первом же сообщении, пришедшем в назначенный момент или позже, поэтому фактический шаг выходит длиннее: 2 с там, где сообщения приходят с частотой 1 Гц, как в том прогоне, что записан здесь. | n = 1 | 22 авг. 2026 г. |
| Ночная сцена не проходит по обеим осям черноты, а картинка с чёрными полосами оказывается чёрной меньше чем наполовину; и то и другое зафиксировано тестами как не чёрное. | n = 2 | 22 авг. 2026 г. |
| Пробник работает одноразовым проходом сразу после того, как поток впервые дал картинку, и ещё раз на переходе в SUSPECT, когда подозрение подняла остановка видео. Как только начальный проход закрыт, в установившемся здоровом режиме он больше не запускается, поэтому чернота посреди воспроизведения на потоке, здоровом во всём остальном, никакого вердикта не даёт. | n = 2 | 22 авг. 2026 г. |
| Пробник снимает кадр через собственный вызов снимка у движка, а обратное чтение снимка с поверхности аппаратного декодирования это известное место, где плеер возвращает пустой кадр по причинам, не связанным с содержимым. Несостоявшийся снимок мы обрабатываем как подтверждающую улику; насколько часто удавшееся чтение возвращается ошибочно чёрным, мы не измеряли.PlaybackEngine.Watchdog.cs:253 calls MediaPlayer.TakeSnapshot (libvlc_video_take_snapshot); the timeout path is handled at WatchdogStateMachine.cs:867-873. | Спецификация | Не применимо |
| На каждом отсчёте пробника считаются 64-битный средний хеш и средняя яркость, оба едут в сообщении, и движок правил не читает ни того, ни другого. | n = 1 | 22 авг. 2026 г. |
| Ровный кадр даёт средний хеш, у которого выставлены все биты, потому что каждая ячейка сидит ровно на общем среднем, а сравнение идёт «больше или равно». Сплошной белый и сплошной чёрный для этого хеша поэтому неразличимы. | n = 1 | 22 авг. 2026 г. |
| Обращение к уже освобождённой памяти внутри libvlc всплывает как нативное нарушение доступа, а не как исключение CLR, поэтому управляемый catch не отрабатывает никогда.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. | Спецификация | Не применимо |
| Закешированная обёртка Media из LibVLCSharp держит собственную нативную ссылку, поэтому после подмены медиа в потоке из пула она продолжает отдавать замершие счётчики мёртвого медиа и ни разу не бросает исключение.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. | Спецификация | Не применимо |
| Из-за закешированной обёртки медиа сборщик сообщал замершие счётчики мёртвого медиа на здоровом потоке, и это читалось как остановка сети. Наблюдалось вживую в полевом приёмочном прогоне 2026-07-24 в 23:24. Исправлено тем, что обёртка берётся и освобождается на каждом тике. | n = 1 | 24 июл. 2026 г. |
| Двенадцать сценарных односекундных тиков доходят до HEALTHY на 2 с, до SUSPECT на 7 с и объявляют F5 VideoFreeze на 11 с с возрастом аномалии 4000 мс. Тот же сценарий, где вдобавок останавливается счётчик звука, объявляет на том же тике F6 TotalFreeze, а третий прогон, в котором снимки в SUSPECT уходят в таймаут, объявляет F5, дописав к строке причины «video stalled while audio keeps playing (snapshot timeout observed)». | n = 12 | 22 авг. 2026 г. |
| Набор тестов Playback прогоняет 108 тестов с 0 отказов. | n = 108 | 22 авг. 2026 г. |
| Пять ступеней: пинок паузой и воспроизведением только для полных замираний, полное повторное применение, переход на следующего кандидата, пересборка плеера, затем сдаться. Одна попытка перезапуска до эскалации, а сбой в первые 15 с после переключения заходит сразу на переходе, а не на повторном применении. | n = 5 | 22 авг. 2026 г. |
| У ступени пересборки плеера пока нет примитива на стороне хоста, и в выпущенных настройках она отключена, поэтому эпизод с таймаутом остановки сейчас обрывает лестницу на переднем плане и уходит в сдачу. | n = 1 | 22 авг. 2026 г. |
| Попытка k ждёт минимум из базы, умноженной на (2^k - 1), и потолка, с симметричным джиттером, поэтому первая попытка срабатывает сразу, а рост со второй идёт 1 с, 3 с, 7 с и дальше до потолка. Джиттер применяется после потолка, поэтому ожидания на потолке рассеиваются симметрично вокруг 30 с, а настоящий предел это 36 с. | n = 1 | 22 авг. 2026 г. |
| Пробник пишет два PNG-файла по кругу в собственную локальную папку данных приложения и перезаписывает их. События сбоев уходят в локальный лог, где канал опознаётся по первым 16 шестнадцатеричным символам SHA-256 и никогда по адресу. | n = 1 | 22 авг. 2026 г. |
| Типы записей телеметрии объявлены и ни разу не создаются, а пакетного отправщика, названного в их doc-комментарии, не существует. | n = 3 | 22 авг. 2026 г. |