LiteCoding

Заметки о программировании

Domination под микроскопом

without comments

В этой статье пойдет речь о видеосервере Domination. Не так давно была одна интересная задача, связанная с написанием клиента, способным принимать с него поток видеоданных. Было довольно интересно разбираться с ним, а по итогам этого исследования родилась мысль написать статью. Итак, приступим.

1. Что же это такое?
Видеосервер Domination — решение от компании «Випакс», позволяющее организовать систему видеонаблюдения. Для того, чтобы все заработало достаточно иметь этот самый сервер (не самый изящный ящик, зато работает) и камеры. Список поддерживаемых камер впечатляет. В целом он представляется законченным решением, уже готовым к интеграции. Но для моей конкретной задачи требовалось закоммутировать его с самописным решением, представляющим собой смесь экспертной системы с модулем принятия решений. Т.к. необходимого для этого ПО не нашлось, пришлось немного покопаться во внутренностях программы Видеоклиент версии 1.4.5.

2. Первый подход или бенефис WireShark
Фрагмент трафика между видеосервером Domination и Видеоклиентом
На скриншоте изображен фрагмент трафика между видеосервером Domination и Видеоклиентом. Сразу же в глаза бросается, что первым общение начинает сервер, хотя соединение по протоколу TCP инициирует клиент. В дальнейшем станет ясно, что это важная деталь. После получения этих четырех байтов Видеоклиент отправляет свое сообщение, которое на первый взгляд является шифрованным. Во всяком случае, нет ни единого намека на длину сообщения или длину его полезной нагрузки. А вот ответы приходят в незашифрованном виде, и в нем сразу же в глаза бросаются стандартный восьмибайтовый заголовок, а также следующие 8 байт, в которых могут быть закодированы тип и длина сообщения. На этом этапе я предположил, что формат сообщения в обоих случаях (от клиента к серверу и от сервера к клиенту) один и тот же. Однако, пока что никаких доказательств тому не было.

3. Что показал dotPeek
Со странной смесью азарта и лени я уже было приготовился вооружиться дизассемблером, как вдруг поверхностный анализ ActiveXControl.dll показал, что сие творение использует .Net Framework. А это означает, что есть шанс получить его исходный код в более-менее читаемом виде. Для выполнения этой почетной миссии я выбрал JetBrains dotPeek.

Конечно, я не ожидал, что будет просто, но такое количества классов, интерфейсов и перечислений я тоже встретить не рассчитывал.
Фрагмент дизассемблированного ActiveXControl.dll

Чутье подсказало, что нужно двигаться дальше, т.к. оставалась еще библиотека vi.dll, где по логике вещей должна скрываться работа с сетью. Продвигаясь по коду я удивлялся кириллическим идентификаторам. Точнее, не им самим, а упорству, с которым разработчики переключались между раскладками при их наименовании. Зато сам код оказался читаемым, что не могло не радовать.

И вот, наконец, глаз зацепился за пространство имен packetParser, в котором нашелся замечательный класс TPacket. Его анализ показал, что все пакеты начинаются с «магической» константы VIPAKSRU в ASCII (хотя я это уже подозревал после анализа трафика), а также, что часть пакетов xor’ится по одному из байтов этого заголовка. Кроме того, в конструкторе явно указано, что по смещениям 8 и 12 находятся тип пакета (или сообщения, что не имеет значения) и длина его полезной нагрузки, увеличенная на единицу. Последний байт всегда будет нулевым.

Тут же находится перечисление типов пакетов.

namespace packetParser
{
  public enum EPacketType
  {
    ptNone = -1,
    ptCommand = 0,
    ptReply = 1,
    ptEvent = 2,
    ptMotion = 3,
    ptJpeg = 4,
    ptPlayerCommand = 5,
    ptGSM = 6,
    ptHW = 7,
    ptCarno = 8,
    ptMotionGraph = 9,
    ptDaysList = 10,
    ptDayList = 11,
    ptPlayerPos = 12,
    ptMSJpeg = 13,
    ptLog = 14,
    ptMacros = 15,
    ptMacrosList = 16,
    ptEventList = 17,
    ptDetector = 18,
    ptSDKCommand = 19,
    ptPROIFrame = 20,
    ptPRODeltaFrame = 21,
    ptPROAudioFrame = 22,
    ptDiskInfo = 23,
    ptPTZ3 = 24,
    ptArecaInfo = 25,
    ptAxisAudio = 26,
    ptArecontIFrame = 27,
    ptArecontDeltaFrame = 28,
    ptDahuaIFrame = 29,
    ptDahuaDeltaFrame = 30,
    ptDahuaAudioFrame = 31,
    ptDahuaInvertedIFrame = 32,
    ptSubIFrame = 33,
    ptSubDeltaFrame = 34,
  }
}

