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

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

Скриншаринг вообще очень востребованная штука, особенно в современных реалиях, когда на пике моды разные курсы саморазвития. Хочешь похудеть, потолстеть, заработать миллионы, сменить профессию, правильно воспитывать детей, достичь просветления, затемнения или убираться по Фен-Шую? В сети есть нужный тебе наставник и даже не один. Главный инструмент таких тренеров - проведение вебинаров, чаще всего платных. И если браться разрабатывать свою систему вебинаров, то сразу появляется несколько вопросов:

  1. Где разместить?

  2. Как реализовать?

  3. Как ограничить доступ?

Мы решили собрать все ответы воедино, так получилась эта статья.

Как выбирать сервер и где размещать?

Для запуска стриминга в эксплуатацию нужно оценить масштабы системы.

  • число пользователей стримеров;

  • число входящих стримов;

  • число пользователей подписчиков;

  • число исходящих стримов;

  • параметры видео стримов;

  • временные периоды;

  • география размещения серверов и подписчиков.

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

Примерные требования по характеристикам сервера для типовых задач указаны ниже:

Количество подписчиков

CPUs

RAM, GB

Трафик, TB

Пример использования

до 200

4

8

5

Система видеонаблюдения

до 500

8

16

6

Вебинары

до 1000

16

64

9

Видеочат

до 2000

20

96

10

Стриминг HD видео

Если в вашем проекте планируется большее число подписчиков, не имеет смысла увеличивать технические характеристики одного сервера т.к. это приведет к появлению единой точки отказа. В этом случае можно использовать технологии CDN из расчета 1 Edge сервер на 2000 подписчиков. Благодаря масштабированию, географическому и логическому разделению (с выделением функций транскодинга и доставки контента) можно гибко определить необходимый уровень производительности для каждого из серверов. Нагрузка на системные ресурсы сервера не должна превышать 80%. В этом случае, все подписчики получат видеопотоки с приемлемым качеством.

Достаточно часто CDN разворачивают и при небольшом числе пользователей, если планируется значительное количество трафика. Арифметика простая: 1 стрим это примерно 1 Mbps трафика, соответственно 1000 стримов - 1000 Mbps. Не всегда есть техническая и финансовая возможность подключить к серверу канал 10Gbps. Большая часть интернет провайдеров не могут предоставить канал более 400 Mbps, поэтому, чтобы транслировать потоки со скоростью 1000 Mbps понадобится три сервера.

Рассмотрим некоторые настройки WCS, от которых напрямую зависит конечное число зрителей и качество трансляции.

Настройка портов

По умолчанию, для WebRTC подключений доступно только 499 портов. Это ограничение задается в файле flashphoner.properties. При необходимости можно увеличить диапазон портов в строках

media_port_from = 31001
media_port_to = 32000

Внимательный читатель, конечно, возразит, что в указанном диапазоне 999 портов. Да, так и есть, но для передачи медиатрафика используются только четные порты. Поэтому по умолчанию доступно всего 499 соединений по WebRTC.

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

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

Настройка сборщика мусора

При стриминге в памяти создается и уничтожается много объектов с данными. Эти объекты хранятся в Java heap, если не рассматривать случай транскодинга, при котором картинки хранятся в физической памяти сервера.

Heap (куча) – основной сегмент памяти, где Java VM хранит все ваши объекты. Когда вы хотите создать новый объект, но места в heap уже нет, JVM проводит garbage collection (сборку мусора), что значит, что JVM ищет в памяти все объекты, которые более не нужны, и избавляется от них. В момент работы garbage collection работа остальных процессов в JVM приостанавливается. Поэтому, для задач стриминга, крайне важно, чтобы эта пауза занимала как можно меньше времени. Для этого в Java был разработан новый сборщик мусора — "Z Garbage Collector" (ZGC), позволяющий обеспечить низкую задержку при сборке мусора, не останавливая выполнения потоков приложения более чем на 10 миллисекунд, даже при работе с очень большой memory heap.

Рекомендуется выделять под Java memory heap не менее, чем 1/2 физической памяти сервера. Например, если объем оперативной памяти сервера составляет 32 Гб, рекомендуется выделить 16 Гб. Размер Java memory heap указывается в файле wcs-core.properties в строчках

### JVM OPTIONS ###
-Xmx1024M

Значение по умолчанию - 1024 Мб. Что бы выделить под Java memory heap 16 ГБ укажите

-Xmx16g
-Xms16g

Установим Z Garbage Collector в составе OpenJDK 12.

1.Скачиваем последнюю сборку OpenJDK 12 со страницы http://jdk.java.net/12/:

wget https://download.java.net/java/GA/jdk12.0.2/e482c34c86bd4bf8b56c0b35558996b9/10/GPL/openjdk-12.0.2_linux-x64_bin.tar.gz

2.Распаковываемый полученный файл и перемещаем его содержимое в рабочую директорию:

tar xvf openjdk-12.0.2_linux-x64_bin.tar.gz
mv jdk-12.0.2 /usr/java/jdk-12.0.2

3.Создаем символические ссылки на OpenJDK 12:

ln -sfn /usr/java/jdk-12.0.2 /usr/java/default
ln -sfn /usr/java/default/bin/java /usr/bin/java
ln -sfn /usr/java/default/bin/jstack /usr/bin/jstack
ln -sfn /usr/java/default/bin/jcmd /usr/bin/jcmd
ln -sfn /usr/java/default/bin/jmap /usr/bin/jmap

4.Если WCS был установлен ранее, комментируем или удаляем следующие строки в файле wcs-core.properties:

-XX:+UseConcMarkSweepGC
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70
-XX:+PrintGCDateStamps
-XX:+PrintGCDetails

и заменяем строку

-Xloggc:/usr/local/FlashphonerWebCallServer/logs/gc-core-

на

5.Добавляем настройки для ZGC и логирования:

# ZGC
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC -Xms24g -Xmx24g -XX:+UseLargePages -XX:ZPath=/hugepages
 
# Log
-Xlog:gc*:/usr/local/FlashphonerWebCallServer/logs/gc-core.log
-XX:ErrorFile=/usr/local/FlashphonerWebCallServer/logs/error%p.log

Обычно этих настроек хватает для улучшения производительности сервера и подключения большого количества зрителей.

Итак, мы определились с выбором сервера или виртуального инстанса для WCS. Теперь переходим непосредственно к написанию кода функции скриншаринга для последующего добавления на сайт.

Программируем

Подготовка к разработке

Архитектура решения для этой статьи:

  1. Фронтенд Web сервер - организует интерфейс со стримером и зрителями.

  2. WCS - обрабатывает входящий видеопоток от стримера и отдает его для воспроизведения зрителям.

Начнем развертывание с установки WCS. Следуем указаниям инструкции или запускаем виртуальный инстанс на Amazon, Google Cloud или DigitalOcean. WCS так же можно запустить как контейнер в Docker. Выполняем настройку сервера согласно предыдущей главе и рекомендациям инструкции по подготовке к промышленной эксплуатации.

Теперь запускаем web сервер. В этой статье мы используем Nginx на CentOS 7. На Apache все будет конфигурироваться сходным образом. Если у вас уже есть сайт, к которому планируется добавить функционал скриншаринга, тогда готовить web сервер отдельно не нужно. Или можно развернуть web сервер на той же машине, что и WCS.

На web сервере создаем два файла: страницу будущего интерфейса скриншаринга и скрипт, который будет реализовывать работу. У нас это файлы — "screen-sharing-min.html" и "screen-sharing-min.js". Рядом с этими файлами нужно разместить скрипт основного API - "flashphoner.js", который можно скачать здесь. Или укажите путь до этого файла, если работы проводятся на той же машине, где установлен WCS.

Код

Начнем с подготовки HTML страницы со всеми нужными элементами. Подключаем скрипты:

<script type="text/javascript" src="flashphoner.js"></script> 
<script type="text/javascript" src="screen-sharing-min.js"></script> 

На загрузку тела страницы навешиваем функцию запуска API:

<body onload="init_api()">

И добавляем div-элемент для превью потока скриншаринга и кнопку для запуска:

<div id="screen-sharing" style="width:320px;height:240px;border: solid 1px"></div>
<input type="button" onclick="connect()" value="Share Screen"/>

Кода HTML страницы совсем немного:

<!DOCTYPE html>
<html lang="en">
<head>
   <script type="text/javascript" src="flashphoner.js"></script>
   <script type="text/javascript" src="screen-sharing-min.js"></script>
</head>
<body onload="init_api()">
    <div id="screen-sharing" style="width:320px;height:240px;border: solid 1px"></div>
    <input type="button" onclick="connect()" value="Share Screen"/>
</body>
</html>

Теперь переходим к созданию JS скрипта для работы скриншаринга.

Функция "init_api()" которая вызывается при загрузке HTML страницы инициализирует основной API

function init_api() {
    Flashphoner.init({});
}

При нажатии на кнопку "Share Screen" будут последовательно выполняться две функции нашего скрипта — функция "connect()", которая будет устанавливать подключение к WCS по WebSocket

function connect() {
    session = Flashphoner.createSession({
        urlServer: "wss://demo.flashphoner.com"
    }).on(SESSION_STATUS.ESTABLISHED, function(session) {
        startStreaming(session);
    });
}

и функция "startStreaming()", которая формирует и публикует поток скриншаринга на WCS

function startStreaming(session) {
    var constraints = {
        video: {}
    };
    constraints.video.type = "screen";
    constraints.video.withoutExtension = true;
    session.createStream({
        name: "mystream",
        display: document.getElementById("screensharing"),
        constraints: constraints
    }).publish();
}

Важное отличие от обычного стриминга с веб камеры – чтобы захватить экран, а не камеру, в constraints нужно явно указать два параметра:

constraints.video.type = "screen";
constraints.video.withoutExtension = true;

Файл скрипта тоже достаточно компактный. Всего 34 строчки с комментариями.

//Status constants
var SESSION_STATUS = Flashphoner.constants.SESSION_STATUS;
var STREAM_STATUS = Flashphoner.constants.STREAM_STATUS;
 
//Websocket session 
var session;
 
//Init Flashphoner API on page load
function init_api() {
    Flashphoner.init({});
}
 
//Connect to WCS server over websockets
function connect() {
    session = Flashphoner.createSession({
        urlServer: "wss://demo.flashphoner.com"
    }).on(SESSION_STATUS.ESTABLISHED, function(session) {
        startStreaming(session);
    });
}

//Publishing Share Screen
function startStreaming(session) {
    var constraints = {
        video: {}
    };
    constraints.video.type = "screen";
    constraints.video.withoutExtension = true;
    session.createStream({
        name: "mystream",
        display: document.getElementById("screensharing"),
        constraints: constraints
    }).publish();
}

Сохраняем, запускаем и проверяем работу.

Тестируем

Нажимаем на кнопку «Share Screen» и сообщаем браузеру, что именно будем шарить: весь экран, приложение или определенную вкладку браузера. Превью потока скриншаринга будет отображаться в div-элементе на HTML странице.

В коде скрипта мы жестко задали имя публикуемого потока - "mystream"

session.createStream({
        name: "mystream",

Этот поток в дальнейшем может быть может быть записан, воспроизведен, транскодирован или ретранслирован по любой из технологий, поддерживаемых WCS

Итак, все отлично работает. Теперь предлагаю рассмотреть, как можно реализовать простую меру защиты вашего контента — доступ к стриму с использованием Basic Auth.

Защищаем

Настраиваем web сервер на использование Basic Auth.

Создаем файл .htpasswd:

sudo yum install apache2-utils
sudo htpasswd -c /etc/nginx/conf.d/.htpasswd admin

Первая команда уставит в системе утилиту для генерации файла .htpasswd. Вторая команда создаст файл по указанному пути и запросит пароль для пользователя "admin". Ключ "-c" нужен только при первичной генерации файла. Для записи в файл .htpasswd других учетных записей он не нужен.

Далее переходим к конфигам Nginx. Мы не выделяли настройки сайта в отдельный конфиг, поэтому делаем настройки в основном конфигурационном файле /etc/nginx/nginx.conf

В секции http добавляем следующие строчки:

auth_basic "Enter password";
auth_basic_user_file /etc/nginx/.htpasswd;

Первая строка – строка приветствия, вторая путь к файлу .htpasswd

На этом этапе можно перезапустить Nginx и посмотреть, заработал ли запрос пароля при открытии web интерфейса.

Далее настраиваем web server на работу по протоколу HTTPS. Дописываем следующие строчки в секцию "Server" в файле nginx.conf

listen 443 ssl;
    ssl_certificate /etc/pki/tls/yourdomain/yourdomain.crt;
    ssl_certificate_key /etc/pki/tls/yourdomain/yourdomain.key;
    server_name wcs.yourdomain.com;
    server_tokens off;
    client_max_body_size 500m;
    proxy_read_timeout 10m;

Настраиваем реверсивное проксирование на WebSocket порт WCS

location /wss {
    proxy_set_header Host $host;
    proxy_pass http://IP:8080;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 86400;
}

Еще раз перезапустите Nginx. Теперь немного модифицируем код, который мы написали и протестировали в предыдущей главе, чтобы передавать логин и пароль. В параметре "urlServer" функции "Connect" указываем обращение к серверу в виде "wss://login:password@wcs.yourdomain.com:443/wss"

function connect() {
    session = Flashphoner.createSession({
        urlServer: "wss://login:password@wcs.yourdomain.com:443/wss"
    }).on(SESSION_STATUS.ESTABLISHED, function(session) {
        startStreaming(session);
    });
}

Теперь при обращении к HTML странице "screen-sharing-min.html" браузер будет запрашивать логин и пароль. И дальнейший доступ по WebSocket протоколу к WCS серверу также будет использовать логин и пароль, который мы задали в коде.

Таким образом мы развернули и протестировали пример скриншаринга и прикрутили к нему минимальную авторизацию по технологии Basic Auth. Для чего в 2021 году не нужно создавать и регистрировать свое расширение для Google Chrome и утруждать конечных пользователей установкой каких-то дополнительных компонентов — все работает "из коробки".

Удачного скриншаринга!

Ссылки

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

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

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

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

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

Описание файла настроек flashphoner.properties

Описание файла настроек wcs-core.properties

Документация по управлению памятью в Java

Рекомендации по тонкой настройке сервера

Документация по подготовке к промышленной эксплуатации WCS

Screen Sharing в браузере по WebRTC

Документация по Screen Sharing Web SDK