Во время карантина, который то ослабевает, то активизируется с удвоенной силой, появилось огромное множество новых профессий. Сторисмейкеры, инфопродюсеры, специалисты по запускам инфопродуктов и прочие неведомые звери - все они освоили новую работу, которая смогла прокормить их в пандемию. Нанимаете сторисмейкера, который прошел какие-то курсы (дай бог, нормальные, а не от инфоцыган) - он пилит за вас вовлекающие истории в Инстаграм и ваш курс продается. С контентмейкерами та же история - просто нанимаете такого человека, он вам и шапку профиля везде оформит, и на каком-нибудь Геткурсе уроки упорядочит, и статьи напишет, которые тоже помогут в продвижении курсов. Полный полет фантазии: если вам кажется, что какой-то профессии не существует, значит вы просто мало сидите в интернете. В принципе онлайн-работа популяризировалась и теперь уже вряд ли кто-то скажет: "Да он фигней какой-то занимается - ТикТоки свои снимает, а лучше бы на заводе вкалывал!".

Массовый переход в онлайн породил КУЧУ новых профессий и тем самым дал полный полет фантазии всем, кто хотел заниматься креативом...

Екатерина. Специалист по развитию онлайн-школ.

И всем этим новорожденным мейкерам, продюсерам, тиктокерам и стримерам конечно же нужны инструменты для работы. И главный из них (ну, после электричества, конечно) - Интернет.

Статистика использования Интернета звучит как прогноз погоды: "За последний год объемы трафика возросли до 50% в общем по всему миру и до 70 % в отдельных странах. Местами до 9 терабайт в секунду"

9 терабайт в секунду! Ничего удивительного, что многие развлекательные и стриминговые сервисы были вынуждены искусственно ограничивать трафик, что привело к снижению качества картинки. Пользоваться в таких условиях некоторыми популярными приложениями стало не совсем удобно, и многие стартапы и компании разных размеров, деятельность которых так или иначе связана с общением через сеть, задумались о разработке своих инструментов.

Прошлогодние выпускники моего универа устроили на Твиче стрим, когда защищали дипломы. Наверное, так делать нельзя, но было весело...

Олег. Выпускник СПБГУ 2021

Защита выпускной квалификационной работы, как известно, конфиденциальный процесс, поэтому проводить ее в социальной сети пришлось только лишь из-за отсутствия альтернативы. Например, уже пару лет нам обещают разработать государственную систему для проведения видеоконференций. "Powered by Госуслуги" так сказать...

Ко мне обратился мой хороший знакомый, который является владельцем школы по обучению программированию для детей и подростков. Естественно, что, подчиняясь сложившимся обстоятельствам, ему пришлось перенести занятия в on-line. И теперь перед ним стояла очень важная задача - сохранить прежний объем учащихся. Одним из вариантов решения которой он, как и многие, видел разработку собственной системы для проведения занятий.

Прежде, чем изобретать велосипед, давайте рассмотрим причины, по которым не хотелось бы использовать популярные платформы для проведения конференций. Ведь проведение группового on-line урока и есть многоточечная конференция во всей своей красе.

К минусам платформ для проведения видеоконференций и/или дистанционного обучения можно отнести:

  • Низкое качество связи. Здесь без комментариев. Многие ненавидят дистанционку как раз из-за низкого качества картинки/звука и как следствие не очень качественной подачи материала.

  • Не всегда удобный интерфейс. Избыточность или недостаток дополнительных функций. Замечали, что в какой-то программе вы никогда не нажимали на вон ту красную кнопку? Или вам не хватало функции вставки видео с котиком в один клик? Вот это оно.

  • Низкая безопасность передаваемых данных. Ваш видеопоток проходит через один или несколько чужих серверов. Можно ли быть полностью уверенным, что его не перехватят по дороге? И даже если вы не транслируете ничего сверхсекретного, все равно неприятно.

  • Зависимость от сторонних серверов и каналов связи. Если сервер решит отдохнуть, вместе с ним вынужденно отдохнут все его пользователи. И это не всегда хорошо.

  • Высокая стоимость решения для организаций. Здесь так же, как с тарифами на интернет: если ты юрлицо, плати в несколько раз больше!

Сочетание этих факторов в разных комбинациях и приводит к решению создавать свою платформу, да и вообще, хочется уже свою программу с играми и переменами учителями и учениками.

Краткое ТЗ

Разработать веб приложение, с помощью которого будут проводиться курсы по обучению детей основам программирования. Преподаватель должен иметь возможность не только объяснять материал по видеосвязи, но и показать свой рабочий стол. Каждый урок записывается и доступен для скачивания на портале школы.

Приложение должно корректно работать на компьютерах со скромными техническими характеристиками и средним каналом доступа к интернету, в браузере, без установки дополнительного программного обеспечения или плагинов.

Выбор решения

Можно выделить четыре типа архитектуры систем многоточечных видеоконференций.

  • SFU (Selective Forwarding Unit) - Видеоконференция на основе простого перенаправления потоков;

  • Simulcast - Видеоконференция на основе параллельной передачи потоков;

  • SVC (Scalable Video Coding) - Видеоконференция на основе масштабируемого видеокодирования;

  • MCU (Multipoint Control Unit) - Видеоконференция на основе микширования потоков.

