Исправляем ошибки своими руками, или баг, который «никого не колышет»

в 17:39, , рубрики: Без рубрики

Исправляем ошибки своими руками, или баг, который «никого не колышет» Недавно я уже поднимал волну о баге TCP стриминга камер, но тогда я её катил исключительно на китай. А проблема куда как шире. Для себя я проблему решил, страждущим выкладываю прошивки с фиксом.

А теперь садитесь поудобнее, я поведаю вам об этом баге подробно.

Вкратце о баге

Для тех, кто не читал предыдущее растекание мыслью по древу, да и просто для формализации проблемы, распишу вкратце сам баг.
Итак, у вас есть камера. Вы её ставите, подключаетесь, смотрите — и всё замечательно. Теперь вы оставляете её на некоторое время — и начинаете наблюдать видеоартефакты (по разным причинам теряются пакеты). Вы говорите АГА! и переключаете камеру с UDP на TCP (там-то теряться не должны!). И наблюдаете более интересную картину — с завидной регулярностью просто пропадает соединение. При этом сеть в порядке, потерь не видно, вообще ничего не видно — но камера регулярно отваливается…

Природа появления

Итак, есть некая камера, вещающая поток в сеть по RTSP.
Вещание может вестись по UDP, и тогда каждый сетевой пакет — самодостаточен (более-менее). Протокол и всё остальное должны быть готовы в любой момент что любой из пакетов пропадёт, или будет испорчен порядок. Протокол на это расчитан, клиенты тоже.
Вещание может вестись по TCP. Так как TCP это потоковый протокол с гарантией доставки, он не разбивается на пакеты (в теории), в протокол добавлены метки каждого кадра с длиною их. Это позволяет представить TCP как UDP — просто считываем маркер, длину, и читаем после этого нужную длину байт => получили пакет, задача сведена к предыдущей.
Но есть пара нюансов: если канал между камерой и клиентом тоньше, чем создаваемый камерой поток, то если на UDP просто пакеты будут пропадать, сами, то на TCP начнутся перезапросы. Исходящие пакеты начнут копиться в памяти камеры. Поэтому в определённый момент, память у камеры закончится.
Чтобы этого не происходило, все производители серверной части делают так или иначе выкидывание данных, в случае если они не влезают в исходящую дырку.
Вот только это выкидывание надо делать корректно — если на UDP пакеты теряются целиком, то на TCP пакет замирает в произвольный момент. И вот тут начинается та самая магия, которая и является корнем всех зол.

Самым известным opensource RTSP стримером является сервер Live555.
В той или иной форме он лежит в основе многих других производных используемых в кулуарах производителей камер.

Рассмотрим хрестоматийную реализацию отправки пакетов поверх TCP, который до сих пор встречается в камерах даже местами в исходной форме.

Взглянем в функцию sendRTPOverTCP: отправка реализована «в лоб». Отправляем метку начала пакета '$'. Отправка номера канала следующим байтом. Затем формируем длину и отправляем следующими двумя байтами. И, наконец, отправляем весь пакет (который на UDP уехал бы одним send()'ом).

Каждая отправка проверяется на то, что данные отправлены (сокет в неблокирующем режиме, поэтому отправляется только то, что может быть отправлено). Если send() вернул не исходную длину пакета — ошибка, выходим из функции отправки. Дропая пакет.
Дропая ли? Нет!

Итак, начнём с того, что отправка одного пакета представляет собой 4 отдельных send()'а. И ошибка на любом из них, все остальные не будут вызваны. То есть может получиться, что отправится только $, и ничего более. Либо отправится $ и номер канала, а длина нет. Либо отправится $, номер канала и длина, а сам пакет нет. Либо…

send() в неблокирующем режиме копирует в исходящий буфер/пытается отправить переданный пакет. Возвращает число байт, которые ушли, либо легли в буфер отправки. Еще раз: число байт, которые УШЛИ либо ЛЕГЛИ в буфер отправки.

Таким образом получается, что так же может быть отправлено пол пакета. Либо пол длины… В результате, отправляемый поток будет сломан, так как один пакет уходит не целиком. Простые клиенты, которые просто читают $+канал+длина+пакет_нужной_длины будут ломаться в этих местах — длина будет самая разнообразная, либо после считывания всего пакета дальше $ не будет (так как мы прочитали больше, чем было там заявлено).

В один прекрасный момент баг был замечен, и «исправлен». Смотрим более свежую реализацию: отправка производится в два шага вместо четырех, сперва собирается префикс пакета, и затем отправляется сам пакет. Причем отправка производится специальной функцией sendDataOverTCP, которая должна гарантировать отправку пакета целиком, и вернуть были ли проблемы с отправкой.

Не получилось. Найдёте сами почему?

Алгоритм «гарантированной» отправки: делаем send() на неблокирующем сокете. Если возвращает ошибку — переключаем сокет на блокирующий, и отсылаем в блокирующем режиме. Потом возвращаем сокет в неблокирующий, и сообщаем что была ошибка.

Еще раз: уверены, что нашли ошибку? Все? ;)

Итак, главная ошибка: первый send() УЖЕ что-то отправил! Таким образом, делая send() в блокирующем режиме, мы повторно отправляем начало пакета!

У нас ДВЕ отправки. И первая, хоть и передаёт forceSendToSucceed==False, тоже могла передать что-нибудь. Нарпимер, 3 байта — $, номер пакета и младший байт длины. Затем ошибка, отправка данных не производится, потом приходит отправка следующего пакета, и его $ идёт как старший байт длины…

Неужели баг будет вечным? Нет! В декабре 13го года баг «пофиксили». Вот финальная версия.

Вроде, предусмотрели всё: если ничего не отправилось — то возвращают ошибку, и пакет не уходит целиком. Если что-то отправилось, до доотправляем в блокирующем режиме только остаток, и если остаток ушел целиком, то возвращаем успех. Таким образом, отправка следующим шагом данных пакета будет выполнена. И всё «хорошо».

Ну сейчас-то что не так? А вот что: пакет уходит целиком, всегда, в блокирующем режиме. Таким образом, проблема с отправкой к одному клиенту, вызывает тормоза на всех клиентах, подключенных к камере.
А еще, sendPacket() теперь ничего не дропает, если хоть один байт да влез в исходящий пакет. А так как размеры пакетов никто не выравнивал, совпадение размера исходящего буфера и кратности его отправляемым пакетам не равна, получится, что при наличии проблем отправки, ситуации дропа пакета просто не будет…

Ну хоть поток ломаться не будет. Спасибо и на этом. Главное, OOM не словить за это время. Вот только видео начнёт отставать…

Иначе говоря — финальное решение в Live555 я считаю некорректным…

Корректное решение (причем довольно простое!) я опишу в следующий раз, чтобы подогреть интерес читателя :)

Ареал распространения

Итак, баг распространён широко. Все ошибки, которые мы видели выше в коде Live555 не являются из ряда вон — это стандартные ошибки, которые повторяют абсолютно все программисты, работающие с сетью.

Баг замечен на море китайских камер; причем не только на тех, которые основаны на Live555. Баг встречали в D-Link камерах. Баг встречали в самых разных брендовых камерах (которые, как всегда, основаны на различных модулях китайских производителей).

Вероятность получения проблем растёт с увеличением разрешения и битрейта камеры. Именно по этой причине он долгое время оставался незамеченным: соотношение разрешения к цене камер капитально начало расти в последнее время, FullHD и более толстые камеры начинают пользоваться спросом. А благодаря ценам китайцев, именно на них и начинают замечаться они чаще всего. Как всегда, грешить начинают на китайцев… Хотя ошибки внесены совсем не китайскими программистами.

Диагностика

Если у вас есть камеры и софт мониторинга — переключите его для пробы в TCP режим на некоторое время. Если несмотря на стабильное подключение будут наблюдаться обрывы связи, либо если на нестабильном подключении вместо целого видео или обычных видеоартефактов софт падает либо теряет соединение — у вас багованый и клиент и камеры.

Чтобы потыкать палочкой только вашу камеру — можно воспользоваться моим скриптом.
Он не предназначен для конечных пользователей, так как накидан на коленке.

В начале скрипта задаются параметры: host — IP камеры, url — полный путь в URL потока до прямо нужного трека. При нестандартном порту можно переопределить port.

Параметры dump позволяет писать RTP поток в видео, которое можно посмотреть тем же mplayer; dumpraw позволяет писать сырой поток как есть.

Для повышения частоты сбивания, можно раскомментировать строку 112 (с «time.sleep(st)»). А в строке 176 if позволяет выбрать режим восстановления потока. При False производится ресинхронизация потока, при True производится полное пересоединение. Это позволяет оценить разницу во времени.

Методы лечения

Итак, есть баг. Распространен очень широко, но всплывать начал в недавнее время — огромное количество железа уже in the wild с этим багом. Как правльно лечить в таком случае?

Моё личное мнение: лечение должно быть двусторонним. Одновременно надо править прошивки камер, чтобы новое железо шло без бага, и была возможность обновить прошивки уже имеющихся камер.

Но и клиенты обязаны уметь жить в условии возможных поломок потока. Сбой потока = превышение => просто ищем начало следующего целого пакета. По сути, это сводит ситуацию к UDP, только контроль целостности выпадания пакета переходит к приложению.

Чтобы поправить баг со стороны клиента, надо мучать саппорт каждого из этих клиентов. macroscop уже отписывались в прошлом посте, и, может, прочитают этот. Я сейчас полностью пересел на AxxonNext — их саппорт уведомлен, впрочем, я их мучаю этим багом уже давно. Пользователей, которые сталкиваются с этой проблемой — призываю вас создавать тикеты и просить производителей вашего софта принять меры со своей стороны. Erlyvideo недавно добавили поддержку восстановления потока после сбоя с моей подачи.

