Вот уже не первый месяц все сидят дома. На фоне этого на первый план выходит коммуникация между людьми. Как-то надо звонить друг другу и общаться. Для одних это развлечение и тоска по близким, а для других это бизнес.
В один из томных вечеров мне позвонил приятель, который, еще задолго до всех карантинных событий, открыл школу обучения программированию для детей и подростков. Естественно, что, подчиняясь сложившимся обстоятельствам, ему пришлось перенести занятия в on-line. В качестве основного инструмента была выбрана известная платформа, которая обещала бесплатно организовать видеоконференции до 1000 участников. Но, видимо из-за большого наплыва желающих воспользоваться этой платформой, качество работы оставляло желать лучшего. Многие ученики стали отказываться от занятий. Перед моим приятелем сейчас стояла задача — сохранить прежний объем учащихся. Одним из вариантов решения которой он видел разработку собственной системы для проведения занятий.
Прежде чем изобретать велосипед, давайте рассмотрим причины, по которым не хотелось бы использовать популярные платформы для проведения конференций. Ведь проведение группового on-line урока и есть многоточечная конференция во всей своей красе.
С большинством недостатков популярных платформ столкнулись родители детей, которых перевели на домашнее обучение в середине учебного года. Платформа, которую «рекомендовали» к использованию для общеобразовательных школ, попросту не справилась с нагрузкой.
Те несколько учеников, которые успевали первыми подключиться к уроку, еще видели и слышали учителя: у остальных же или была картинка без звука, или звук без картинки, или совсем не получалось подключиться. Да и тем «счастливчикам», что подключились удачно, приходилось переживать, что бы не было сбоя, потому что успешно переподключиться потом было практически невозможно. Естественно, что в таких условиях нельзя говорить о хороших результатах обучения.
Итак, к минусам платформ для проведения видеоконференций можно отнести:
- низкое качество связи;
- не всегда удобный интерфейс;
- низкая безопасность передаваемых данных;
- зависимость от сторонних серверов и каналов связи;
- высокая стоимость решения для организаций
Конечному пользователю совсем не хочется заморачиваться с тонкими настройками, длительными авторизациями и т.п. Ему хочется нажать красивую кнопку и что бы все работало. Так же пользователь не собирается ежегодно менять железо, ему нужно, что бы все работало без лишних усилий на компьютере 3-5 летней давности.
Задача
Разработать веб приложение, с помощью которого будут проводиться курсы по обучению детей основам программирования. Преподаватель должен иметь возможность не только объяснять материал по видеосвязи, но и показать свой рабочий стол. Каждый урок записывается и доступен для скачивания на портале школы.
Приложение должно корректно работать на компьютерах со скромными техническими характеристиками и средним каналом доступа к интернету, в браузере, без установки дополнительного программного обеспечения или плагинов.
Выбор решения
Можно выделить четыре типа архитектуры систем многоточечных видеоконференций.
- SFU (Selective Forwarding Unit) — Видеоконференция на основе простого перенаправления потоков;
- Simulcast — Видеоконференция на основе параллельной передачи потоков;
- SVC (Scalable Video Coding) — Видеоконференция на основе масштабируемого видеокодирования;
- MCU (Multipoint Control Unit) — Видеоконференция на основе микширования потоков.
Видеоконференция на основе простого перенаправления потоков (SFU) — это классическая видеоконференция, которая работает по следующему принципу:
- Каждый подключившийся пользователь публикует свой видеопоток на сервер.
- Сервер создает копии потоков без перекодирования и отправляет их другим участникам «as is».
Таким образом, в видеоконференции из пяти участников, каждому из участников придется воспроизводить по четыре видеопотока с аудио и транслировать свой поток для остальных участников. Формула количества потоков в этом случае простая:
N участников = N-1 входящих видеопотоков + 1 исходящий видеопоток
И если пользователь получает 4 потока по 1 Mbps, то суммарный битрейт составляет 4 Mbps и это уже ощутимая нагрузка на сеть и ресурсы CPU и RAM пользовательского компьютера.
Конечно, это никуда не годится и не подходит под условия нашей задачи.
Видеоконференция на основе параллельной передачи потоков (Simulcast) — технология, которую можно считать надстройкой над SFU. Simulcast позволяет не просто перенаправлять потоки, а перенаправлять их умно, потоки с высоким разрешением — тем, у кого хорошая связь, а потоки с низким разрешением — тем, у кого плохая.
Работает следующим образом:
- Каждый подключившийся пользователь публикует 3-5 видеопотоков в разном разрешении и качестве на сервер.
- Сервер выбирает копии потоков с нужными для каждого получателя характеристиками и без перекодирования отправляет их другим участникам.
Количество входящих и исходящих потоков можно описать формулой:
N участников = N-1 входящих видеопотоков + 3 исходящих видеопотоков
Этот вариант не подходит под условия нашей задачи, потому что вся нагрузка, связанная с поддержкой нескольких уровней качества видеопотоков, ложится на компьютеры пользователей. И такая система выглядит избыточной — ведь передаваемые потоки, по сути, одинаковые, просто с разным качеством.
Суть видеоконференции на основе масштабируемого видеокодирования (SVC) в том, что видеопоток со стороны пользователя сжимается слоями. каждый дополнительный слой повышает разрешение видео, качество и fps. При стабильном широком канале связи между пользователем и сервером видеоконференции пользователь отправляет на сервер видеопоток с максимальным количеством таких слоев. Сервер, после получения видеопотока со слоями просто отсекает лишние слои по определенному алгоритму и отправляет другим пользователям количество слоев потока в зависимости от ширины канала.
Количество входящих и исходящих потоков можно описать формулой:
N участников = N-1 входящих видеопотоков + 1 исходящий видеопоток
Преимущество этой технологии в том, что нарезкой слоев занимается сам видеокодек.
Минус технологии в том, что она не поддерживается в браузере из коробки и требует установки дополнительного программного обеспечения, что не подходит под условия задачи.
Видеоконференция на основе микшера реального времени (MCU)
Ключевое отличие MCU от других типов видеоконференций в количестве получаемых каждым участником видеопотоков.
Количество входящих и исходящих потоков можно описать формулой:
N участников = 1 входящий видеопоток + 1 исходящий видеопоток
В случае MCU, каждый участник получает только 1 поток-мозаику, собранную из потоков других участников, с фиксированным битрейтом, который зависит от выходного разрешения микшера, например 720p 2Mbps. И воспроизводит в одном <video> элементе на web странице. Микширование производится на стороне сервера и тратит ресурсы CPU и RAM сервера, а не пользователя.
Преимущество MCU очень хорошо видно на мобильных платформах. Чтобы проиграть несколько потоков, мобильному устройству пришлось бы принять их все по сети, корректно обработать, декодировать и отобразить на экране. А в случае с одним потоком MCU смартфон будет его воспроизводить практически не напрягаясь.
Из недостатков — необходимо будет предусмотреть серверные мощности для работы микшера реального времени в составе MCU.
Реализация
Итак, разберем шаги реализации нашей задачи.
- Устанавливаем и настраиваем сервер, который будет бэкендом и фронтенд web сервер.
- На web-сервере создаем страницу для конференции
- Пишем скрипт,который будет управлять функционалом конференции.
- Проверяем работу конференции.
В качестве фронтенда будет использоваться web сервер, на котором «крутится» сайт школы.
Предвижу вопрос: А не повлияет развертывание многопользовательской конференции на основном веб сервере на работу сайта? Не приведет ли это к подвисаниям сайта из-за возросшей нагрузки?
Переживать насчет увеличения нагрузки на web сервер не стоит, потому что вся основная работа по организации конференции будет проводиться на стороне бэкенда. Фронт нужен только для организации интерфейса с пользователем.
В этой статье не будем останавливаться на развертывании web сервера, для человека «в теме» это тривиальная задача, да и в сети достаточно мануалов. Например — LAMP под Ubuntu
Бэкендом будет Flashphoner Web Call Server 5 (далее по тексту «WCS»). В WCS реализованы два варианта конференций — видеоконференция на основе мультиплексирования (SFU) и видеоконференция на основе микширования потоков (MCU). Выше мы уже определились с технологией, по которой будет работать наша будущая конференция — это технология MCU. MCU в составе WCS реализована на основе микшера реального времени, который объединяет видео в одну картинку, а аудио микширует индивидуально для каждого участника.
Каждый участник MCU отправляет на WCS видео+ аудио потоки. WCS отдает участникам MCU микшированный поток который содержит видео всех участников и аудио всех, кроме собственного.
Для быстрого развертывания своего WCS воспользуйтесь этой инструкцией или запустите один из виртуальных инстансов на Amazon, DigitalOcean или в Docker. После развертывания сервера укажите следующие настройки в файле конфигурации flashphoner.properties для активации функционала MCU на основе микшера реального времени.
mixer_auto_start=true mixer_mcu_audio=true mixer_mcu_video=true
Запись потока микшера включается в том же файле с помощью следующей настройки:
record_mixer_streams=true
Благодаря этой настройке каждая конференция будет автоматически записываться в .mp4 файлы на сервере WCS. Эти файлы можно найти по следующему пути:
/usr/local/FlashphonerWebCallServer/client/records
Итак, с серверами разобрались. Теперь переходим к программированию. В статье мы рассмотрим только «скелет» многоточечной конференции с функцией скриншаринга и записью. В дальнейшем на этот «скелет» можно навесить и нужный интерфейс, и дополнительные функции: такие как снятие снапшотов, добавление водяного знака и пр.
На web сервере создаем два файла: страницу будущего интерфейса конференции и скрипт, который будет управлять работой нашей MCU.
У нас это файлы — «mcu-min.html» и «mcu-min.js».
На HTML странице разместим простой div блок с рамкой в котором будет отображаться видео всех участников многопользовательской конференции:
<div id="remoteVideo" class="display" style="width:640px;height:480px;border:solid 1px"></div>
В хедере станицы пропишем стили для класса «display». Это нужно для дальнейшего корректного отображения видео в div-элементе:
<style> .display { width: 100%; height: 100%; display: inline-block; } .display > video, object { width: 100%; height: 100%; } </style>
Добавим поле для ввода имени пользователя и кнопку для подключения к конференции:
<input id="login" type="text" placeholder="Login"/> <button id="joinBtn">Join</button>
Для правильной работы нашей MCU на HTML странице еще потребуется невидимый div элемент для вывода превью локального видеопотока пользователя:
<div id="localDisplay" style="display: none"></div>
Полный листинг кода HTML страницы:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <script type="text/javascript" src="https://flashphoner.com/downloads/builds/flashphoner_client/wcs_api-2.0/current/flashphoner.js"></script> <script type="text/javascript" src="mcu-min.js"></script> <style> .display { width: 100%; height: 100%; display: inline-block; } .display > video, object { width: 100%; height: 100%; } </style> </head> <body onload="init_page()"> <div id="localDisplay" style="display: none"></div> <div id="remoteVideo" class="display" style="width:640px;height:480px;border:solid 1px"></div> <br> <input id="login" type="text" placeholder="Login"/> <button id="joinBtn">Join</button> </body> </html>
Затем переходим к самому интересному. Будем оживлять нашу HTML страницу.
Открываем для редактирования файл «mcu-min.js»
При загрузке HTML страницы мы инициализируем основной API, навешиваем на нажатие HTML кнопки «Join» функцию «joinBtnClick()» и создаем подключение к WCS по WebSocket-ам. При копировании кода не забудьте заменить «demo.flashphoner.com» на адрес своего WCS.
function init_page() { Flashphoner.init({}); joinBtn.onclick = joinBtnClick; var remoteVideo = document.getElementById("remoteVideo"); var localDisplay = document.getElementById("localDisplay"); session = Flashphoner.createSession({ urlServer: "wss://demo.flashphoner.com" }).on(SESSION_STATUS.ESTABLISHED, function(session) {}); }
Функция «joinBtnClick()» запускает публикацию локального видеопотока на WCS и вызывает следующую по цепочке функцию «playStream()».
При создании и публикации локального видеопотока передаются следующие параметры:
- streamName — имя потока, публикуемого участником конференции (в данном случае login + «# room1», где login — имя участника, которое было указано на HTML странице. )
- display — невидимый div-элемент для отображения превью локального видеопотока, который мы создали в HTML файле (localDisplay)
- constraints — параметры наличия воспроизведения аудио и видео.
function joinBtnClick() { var login = document.getElementById("login").value; var streamName = login + "#room1"; var constraints = { audio: true, video: true }; publishStream = session.createStream({ name: streamName, display: localDisplay, receiveVideo: false, receiveAudio: false, constraints: constraints, }).on(STREAM_STATUS.PUBLISHING, function(publishStream) { playStream(session); }) publishStream.publish(); }
И, наконец, последняя в цепочке функция — «playStream()». Эта функция запускает на HTML странице воспроизведение аудио-видео потока нашей многопользовательской конференции. В качестве параметров передаются:
- name — имя микшера, который будет воспроизводиться для участника (в данном случае room1);
- display — div-элемент, в котором будет отображаться видео (remoteVideo);
- constraints — параметры наличия воспроизведения аудио и видео.
function playStream(session) { var constraints = { audio: true, video: true }; conferenceStream = session.createStream({ name: "room1", display: remoteVideo, constraints: constraints, }).on(STREAM_STATUS.PLAYING, function (stream) {}) conferenceStream.play(); }
Полный листинг js скрипта.
var SESSION_STATUS = Flashphoner.constants.SESSION_STATUS; var STREAM_STATUS = Flashphoner.constants.STREAM_STATUS; var session; var conferenceStream; var publishStream; function init_page() { Flashphoner.init({}); joinBtn.onclick = joinBtnClick; shareBtn.onclick = startShareScreen; var remoteVideo = document.getElementById("remoteVideo"); var localDisplay = document.getElementById("localDisplay"); session = Flashphoner.createSession({ urlServer: "wss://demo.flashphoner.com" }).on(SESSION_STATUS.ESTABLISHED, function(session) {}); } function joinBtnClick() { var login = document.getElementById("login").value; var streamName = login + "#room1"; var constraints = { audio: true, video: true }; publishStream = session.createStream({ name: streamName, display: localDisplay, receiveVideo: false, receiveAudio: false, constraints: constraints, }).on(STREAM_STATUS.PUBLISHING, function(publishStream) { playStream(session); }) publishStream.publish(); } function playStream() { var constraints = { audio: true, video: true }; conferenceStream = session.createStream({ name: "room1", display: remoteVideo, constraints: constraints, }).on(STREAM_STATUS.PLAYING, function(stream) {}) conferenceStream.play(); }
После завершения всех работ по программированию настал черед проверить, как же это все работает.
Открываем созданную HTML страницу, указываем имя первого участника MCU и нажимаем кнопку «Join».
В новых вкладках браузера повторяем эти действия для второго и третьего пользователя. Вид многопользовательской конференции для трех пользователей на скриншоте ниже.
В результате мы получаем простую многоточечную конференцию, где все участники равнозначны.
Но, в задаче требуется реализовать в конференции функции демонстрации экрана и записи .
Посмотрим, как можно немного модифицировать код, что бы подключить к конференции скриншаринг.
Добавляем на HTML страницу кнопку «Share Screen»
<button id="shareBtn">Share Screen</button>
В файл скрипта «mcu-min.js» добавляем функцию «startShareScreen()», которая будет обрабатывать нажатие на кнопку «Share Screen» и запускать трансляцию потока скриншаринга в MCU
function startShareScreen() { var login = document.getElementById("login").value; var streamName = login + "#room1" + "#desktop"; var constraints = { audio: true, video: { width: 640, height: 480 } }; constraints.video.type = "screen"; constraints.video.withoutExtension = true; publishStream = session.createStream({ name: streamName, display: localDisplay, receiveVideo: false, receiveAudio: false, constraints: constraints, }).on(STREAM_STATUS.PUBLISHING, function(publishStream) {}) publishStream.publish(); }
При создании и публикации потока скриншаринга передаются следующие параметры:
- streamName — имя потока скриншаринга (в данном случае login + «#room1» + «#desktop», где login — имя участника, которое было указано на HTML странице. )
- display — невидимый div-элемент для отображения превью локального видеопотока, который мы создали в HTML файле (localDisplay)
- constraints — параметры наличия воспроизведения аудио и видео.
Чтобы захватить экран, а не камеру, в constraints требуется явно указать два параметра:
constraints.video.type = "screen"; constraints.video.withoutExtension = true;
Полный минимальный JS код для многопользовательской конференции со скриншарингом выглядит так:
var SESSION_STATUS = Flashphoner.constants.SESSION_STATUS; var STREAM_STATUS = Flashphoner.constants.STREAM_STATUS; var session; var conferenceStream; var publishStream; function init_page() { Flashphoner.init({}); joinBtn.onclick = joinBtnClick; shareBtn.onclick = startShareScreen; var remoteVideo = document.getElementById("remoteVideo"); var localDisplay = document.getElementById("localDisplay"); session = Flashphoner.createSession({ urlServer: "wss://demo.flashphoner.com" }).on(SESSION_STATUS.ESTABLISHED, function(session) {}); } function joinBtnClick() { var login = document.getElementById("login").value; var streamName = login + "#room1"; var constraints = { audio: true, video: true }; publishStream = session.createStream({ name: streamName, display: localDisplay, receiveVideo: false, receiveAudio: false, constraints: constraints, }).on(STREAM_STATUS.PUBLISHING, function(publishStream) { playStream(session); }) publishStream.publish(); } function playStream() { var constraints = { audio: true, video: true }; conferenceStream = session.createStream({ name: "room1", display: remoteVideo, constraints: constraints, }).on(STREAM_STATUS.PLAYING, function(stream) {}) conferenceStream.play(); } function startShareScreen() { var login = document.getElementById("login").value; var streamName = login + "#room1" + "#desktop"; var constraints = { audio: true, video: { width: 640, height: 480 } }; constraints.video.type = "screen"; constraints.video.withoutExtension = true; publishStream = session.createStream({ name: streamName, display: localDisplay, receiveVideo: false, receiveAudio: false, constraints: constraints, }).on(STREAM_STATUS.PUBLISHING, function(publishStream) {}) publishStream.publish(); }
Теперь посмотрим, как это работает на практике.
Открываем созданную HTML страницу, указываем имя первого участника MCU и нажимаем кнопку «Join». Повторяем действия еще несколько раз в новых вкладках браузера.
Получаем следующий вид MCU:
Затем, для одного из пользователей нажимаем кнопку «Share Screen» . После нажатия на кнопку «Share Screen» браузер запрашивает, что именно нужно расшарить — весь экран, приложение или определенную вкладку браузера. Для этого тестирования мы выбрали пункт «Application Window» и приложение «VLC media player». Сделайте выбор и нажмите кнопку «Share»
Вид MCU с включенным скриншарингом мелко отображаются потоки участников конференции и крупно — трансляция экрана участника «user1»
Вот таким нехитрым образом, без написания сложных алгоритмов, мы сделали многоточечную аудио-видео конференцию с функцией скриншаринга. Сложные алгоритмы, конечно же используются в работе MCU, но на стороне WCS и работают совершенно незаметно, как для конечного пользователя, так и для разработчика.
В задаче еще упоминается функция записи. Тут все достаточно просто. WCS автоматически формирует файл записи, после чего с этим файлом можно производить все необходимые операции — копировать, перемещать, удалять, воспроизводить или архивировать.
Что бы скачать файл с сервера можно использовать файловые менеджеры (FAR, TotalCommander, WinSCP), команды, встроенные в оболочку Linux системы, и/или добавить на web страницу конференции прямую ссылку для скачивания файла с сервера с помощью браузера.
Скачанный файл можно открыть любым удобным проигрывателем. Мы использовали VLC Media Player. На скриншоте ниже кадр из видеофайла в который был записан поток нашей минимальной многоточечной видеоконференции.
В качестве заключения
У нас JS скрипт занял всего 70 строк и это действительно очень мало.
Мы не используем в примере дополнительных фреймворков и проверок существования данных и/или выполнения условий. Это все безусловно необходимо для конечного продукта, который запускается в Production, но цель этой статьи — разобрать пример минимально необходимого для работы многопользовательской конференции кода и, как видите, цель достигнута – конференция работает.
Хорошего стриминга!