Сообщения типа ptCommand — это команды, как можно догадаться из названия. Их существует довольно много. Ниже объявление их подвидов на Java, которые я перенес из кода на C#:

    public static final byte COMMAND_GET = (byte) 0;
    public static final byte COMMAND_SET = (byte) 1;
    public static final byte COMMAND_TEMPSET = (byte) 2;
    public static final byte COMMAND_UNSET = (byte) 3;
    public static final byte COMMAND_MULTISET = (byte) 4;
    public static final byte COMMAND_DELETE = (byte) 5;
    public static final byte COMMAND_CREATE = (byte) 6;
    public static final byte COMMAND_LOGIN = (byte) 7;
    public static final byte COMMAND_EXIT = (byte) 8;
    public static final byte COMMAND_HALT = (byte) 9;
    public static final byte COMMAND_GETIMAGE = (byte) 10;
    public static final byte COMMAND_SUBSCRIBE = (byte) 11;
    public static final byte COMMAND_UNSUBSCRIBE = (byte) 12;
    public static final byte COMMAND_PTZ = (byte) 13;
    public static final byte COMMAND_HELLO = (byte) 14;
    public static final byte COMMAND_NETIPCFG = (byte) 15;
    public static final byte COMMAND_SAVECONFIGURATION = (byte) 16;
    public static final byte COMMAND_READCONFIGURATION = (byte) 17;
    public static final byte COMMAND_REMEMBER_VAR = (byte) 18;
    public static final byte COMMAND_RESTORE_VAR = (byte) 19;
    public static final byte COMMAND_SLEEP = (byte) 20;
    public static final byte COMMAND_RAISE_EVENT = (byte) 21;
    public static final byte COMMAND_VERS_SWITCH = (byte) 22;
    public static final byte COMMAND_ADAM_SWITCH = (byte) 23;
    public static final byte COMMAND_TIMECFG = (byte) 24;
    public static final byte COMMAND_PTZ2 = (byte) 25;
    public static final byte COMMAND_FLUSH_STORAGE = (byte) 26;
    public static final byte COMMAND_RAISE_CAMALARM = (byte) 27;
    public static final byte COMMAND_IFRAME_ACK = (byte) 28;
    public static final byte COMMAND_RECORDS_COPY = (byte) 29;
    public static final byte COMMAND_RECORDS_REMOVE = (byte) 30;
    public static final byte COMMAND_RECORDS_MOUNT = (byte) 31;
    public static final byte COMMAND_AV = (byte) 32;
    public static final byte COMMAND_IPPTZ = (byte) 33;
    public static final byte NUMBER_OF_COMMANDS = (byte) 33;
    
    public static final byte COMMAND_PLAYER_DAYSLIST = (byte) 0;
    public static final byte COMMAND_PLAYER_DAYLIST = (byte) 1;
    public static final byte COMMAND_PLAYER_FILTER = (byte) 2;
    public static final byte COMMAND_PLAYER_REWIND = (byte) 3;
    public static final byte COMMAND_PLAYER_PLAY = (byte) 4;
    public static final byte COMMAND_MACROS_GET = (byte) 5;
    public static final byte COMMAND_MACROS_SET = (byte) 6;
    public static final byte COMMAND_GET_MACROSLIST = (byte) 7;
    public static final byte COMMAND_GET_EVENTLIST = (byte) 8;
    public static final byte COMMAND_SET_EVENTLIST = (byte) 9;
    public static final byte COMMAND_SET_DETECTOR = (byte) 11;
    public static final byte COMMAND_GET_DETECTOR = (byte) 12;
    public static final byte COMMAND_PLAYER_ACK = (byte) 13;
    public static final byte COMMAND_PLAYER_ZOOMCAM = (byte) 14;
    public static final byte COMMAND_GET_DISKINFO = (byte) 15;
    public static final byte COMMAND_PLAYER_SWITCH_RECORD_SRC = (byte) 16;
    public static final byte COMMAND_PTZ3 = (byte) 17;
    public static final byte NUMBER_OF_PLAYER_COMMANDS = (byte) 18;
    
    public static final byte COMMAND_SDK_WINDOW = (byte) 0;
    public static final byte COMMAND_SDK_CAMERA_SHOW = (byte) 1;
    public static final byte COMMAND_SDK_VERSION = (byte) 2;
    public static final byte COMMAND_SDK_SET_RECORD_SPEED = (byte) 3;
    public static final byte COMMAND_SDK_ADD_CAM = (byte) 4;
    public static final byte COMMAND_SDK_REMOVE_CAM = (byte) 5;
    public static final byte COMMAND_SDK_OPEN_VIEW = (byte) 6;
    public static final byte COMMAND_SDK_SET_PLAY_TIME = (byte) 8;
    public static final byte COMMAND_SDK_PLAY = (byte) 9;
    public static final byte COMMAND_SDK_GET_MACROS_LIST = (byte) 10;
    public static final byte COMMAND_SDK_GET_USER_EVENTS = (byte) 11;
    public static final byte COMMAND_SDK_RAISE_USER_EVENT = (byte) 12;
    public static final byte COMMAND_SDK_GET_TIME = (byte) 13;
    public static final byte COMMAND_SDK_SET_TIME = (byte) 14;
    public static final byte COMMAND_SDK_RAISE_USER_EVENT_BY_MACROSNAME = (byte) 15;
    public static final byte COMMAND_SDK_PTZ_POSITION = (byte) 16;

