Концепт бюджетной видеостены неограниченного размера для web-приложения
Поделюсь идеей создания видеостены из абсолютно любого количества дисплеев при минимальных вложениях в доработку имеющегося web-приложения. Суть сводится к примитивной вещи — создаём количество экземпляров приложения по количеству экранов и позиционируем их между собой. Для мультимедиа такой подход не годится, конечно, но для различного рода схем, SCADA, средств мониторинга и диспетчеризации — весьма выгодное решение, как с точки зрения финансовых затрат, так и с точки зрения прилагаемых усилий на переписывание используемого движка приложения.
В отличии от профессиональных видеостен, где мониторы собираются в один экран, с которым приложение работает как с одним целым, здесь используются все доступные в системе мониторы, вне зависимости от типа видеокарт и видеовыхода, к которым они подключены. В принципе, можно создать стену из мониторов с разными разрешениями экранов, требуется только выстроить их физически удобно для того, чтобы совпадали общие размеры рабочей области. В теории, учитывая наличие материнских плат с 10-ю PCI-E x8, на один компьютер можно подключить до 40 мониторов. Следующие 40 можно подключить на другой компьютер и позиционировать их изображение относительно мониторов первого. Не требуются специфичные видеосерверы и профессиональные видеокарты, желательно только ядер процессора и оперативной памяти в достатке иметь.
Применил этот подход в своём проекте, редактор схемы диспетчерского мнемощита, посмотреть для интереса, что выйдет. Видео, как двигается сорокаметровая схема с двухсантиметровыми элементами на двух мониторах на встроенном видео AMD Ryzen 3 3200G с 32Гб оперативной памяти.
Когда указываем несколько экранов при запуске приложения, первый экземпляр приложения создаёт копии своего окна, помещая объекты дочерних окон в единый массив, передаёт им их позиции в общем массиве экранов и создаёт им ссылку на себя. Далее все копии приложения могут общаться между собой, используя объекты родительского окна. Когда у любого из окон возникает событие прокрутки, например, оно вызывает прокрутку у всех остальных окон через объект родительского окна согласно своей новой позиции. Любое изменение в одном из окон приложения требуется применить во всех остальных, и это именно то, что требует доработки в имеющемся движке приложения. Нет смысла пытаться сделать многое, реализовать редактор с таким мультиоконным подходом можно, но слишком трудоёмко и совершенно не нужно. Такой мультиоконный режим хорошо применим в роли диспетчерского экрана, где не всегда и перемещать схему то нужно, чаще она развёрнута на весь размер видеостены. В таком случае требуется реализовать только обмен изменениями состояний элементов между окнами. И даже этого можно не делать, каждое окно, как самостоятельное приложение ведёт работу с сервером, отправляет ему изменение состояний элементов, а остальные окна получают эти изменения от сервера.
Видео с примером открытия 12 окон приложения с демонстрацией прокрутки. Окна у меня прописаны на позиционирование только в полноэкранном режиме с одинаковым разрешением всех дисплеев — это та схема, которая мне требуется, не стал заморачиваться на позиционирование с динамическим размером окон. В начале видео запускается первый экземпляр приложения и создаёт дочерние окна выстроенные в порядке их расположения на дисплеях видеостены. Запись видео с экрана плохо передаёт перемещение окон и их прокрутку, но общий смысл, думаю, понятен, на слабеньком Ryzen 3 3200G я запускал 40 экранов и они, хоть и с трудом, двигались.
Приведу пример своего кода — это буквально всё, что мне понадобилось добавить в свой проект для реализации мультиоконности. Очень многое у меня захардкожено, да. Это издержки того, что в одно лицо был написан движок редактора SCADA на чистых PHP, JS, SQL не используя ни одной сторонней библиотеки. И нет, я не исправлял фатальный недостаток, я честно пытался использовать готовые решения, первая версия редактора была написана на C# MVC с DevExpress, выбор показался современным, но MVC проявил себя с худшей стороны — он не способен работать с большими схемами. Очень многое приходилось допиливать и как-то костылить, но всегда упирался в плохо организованную систему событий. Большая схема на MVC будет прокручиваться с отрисовкой на каждом шаге — совершенно не приемлемый подход. Череда возникающих событий на каждый чих, так же неминуемо ведёт к тормозам.
Дам ценный совет — выбирайте платформу умеющую прерывать выполнение событий при возникновении таких же новых для приложений работающих с большим размером рабочего пространства и множеством элементов. Такое умеют браузеры и игровые движки. В памяти даже всплывает, как давным-давно по всем новостям радостно встречалось появление такого функционала в браузерах. Если пишете низкий уровень самостоятельно — обязательно реализуйте такой подход к событиям, это залог производительности всей системы. Осознание этого факта привело к изучению имеющихся движков для web-приложений. Есть очень хороший проект app.diagrams.net, в концепции сильно похожий на работу с диаграммами в MVC, но лишённый его недостатков с обработкой событий в платформе. Однако к моменту его изучения у меня уже имелся достаточно солидный опыт использования таких движков, в мучительных наблюдениях за тормозами больших диаграмм были получены знания, создавать каждый элемент схемы отдельным объектом — это путь к тормозам.
Дам ещё один ценный совет — обработчики событий создавайте только на канве и нигде больше в пределах схемы. Самый простой пример, когда быстро водим мышкой над большим количеством элементов, у которых есть собственные обработчики событий, то каждый элемент породит одно событие, которое система будет отрабатывать до конца (в MVC с этим ещё хуже, там каждый элемент породит множество событий). Один обработчик событий даёт огромное преимущество в производительности. Чего-то подходящего по моим требованиям из готовых решений не нашлось, зато уже имелось понимание, как работают движки диаграмм изнутри, закостыливание недостатков MVC привело к тому, что у меня уже имелась половина логики необходимой для реализации собственного движка. Менее чем за полгода был написан основной функционал и дальнейшее проектирование схемы было переведено на новый редактор. Теперь у меня свой велик, самый быстрый на районе, хоть и пока лишённый тормозов и некоторых других деталей. И да, это мой первый проект на JavaScript и HTML, большую часть времени пришлось тратить на изучение платформы, но понравилось, теперь есть и другие.
Если задумали реализовать мультиоконность описанным способом, советы будут не лишними, мало создать кучу окон приложения, надо обеспечить производительность каждого из них и всей системы. Вернёмся к коду, часть касается бокового меню и полос прокрутки — у себя сделал так, что главное меню отображается только в первом окне, а полосы прокрутки только в последнем. Основная часть логики — заполнение и использование массива appwindows
с объектами копий приложения, и использование флага is_busy_operations
для предотвращения возникновения коллизий в событиях.
// здесь бэк устанавливает количество дисплеев в сессии при генерации страницы
var window_vertical_count = settings['displays_vertical']; ?>;
var window_horizontal_count = settings['displays_horizontal']; ?>;
var windows_count = window_vertical_count * window_horizontal_count;
var window_vertical_num = 0;
var window_horizontal_num = 0;
var appwindows = new Array();
var mainwindow = null;
var is_main_window = false;
var is_busy_operations = false;
function InitWindow()
{
// первый экземпляр приложения не содержит дефиса в имени окна,
// дочерние содержат номер по вертикали и горизонтали через дефис
let window_name_split = window.name.split('-');
if (window_name_split.length > 1)
{
window_vertical_num = parseInt(window_name_split[0]);
window_horizontal_num = parseInt(window_name_split[1]);
}
// если это первый экземпляр приложения
if (!window_vertical_num && !window_horizontal_num)
{
mainwindow = window;
is_main_window = true;
// флаг выполнения операции приложением, когда установлен, следует игнорировать возникающее событие
is_busy_operations = true;
let scr_x = 50;
let scr_y = 50;
for (let i = 0; i < window_vertical_count; i++)
{
appwindows[i] = new Array();
let y_offset = i * window.screen.height;
for (let j = 0; j < window_horizontal_count; j++)
{
if (i || j)
{
appwindows[i][j] = window.open("about:blank", i + "-" + j, "width=800,height=600,left=" + ((j + 1) * scr_x) + ",top=" + ((i + 1) * scr_y));
appwindows[i][j].document.open();
appwindows[i][j].document.write("" + document.documentElement.innerHTML + "");
appwindows[i][j].document.close();
appwindows[i][j].mainwindow = window;
let x_offset = 0;
if (j)
x_offset = window.screen.width - 180 + window.screen.width * (j - 1);
appwindows[i][j].is_busy_operations = true;
appwindows[i][j].document.documentElement.scrollLeft = x_offset;
appwindows[i][j].document.documentElement.scrollTop = y_offset;
if (i < window_vertical_count - 1 || j < window_horizontal_count - 1)
{
let doc_style = appwindows[i][j].document.getElementsByTagName('head')[0].getElementsByTagName('style')[0];
if (doc_style)
doc_style.innerHTML += 'html { overflow: scroll; scrollbar-width: none; -ms-overflow-style: none; }';
}
}
}
}
appwindows[0][0] = window;
is_busy_operations = false;
if (windows_count > 1)
{
let doc_style = document.getElementsByTagName('head')[0].getElementsByTagName('style')[0];
if (doc_style)
doc_style.innerHTML += 'html { overflow: scroll; scrollbar-width: none; -ms-overflow-style: none; }';
}
}
else
// дочерние окна
{
// прячутся панели и рулетка
document.getElementById("ruler-main-block").hidden = true;
document.getElementById("sidenav-main-menu").hidden = true;
if (window_horizontal_num)
{
document.getElementById("sidenav-main-block").hidden = true;
document.getElementById("ruler-left-panel").hidden = true;
document.getElementById("main-schema-block").style.setProperty('margin-left', '0');
}
if (window_vertical_num)
{
document.getElementById("ruler-top-panel").hidden = true;
document.getElementById("main-schema-block").style.setProperty('top', '0');
}
else
document.getElementById("ruler-top-panel").style.setProperty('margin-left', '0');
}
}
// Обработчик событий
function HandleEvents()
{
window.addEventListener('scroll', OnScroll);
function OnScroll(evt)
{
if (windows_count > 1)
{
// если событие прокрутки вызвано пользователем
if (!is_busy_operations)
{
for (let i = 0; i < window_vertical_count; i++)
{
let y_offset = mainwindow.screen.height * (i - window_vertical_num);
for (let j = 0; j < window_horizontal_count; j++)
{
if (i != window_vertical_num || j != window_horizontal_num)
{
let x_offset = mainwindow.screen.width * (j - window_horizontal_num);
// на всех экранах слева делается отступ под главное меню, на остальных схема на весь экран
if (!window_horizontal_num)
{
if (j)
x_offset -= 180;
}
else if (!j)
x_offset += 180;
mainwindow.appwindows[i][j].is_busy_operations = true;
mainwindow.appwindows[i][j].document.documentElement.scrollLeft = document.documentElement.scrollLeft + x_offset;
mainwindow.appwindows[i][j].document.documentElement.scrollTop = document.documentElement.scrollTop + y_offset;
}
}
}
}
// если событие прокрутки вызвано приложением
else
is_busy_operations = false;
}
}
}
InitWindow();
HandleEvents();
Вот таким хитрым, но не трудоёмким образом можно сделать приложение солидного уровня при внешнем взгляде. Отсутствие необходимости использовать профессиональное оборудование позволяет иметь конкурентную стоимость проекта.