На 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 же со временем наполнится еще большим количеством возможностей, о которых мы обязательно проинформируем.

Комментарии (5)


  1. mikhaylovns
    01.02.2018 08:46

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


    1. devlineman Автор
      01.02.2018 10:47

      В роли клиента выступает браузер, именно он и отправляет запросы серверу.

      Как это обычно бывает, файлы, выдаваемые веб-сервером по запросу браузера, располагаются в каталоге www. В Линии веб-сервер встроен в ядро программы, и простое «подкидывание» файлов ни к чему не приведет — браузер будет по-прежнему выдавать дефолтную страницу. Поэтому мне при разработке пришлось размещать файлы для браузера на стороннем ресурсе. По окончанию разработки полученные файлы будут добавлены в веб-сервер Линии, что позволит пользователям просматривать видео с серверов «Линии» через браузер без установки дополнительных плагинов (Adobe Flash).


  1. i3laze
    01.02.2018 08:46

    Похвально!

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


    1. devlineman Автор
      01.02.2018 10:12

      Спасибо за отзыв! Лично для меня это была первая подобная статья, учту ваше пожелание на будущее.


    1. devlineman Автор
      01.02.2018 10:47

      В текущий момент видео в браузере отображается при помощи плагина Adobe Flash. Данная технология уже устарела, в то время как HTML5 и JavaScript активно наращивают функционал. Поэтому и встала задача предоставить возможность просмотра видео в браузере, используя современные технологии, что особенно актуально для мобильных устройств.