Концепт бюджетной видеостены неограниченного размера для 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 = <?php echo $schema->settings['displays_vertical']; ?>; var window_horizontal_count = <?php echo $schema->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("<!DOCTYPE html><html>" + document.documentElement.innerHTML + "</html>");           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();

Вот таким хитрым, но не трудоёмким образом можно сделать приложение солидного уровня при внешнем взгляде. Отсутствие необходимости использовать профессиональное оборудование позволяет иметь конкурентную стоимость проекта.


ссылка на оригинал статьи https://habr.com/ru/articles/796335/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *