Концепт бюджетной видеостены неограниченного размера для web-приложения

ecd80e3aee42f399f55fc8c2504f9a1c.jpg

Поделюсь идеей создания видеостены из абсолютно любого количества дисплеев при минимальных вложениях в доработку имеющегося 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();

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

© Habrahabr.ru