Клиент HTML + JS для системы видеонаблюдения «Линия»

Клиент HTML + JS для системы видеонаблюдения «Линия»

На Geektimes я часто встречаю и с удовольствием читаю посты из серии DIY. Решив сделать небольшой вклад в копилку ценного опыта, собранного здесь, я собираюсь подробно описать процесс создания клиента для веб, базирующегося на серверах «Линия».

Система видеонаблюдения «Линия» предоставляет открытое API, и разработчики заявляют, что на его основе можно написать собственный клиент для просмотра видеоархива и камер онлайн. Также при желании можно реализовать такие функции, как добавление событий в архив, наложение OSD поверх видео. Описание всех возможностей представлено в спецификации на официальном сайте.

Данная статья — это реальный пример, как я, пользователь с начальными знаниями JS, HTML, написал собственное приложение, реализующее базовые принципы работы с серверами «Линия» через встроенный web-сервер.

Вводные данные

Автор — новичок в разработке HTML-клиента, имею отношение к разработке системы видеонаблюдения «Линия».
Уровень знаний JS, HTML — начальный.
Задача — написать HTML-клиент для работы с устройствами на базе программного обеспечения «Линия» с помощью спецификации с сайта.

Главную интригу раскрою сразу – я пришел к двум выводам:

  1. Спецификация реальная, описано достаточно понятно, написать клиент можно, используя C++, PHP.
  2. Полноценный HTML-клиент, используя только лишь JS, написать нельзя — только онлайн-наблюдение по спецификации до RPC.

Первый вывод вполне закономерен, если учесть большое количество интеграций со сторонними программами. Все они описаны на сайте: тут есть СКУД, весовые, POS-системы, программы для определения автомобильных номеров и .

Второй вывод более интересен, рассмотрим его ниже.

Почему нельзя создать полноценный клиент на HTML + JS?

Ответ: кросс-доменные запросы.

На данный момент веб-сервер «Линии» ограничен, и путем простого копирования кода в папку www доступ получить нельзя. Однако разработчики обещают, что в новой версии для Linux и в «Линии 8.0» веб-сервер будет работать стандартно: в случае запроса, при наличии файла, будет его возвращать.

Сейчас же создаем новый проект и начинаем кодить. Как и все новички в программировании для веб, уточнив, что сервер «Линии» отвечает «*» в заголовке Access-Control-Allow-Origin, я начал усиленно трудиться над кодом, проверяя результат на Firefox 57.0.4 (64-бит). Запросы на сервер отправлял XMLHttpRequest.

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

На момент написания статьи четыре основных современных браузера не дают прочитать заголовки, получаемые от сервера. Согласно спецификации необходимо реализовать Digest-авторизацию, что без заголовков невозможно.

К концу первого дня я понял, что без добавления в веб-сервер «Линии» обработки OPTIONS ничего не получится, так как для запросов с «непростым» методом или особыми заголовками браузер делает предзапрос OPTIONS, указывая их в Access-Control-Request-Method и Access-Control-Request-Headers. Поэтому я начал искать другие варианты авторизации, а настоящие Basic или Digest не взлетели.

Альтернативный способ уже был описан в спецификации, оставалось потратить какое-то время на переписку с программным отделом «Линии». Так как подобные трудности возникают не в первый раз, уже есть костыль для авторизации, и он даже упомянут в спецификации:

На клиентах, где невозможно авторизовать запрос стандартными средствами (HTTP Digest/Basic Authentication), возможна передача заголовка Authorization одним из параметров запроса, например
/kfd3ado1sdrms/streaming/main.flv?authorization=Basic%20d2ViOg==

После всех манипуляций стандартный кросс-доменный запрос стал корректно выполняться! Необходимо также добавить в запрос заголовок Accept c правильным типом — я решил использовать JSON.

Код запроса:

function get_request_url(method,current_server_data, resource, additional){     var request = current_server_data.server_ip + ':' +current_server_data.port +resource+'?authorization=Basic '+ utf8_to_b64(current_server_data.user+':'+current_server_data.password);     if (additional != '' && typeof additional  != "undefined") {         request += '&' + additional;     }     return request; }  function http_request_of_resource (server_index , resource, auth_attempt) {     var request = get_request_url('GET', servers_array[server_index], resource,'');     var req_ = new XMLHttpRequest();     req_.open('GET', 'http://'+ request, true);     //req_.timeout = 9000;     // выполнить код, когда придёт ответ     req_.onreadystatechange = function() {         if (this.readyState == 2) {            if (this.status == 401) {                 //console.log('---unauthorized');                 hideModal();                 update_nessecary_structure(resource, 'unauthorized', server_index);             }         } else if (this.readyState === 4) {             if (this.status === 0) {                 hideModal();                 update_nessecary_structure(resource, 'server_down',server_index)             }             if (this.status == 200) {                 if (auth_attempt) hideModal();                 else resource =(resource =='/cameras') ? resource+'_update_info': resource;                 //console.log('200' + this.responseText);                 update_nessecary_structure(resource, this.responseText, server_index);             } else if (this.status == 404) {                 //console.log('404');                 update_nessecary_structure(resource, '404', server_index);             }         }     };          // Оправка запроса     req_.setRequestHeader('Content-type', 'text/plain; charset=utf-8');     req_.setRequestHeader('Accept', 'application/json');     req_.send();          }

Меняем resource на нужный нам согласно спецификации и получаем те или иные данные. Переменная additional содержит дополнительные параметры для запроса, если таковые необходимы. На этом освоение первой половины спецификации, а именно получение/отправки текстовых данных посредством GET-запросов, можно считать закрытым.

Далее я столкнулся с тем, что тэг IMG в IE не воспроизводит MJPEG-поток и нужно самостоятельно реализовать обновление изображений с камер. Код открыт, его можно посмотреть и при желании изменить. В текущей реализации доступно одновременное воспроизведение максимум шести MJPEG потоков, так что работу с видом, отображающим большее количеством камер, придется делать самим. Все это есть в примере, при желании можно найти и разобраться, но если возникнут вопросы, обязательно задавайте в комментариях.

Спецификация RPC

Нам предложено отправлять и получать данные либо в JSON (версия сервера «Линии 7.1.1» и выше) или MessagePack (версия «Линия 7.0» и выше). Упоминают, что MessagePack меньше весит и работает быстрее, но, если честно, я бы выбрал JSON (он уже встроен в JS), если бы не одно но в спецификации: получение кадров из архива возможно только в MessagePack. Пришлось идти на их официальный сайт и качать JS-файл, который имеет на борту методы encode и decode.

Функция отправки запроса готова! Но праздновать победу рано: при попытке поменять заголовок запроса Content-type браузер ругается и не отправляет данные серверу. Дело в том, что сервер «Линии» анализирует это поле и в зависимости от типа производит парсинг. Своими силами дальше обойтись не смог.

Отправил заявку в программный отдел, и после обсуждения мне добавили костыль, как и в случае с авторизацией, — Content-type будет передаваться в url-запросе:

function rpc_request_of_resource (current_server_data , rpc_method, rpc_request) {     var request = get_request_url('POST', current_server_data, '/rpc','');     //console.log("i'm here request = " + request + '  '+ current_server_data.user);     request += "&content-type='application/x-msgpack'";     var req_ = new XMLHttpRequest();     req_.open('POST', 'http://'+ request, true);        // выполнить код, когда придёт ответ     req_.responseType = 'arraybuffer';     req_.onreadystatechange = function() {         if (this.readyState == 2) {            if (this.status == 401) {                 //console.log('401' + this.getAllResponseHeaders());                 console.log('unauthorized');             }         } else if (this.readyState == 4) {            if (this.status == 200) {                 //if (auth_attempt) hideModal();                 //console.log('200' + this.responseText);                 rpc_update_nessecary_method(rpc_method, this.response);             } else if (this.status == 404) {                 console.log('404');             } else if (this.status == 500) {                 //console.log('500');                 rpc_update_nessecary_method(rpc_method, '500');             }         }        };          // Оправка запроса     //req_.setRequestHeader('Content-type', 'text/plain; charset=utf-8');     //req_.setRequestHeader('Content-type', 'application/x-msgpack');     req_.setRequestHeader('Accept', 'application/x-msgpack');     req_.send(rpc_request);          }

Данное изменение будет работать с версии «Линия 7.4.1» и выше. Для всех серверов ниже этой версии работа с ресурсом /rpc будет недоступна.

В конце хочу поблагодарить всех клиентов, которые нам присылали вопросы/пожелания, связанные с реализацией приложений на базе нашего API. Благодаря вам было проведено исследование, в рамках которого выявлены и исправлены некоторые недочеты.

Пример, который описан в данной статье, постепенно вырастет в полноценный HTML-клиент «Линии». Весь код будет читабельным, вы его сможете менять или использовать как основу для построения собственных решений. API же со временем наполнится еще большим количеством возможностей, о которых мы обязательно проинформируем.

 
Источник

Читайте также