Разбирать весь код этой монструозной библиотеки — это тема, достойная отдельного цикла статей (которые, признаться, я не собираюсь писать, т.к. охладел к этой теме, докопавшись до результата). Этот разбор сделать нетрудно, но он грозит отъеданием огромного промежутка времени. Если у вас когда-нибудь возникнет подобная задача, то сразу же закладывайте не меньше 5 полных дней на погружение в код, ибо его тут много. Есть еще один нюанс, который стоит отметить отдельно: для получения данных используется механизм подписки (COMMAND_SUBSCRIBE) на сообщения определенного вида. Причем, каждое соединение поддерживает только один вид сообщений. Таким образом для получения данных с N камер потребуется 1 соединение, по которому будет передаваться служебная информация о статусе камер, и еще N соединений для передачи видеоданных.

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

А теперь попробуем добраться до изображения. Каждый пакет с видеоданными содержит еще подзаголовок, в котором передается 12 байт служебной информации. На Java я объявил его следующим образом (калька с оригинального кода на C#):

public int mDetectorMask;
public byte mSource;
public byte mDetector;
public byte mSpeed;
public byte mCamerainfo3;
public int mTimestamp;

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

4. Китайский след
После отделения служебных заголовков у нас на руках остается Dahua-фрейм, с которым нужно что-то делать. Передать его напрямую libav не получится, т.к. он (вы не поверите! :)) тоже содержит служебную информацию, которая частично дублирует ту, что мы уже получили. Исходный код указывал на то, что его разбором занимается библиотека dhplay.dll, которая написана уже не на .NET, а потому dotPeek тут уже не помощник. Но на просторах сети я нашел исходники проекта, который включал в себя нечто похожее на dhplay. Если вы решите посмотреть, как происходит разбор, рекомендую взглянуть на newstream.cpp.

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

5. Заключение
Нельзя объять необъятное. По этой причине в этой статье не получилось дать полную информацию по тому, как общаться с видеосервером Domination, как добираться до данных и декодировать их. Моей целью было «подсказать дорогу» тому, кто решится интегрировать это решение со своими сервисами. Надеюсь, эта статья вам помогла.

Share and Enjoy:
  • Print
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google Bookmarks
  • LinkedIn
  • Tumblr

Written by Дмитрий Воробьев

Понедельник, Январь 14th, 2013 at 06:21

Leave a Reply

You must be logged in to post a comment.