Не следует код ресинхронизации, реализованный у меня в тест-скрипте, считать за оптимальный — он быстр в написании; можно реализовать более корретный (ресинхронизующийся раньше и корректнее) и быстрый, однако он прекрасно подходит как отправная точка, а так же как proof-of-concept.

Чтобы поправить баг со стороны камер, я пытался мучать всех, до кого дотянулся: я писал продавцу-китайцу, которые из этих модулей собирают. Я писал другим продавцам-производителям. Я пытался писать производителю камеры. Я пытался писать производителю embedded linux, который стоит на камере. Я писал на хабр.

К сожалению, итог нулевой: слишком уж я мелкая сошка. К счастью, ко мне постучал Андрей Сёмочкин (deepweb), который работает в ipeye.ru/

IP EYE предоставляют облачное хранилище видеозаписей и собственные камеры, на базе точно таких же модулей, как у меня на внешних камерах — на модулях от TopSee (TS38). Они очень сильно перерабатывают интерфейс и функционал прошивок этих камер. Впрочем, как я понял, исходников оригиналов прошивок нет, они перебирают уже имеющиеся камеры собирая нужные модули, заменяя софт и т.д. Так как они предоставляют облако, большинство камер при этом подключается к интернету напрямую. Используя настолько удалённые камеры, использование UDP становится уже неприятно — слишком велика вероятность ретрансмита, хотя толщины канала хватает за глаза. В качестве серверного приёмника используется erlyvideo (в смысле, flussonic). Используя старую версию flussonic (без ресинхронизации) количество отваливания различных камер просто огромное. Использование обновлённой версии (с ресинхронизацией) значительно снижает количество реконнектов (хотя объёмы потерь всё еще неприятные).

Так вот, отвлекся. Андрей предложил потестировать на его стенде исправленный мною стример… Таким образом у меня появился доступ до тестовых камер значительно проще, чем пытаться собрать исходники от sigrand.

Лечение

К сожалению, уже текущая статья вышла громадной, так что применённый мною метод лечения будет описан на днях в отдельной статье.

Вкратце: распаковываем прошивку, достаём оттуда стример, ищем сбойное место, патчим в бинаре, пересобираем прошивку обратно.

Бонусные баги

Пока разбирался с этим багом, Макс Лапшин (erlyvideo) напоролся на другую интересную багу. Камера в целом выглядит красивой, они применили правильный метод буферизации — если пакет не уходит сразу целиком, он остаётся в буфере и пытается отправляться позже как подойдёт время; все пакеты, которые были пока не ушел задержавшийся молча дропаются (причем дропаются целиком — включая $/канал/размер и номера пакетов), так что выглядит это именно как красивый пакетдроп аналогичный UDP.

Вот только побочный эффект такого метода буферизации: ответ на keep-alive запросы, проходящие по TCP потоку (GET_PARAMETER либо OPTIONS) приходит сразу, как сервер получает запрос. Сразу. Как получает запрос. То есть ответ может придти даже посреди пакета данных! Таким образом, если пытаться декодировать поток «как есть» — $+пакет+длина… и ждать RTSP вместо $ — то каждый раз, как мы отправляем keepalive (раз в 30 секунд, так как без них камера рвёт поток через 45 секунд), поток ломается — в ответ приходит ~100 байт мусора, а на видео вылезает косяк, который исправляется следующим ключевым файлом.

Лечение этого бага со стороны клиента уже сложнее: надо искать и анализировать (удалять) RTSP...rnrn сперва, и уж потом в остатке искать $+пакет+длина+видео. Flussonic последней версии уже это умеет делать.

Таблетки

Подробности исправления (что и как сделано) будет позже, а пока, если ваша камера собрана на базе TS38, вы можете взять и поставить прошивки с моими изменениями, где проблема с TCP вылечена полностью.

Итак, я собрал следующие прошивки последней версии 2.5.0.6:

  1. firmware_TS38ABFG006-ONVIF-P2P-V2.5.0.6_20140126120110-TCPFIX.bin
  2. firmware_TS38CD-ONVIF-P2P-V2.5.0.6_20140126121011-TCPFIX.bin
  3. firmware_TS38HI-ONVIF-P2P-V2.5.0.6_20140126121444-TCPFIX.bin
  4. firmware_TS38LM-ONVIF-P2P-V2.5.0.6_20140126121913-TCPFIX.bin
  5. firmware_HI3518C-V4-ONVIF-V2.5.0.6_20140126124339-TCPFIX.bin

Если вдруг вам потребуется фикс на какой-либо другой модуль этого же производителя — пишите в комментах, буду посмотреть по возможности.

Автор: datacompboy

Источник


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js