На Geektimes я часто встречаю и с удовольствием читаю посты из серии DIY. Решив сделать небольшой вклад в копилку ценного опыта, собранного здесь, я собираюсь подробно описать процесс создания клиента для веб, базирующегося на серверах «Линия».
Система видеонаблюдения «Линия» предоставляет открытое API, и разработчики заявляют, что на его основе можно написать собственный клиент для просмотра видеоархива и камер онлайн. Также при желании можно реализовать такие функции, как добавление событий в архив, наложение OSD поверх видео. Описание всех возможностей представлено в спецификации на официальном сайте.
Данная статья — это реальный пример, как я, пользователь с начальными знаниями JS, HTML, написал собственное приложение, реализующее базовые принципы работы с серверами «Линия» через встроенный web-сервер.
Вводные данные
Автор — новичок в разработке HTML-клиента, имею отношение к разработке системы видеонаблюдения «Линия».
Уровень знаний JS, HTML — начальный.
Задача — написать HTML-клиент для работы с устройствами на базе программного обеспечения «Линия» с помощью спецификации с сайта.
Главную интригу раскрою сразу – я пришел к двум выводам:
- Спецификация реальная, описано достаточно понятно, написать клиент можно, используя C++, PHP.
- Полноценный HTML-клиент, используя только лишь JS, написать нельзя — только онлайн-наблюдение по спецификации до RPC.
Первый вывод вполне закономерен, если учесть большое количество интеграций со сторонними программами. Все они описаны на сайте: тут есть СКУД, весовые, POS-системы, программы для определения автомобильных номеров и 1С.
Второй вывод более интересен, рассмотрим его ниже.
Почему нельзя создать полноценный клиент на 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)
i3laze
01.02.2018 08:46Похвально!
Первый абзац стоило бы посвятить описанию задачи (зачем понадобился такой клиент и что он должен уметь).
Тогда остальное читать было бы интереснее.devlineman Автор
01.02.2018 10:12Спасибо за отзыв! Лично для меня это была первая подобная статья, учту ваше пожелание на будущее.
devlineman Автор
01.02.2018 10:47В текущий момент видео в браузере отображается при помощи плагина Adobe Flash. Данная технология уже устарела, в то время как HTML5 и JavaScript активно наращивают функционал. Поэтому и встала задача предоставить возможность просмотра видео в браузере, используя современные технологии, что особенно актуально для мобильных устройств.
mikhaylovns
devlineman Автор
В роли клиента выступает браузер, именно он и отправляет запросы серверу.
Как это обычно бывает, файлы, выдаваемые веб-сервером по запросу браузера, располагаются в каталоге www. В Линии веб-сервер встроен в ядро программы, и простое «подкидывание» файлов ни к чему не приведет — браузер будет по-прежнему выдавать дефолтную страницу. Поэтому мне при разработке пришлось размещать файлы для браузера на стороннем ресурсе. По окончанию разработки полученные файлы будут добавлены в веб-сервер Линии, что позволит пользователям просматривать видео с серверов «Линии» через браузер без установки дополнительных плагинов (Adobe Flash).