Видеоконференция на основе простого перенаправления потоков (SFU) - это классическая видеоконференция, которая работает по следующему принципу:

  • Каждый подключившийся пользователь публикует свой видеопоток на сервер.

  • Сервер создает копии потоков без перекодирования и отправляет их другим участникам "as is".

Таким образом, в видеоконференции из пяти участников, каждому из участников придется воспроизводить по четыре видеопотока с аудио и транслировать свой поток для остальных участников. Формула количества потоков в этом случае простая:

1 участник = N-1 входящих видеопотоков + 1 исходящий видеопоток

И если пользователь получает 4 потока по 1 Mbps, то суммарный битрейт составляет 4 Mbps и это уже ощутимая нагрузка на сеть и ресурсы CPU и RAM пользовательского компьютера.

Конечно, это никуда не годится и не подходит под условия нашей задачи.

Видеоконференция на основе параллельной передачи потоков (Simulcast) - технология, которую можно считать надстройкой над SFU. Simulcast позволяет не просто перенаправлять потоки, а перенаправлять их умно, потоки с высоким разрешением - тем, у кого хорошая связь, а потоки с низким разрешением - тем, у кого плохая.

Работает следующим образом:

  1. Каждый подключившийся пользователь публикует 3-5 видеопотоков в разном разрешении и качестве на сервер.

  2. Сервер выбирает копии потоков с нужными для каждого получателя характеристиками и без перекодирования отправляет их другим участникам.

Количество входящих и исходящих потоков можно описать формулой:

1 участник = N-1 входящих видеопотоков + 3 исходящих видеопотоков

Этот вариант не подходит под условия нашей задачи, потому что вся нагрузка, связанная с поддержкой нескольких уровней качества видеопотоков, ложится на компьютеры пользователей. И такая система выглядит избыточной - ведь передаваемые потоки, по сути, одинаковые, просто с разным качеством.

Суть видеоконференции на основе масштабируемого видеокодирования (SVC) в том, что видеопоток со стороны пользователя сжимается слоями. каждый дополнительный слой повышает разрешение видео, качество и fps. При стабильном широком канале связи между пользователем и сервером видеоконференции пользователь отправляет на сервер видеопоток с максимальным количеством таких слоев. Сервер, после получения видеопотока со слоями просто отсекает лишние слои по определенному алгоритму и отправляет другим пользователям количество слоев потока в зависимости от ширины канала.

Количество входящих и исходящих потоков можно описать формулой:

1 участник = N-1 входящих видеопотоков + 1 исходящий видеопоток

Преимущество этой технологии в том, что нарезкой слоев занимается сам видеокодек.

Минус технологии в том, что она не поддерживается в браузере из коробки и требует установки дополнительного программного обеспечения, что не подходит под условия задачи.

Видеоконференция на основе микшера реального времени (MCU)

Ключевое отличие MCU от других типов видеоконференций в количестве получаемых каждым участником видеопотоков.

Количество входящих и исходящих потоков можно описать формулой:

1 участник = 1 входящий видеопоток + 1 исходящий видеопоток

В случае MCU, каждый участник получает только 1 поток-мозаику, собранную из потоков других участников, с фиксированным битрейтом, который зависит от выходного разрешения микшера, например 720p 2Mbps. И воспроизводит в одном элементе на web странице. Микширование производится на стороне сервера и тратит ресурсы CPU и RAM сервера, а не пользователя.

Преимущество MCU очень хорошо видно на мобильных платформах. Чтобы проиграть несколько потоков, мобильному устройству пришлось бы принять их все по сети, корректно обработать, декодировать и отобразить на экране. А в случае с одним потоком MCU смартфон будет его воспроизводить практически не напрягаясь.

Из недостатков - необходимо будет предусмотреть серверные мощности для работы микшера реального времени в составе MCU.

Разработка

Итак, разберем шаги реализации нашей задачи.

  1. Устанавливаем и настраиваем сервер, который будет бэкендом и фронтенд web сервер.

  2. На web-сервере создаем страницу для конференции

  3. Пишем скрипт,который будет управлять функционалом конференции.

  4. Проверяем работу конференции.

В качестве фронтенда будет использоваться 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, но цель этой статьи - разобрать пример минимально необходимого для работы многопользовательской конференции кода и, как видите, цель достигнута – конференция работает.

Хорошего стриминга!

Ссылки

Наш демо сервер

WCS на Amazon EC2 - Быстрое развертывание WCS на базе Amazon

WCS на DigitalOcean - Быстрое развертывание WCS на базе DigitalOcean

WCS в Docker - Запуск WCS как Docker контейнера

Многоточечная видео конференция (MCU) -Функции сервера для реализации микшера реального времени

Микширование видеопотоков - Функции сервера для реализации микширования потоков

Трансляция экрана по WebRTC - Функция демонстрации и трансляции экрана из браузеров

Запись WebRTC видеопотока - Функция сервера для реализации записи видеопотоков для скачивания и дальнейшей обработки

Документация по быстрому развертыванию и тестированию WCS сервера

Документация по функциям микшера реального времени с функцией MCU