Русский
Русский
English
Статистика
Реклама

Webrtc

Перевод Создание видеочата с помощью Node.js Socket.io WebRTC

06.04.2021 18:11:06 | Автор: admin

Сегодня, специально к старту нового потока по веб-разработке, поделимся с вами туториалом, из которого вы узнаете, как создать видеочат с помощью JavaScript и NodeJS. Также вы научитесь использовать PeerJS, WebRTC и Socket.io.


Здесь вы можете увидеть живой пример приложения, которое вы будете создавать.

Подготовка к проекту

Вот что вам понадобится:

  • NodeJS: посетите официальный веб-сайт Node.js, чтобы загрузить и установить Node;

  • NPM: программа NPM устанавливается на ваш компьютер при установке Node.js.

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

Весь код этого проекта можно найти в репозитории GitHub.

  1. Создайте пустой каталог с именем video-chat-app.

  2. Откройте консоль, перейдите в наш новый каталог и запустите npm init.

  3. Заполните необходимую информацию для инициализации нашего проекта.

  4. Запустите npm install express ejs socket.io uuid peer. Команда установит все зависимости, необходимые для создания этого приложения.

  5. А также в качестве dev-зависимости установите Nodemon. Нужно выполнить npm install-dev nodemon. Это установит nodemon как dev-зависимость.

  6. Создайте файл server.js в этом файле будет храниться вся ваша серверная логика.

Теперь, когда у вас настроен наш проект, вы можете приступить к созданию приложения!

Создание сервера (с Express JS)

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

Давайте создадим шаблонный файл начального приложения Express.

// server.jsconst express = require(express);const app = express();const server = require(http).Server(app);app.get(/, (req, res) => {    res.status(200).send(Hello World);});server.listen(3030);

Теперь ваш сервер запущен, вы можете протестировать его, запустив:

> nodemon server.js

Теперь откройте свой браузер и перейдите по адресу: localhost:3000, вы должны увидеть Hello World.

Создание первой страницы

Вместо того чтобы выводить текст, когда кто-то посещает ваш корневой маршрут, вы хотели бы присылать HTML. Для этого нужно использовать EJS (встроенный JavaScript). EJS это язык шаблонов.

Чтобы использовать EJS в Express, вам нужно настроить ваш шаблонизатор. Для настройки добавьте эту строку кода в файл server.js.

app.set(view engine, ejs)

Доступ к EJS по умолчанию осуществляется в каталоге views. Теперь создайте новую папку views в каталоге. В этой папке добавьте файл с именем room.ejs. Пока что думайте о нашем файле room.ejs как о HTML-файле.

Вот как выглядит ваша файловая структура:

|-- video-chat-app   |-- views      |-- room.ejs   |-- package.json   |-- server.js

Теперь добавьте HTML-код в файл room.ejs.

Как только вы скопируете приведённый выше код, нужно немного поменять app.js:

app.get(/, function (req, res) { // OLD CODE res.status(200).send("Hello World");})

Выше приведён старый код, в котором вы отправляете клиенту текст Hello World!. Вместо этого вы хотите отправить файл room.ejs:

app.get(/, function (req, res) { // NEW CODE res.render(room);})

Теперь откройте браузер и перейдите по адресу: localhost:3030, и вы увидите, что отображается файл room.ejs!

Добавление CSS

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

Нам нужно будет добавить новую папку в проект под названием public. В этой папке создайте файлы style.css и script.js. Вот ваша новая файловая структура:

|-- weather-app   |-- views      |-- index.ejs   |-- public      |-- style.css      |-- script.js   |-- package.json   |-- server.js

Express не даёт доступа к этому файлу по умолчанию, поэтому вам нужно открыть его с помощью следующей строки кода:

app.use(express.static(public));

Этот код позволяет вам получить доступ ко всем статическим файлам в папке public. Наконец, вам нужен CSS. Поскольку это не курс по CSS, я не буду вдаваться в подробности, но если вы хотите использовать мои стили, вы можете скопировать их отсюда.

После того как вы добавили CSS, вы можете посетить: localhost:3030. Вы заметите, что приложение выглядит немного лучше.

Настройка комнат

К настоящему моменту ваш файл server.js должен выглядеть так:

У вас есть один GET-роут и запуск сервера. Однако, чтобы ваше приложение работало, нужно всякий раз, когда новый пользователь посещает ваш роут по умолчанию, перенаправлять его на уникальный URL-адрес. Следует использовать библиотеку uuid для создания случайного уникального URL-адреса для каждой комнаты.

UUID это библиотека javascript, которая позволяет вам создавать уникальные идентификаторы. В вашем приложении вы будете использовать uuid версии 4 для создания уникального URL. Но сначала импортируйте uuid в server.js.

const { v4: uuidv4 } = require("uuid");

Теперь нужно использовать uuid для создания случайного уникального идентификатора для каждой комнаты и перенаправлять пользователя в эту комнату.

app.get(/, (req, res) => {    res.redirect(`/${uuidv4()}`);});

И, прежде чем вы протестируете это, я также хотел добавить страницу для каждой уникальной комнаты, и вы передадите текущий URL этой странице.

app.get(/:room, (req, res) => {    res.render(room, { roomId: req.param.room });});

Вы передали roomId в room.ejs на этом закончили настройку ваших комнат. А теперь, если вы посетите localhost:3030, вы будете перенаправлены на уникальный URL.

Добавление видео пользователя

Вы будете работать с файлом script.js, который вы создали ранее. script.js будет содержать весь клиентский код приложения.

Итак, вот что необходимо сделать: нужно получить видеопоток, а затем добавить этот поток в элемент видео.

let myVideoStream;const videoGrid = document.getElementById("video-grid");const myVideo = document.createElement("video");myVideo.muted = true;navigator.mediaDevices.getUserMedia({    audio: true,    video: true,}).then((stream) => {    myVideoStream = stream;    addVideoStream(myVideo, stream);});

Теперь создайтем функцию addVideoStream, которая добавит поток к видеоэлементу.

const addVideoStream = (video, stream) => {    video.srcObject = stream;    video.addEventListener("loadedmetadata", () => {       video.play();       videoGrid.append(video);    });};

Этот код добавит пользовательский поток к видеоэлементу. Вы можете проверить это, посетив localhost:3030, и вы увидите всплывающее окно с видео

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

Пришло время использовать Socket.io и PeerJS. Для тех, кто не знает, Socket.io позволяет взаимодействовать серверу и клиенту в режиме реального времени. PeerJS позволяют реализовать WebRTC.

Сначала импортируйте socket.io и peerjs в server.js и прослушайте событие соединения.

// server.jsconst express = require(express);const app = express();const server = require(http).Server(app);const { v4: uuidv4 } = require(uuid);app.set(view engine, ejs);const io = require(socket.io)(server);const { ExpressPeerServer } = require(peer);const peerServer = ExpressPeerServer(server, {    debug: true,});app.use(/peerjs, peerServer);app.use(express.static(public));app.get(/, (req, res) => {    res.redirect(`/${uuidv4()}`);});app.get(/:room, (req, res) => {    res.render(room, { roomId: req.param.room });});io.on(connection, (socket) => {    socket.on(join-room, (roomId, userId) => {    socket.join(roomId);    socket.to(roomId).broadcast.emit(user-connected, userId);    });});server.listen(3030);

Теперь ваш сервер прослушивает событие присоединения к комнате. Далее настройте ваш script.js.

// public/script.jsconst socket = io(/);const videoGrid = document.getElementById(video-grid);const myVideo = document.createElement(video);myVideo.muted = true;var peer = new Peer(undefined, {  path: /peerjs,  host: /,  port: 3030,});let myVideoStream;navigator.mediaDevices  .getUserMedia({  audio: true,  video: true,}).then((stream) => {  myVideoStream = stream;  addVideoStream(myVideo, stream);  peer.on(call, (call) => {    call.answer(stream);    const video = document.createElement(video);    call.on(stream, (userVideoStream) => {      addVideoStream(video, userVideoStream);      });    });  socket.on(user-connected, (userId) => {      connectToNewUser(userId, stream);  });});const connectToNewUser = (userId, stream) => {  const call = peer.call(userId, stream);  const video = document.createElement(video);  call.on(stream, (userVideoStream) => {      addVideoStream(video, userVideoStream);  });};peer.on(open, (id) => {    socket.emit(join-room, ROOM_ID, id);});const addVideoStream = (video, stream) => {  video.srcObject = stream;  video.addEventListener(loadedmetadata, () => {    video.play();    videoGrid.append(video);  });};

Теперь, если в комнату войдёт новый пользователь, вы увидите его видео.

Создание пользовательского интерфейса

С видеочастью закончили. А теперь займитесь стилизацией. Но сначала добавьте контент в файл room.ejs. (Добавьте CDN font-awesome внутри тега head.)

// views/room.ejs<body>  <div class="header">    <div class="logo">      <h3>Video Chat</h2>    </div>  </div>  <div class="main">    <div class="main__left">      <div class="videos__group">        <div id="video-grid"></div>      </div>      <div class="options">        <div class="options__left">          <div class="options__button">            <i class="fa fa-video-camera" aria-hidden="true"></i>          </div>        <div class="options__button">            <i class="fa fa-microphone" aria-hidden="true"></i>        </div>       </div>       <div class="options__right">         <div class="options__button background__red">           <i class="fa fa-phone" aria-hidden="true"></i>         </div>       </div>     </div>    </div>    <div class="main__right">      <div class="main__chat_window">        <ul class="messages"></ul>     </div>     <div class="main__message_container">       <input id="chat_message" type="text" placeholder="Type message here...">       <div class="options__button">         <i class="fa fa-plus" aria-hidden="true"></i>       </div>     </div>    </div>  </div></body>

Затем откройте файл style.css и добавьте немного CSS.

@import url(http://personeltest.ru/aways/fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap");:root {   main-darklg: #1d2635;   main-dark: #161d29;   primary-color: #2f80ec;   main-light: #eeeeee;  font-family: Poppins, sans-serif;}* {  margin: 0;  padding: 0;}.header {  display: flex;  justify-content: center;  align-items: center;  height: 8vh;  width: 100%;  background-color: var(  main-darklg);}.logo > h3 {    color: var(  main-light);}.main {  overflow: hidden;  height: 92vh;  display: flex;}.main__left {  flex: 0.7;  display: flex;  flex-direction: column;}.videos__group {  flex-grow: 1;  display: flex;  justify-content: center;  align-items: center;  padding: 1rem;  background-color: var(  main-dark);}video {  height: 300px;  border-radius: 1rem;  margin: 0.5rem;  width: 400px;  object-fit: cover;  transform: rotateY(180deg);  -webkit-transform: rotateY(180deg);  -moz-transform: rotateY(180deg);}.options {  padding: 1rem;  display: flex;  background-color: var(  main-darklg);}.options__left {    display: flex;}.options__right {    margin-left: auto;}.options__button {  display: flex;  justify-content: center;  align-items: center;  background-color: var(  primary-color);  height: 50px;  border-radius: 5px;  color: var(  main-light);  font-size: 1.2rem;  width: 50px;  margin: 0 0.5rem;}.background__red {    background-color: #f6484a;}.main__right {flex: 0.3;    background-color: #242f41;}.main__chat_window {    flex-grow: 1;}.main__message_container {  padding: 1rem;  display: flex;  align-items: center;  justify-content: center;}.main__message_container > input {  height: 50px;  flex: 1;  border-radius: 5px;  padding-left: 20px;  border: none;}#video-grid {  display: flex;  justify-content: center;  flex-wrap: wrap;}

Вот и всё! Поздравляем, вы успешно создали видеочат! Теперь вы можете развернуть его на Heroku и показать его всему миру. Демо и исходный код.

Это лишь небольшой пример того, какие вещи может делать веб-разработчик, причем в одиночку. Сделать just for fun экспорт музыки в Spotify и получить известность, пока огромная компания долго думает над решением задачи без проблем. За один вечер набросать и выкатить расширение для браузера, которое упростит жизнь миллионам пользователей тоже по силам. На что еще способна веб-разработка зависит только от фантазии программиста. Приходите учиться, чтобы освоить дзюцу веб-разработки и стать настоящим самураем интернета.

Узнайте, как прокачаться в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Как мы интрегрировали Agora SDK в проект

11.06.2021 18:10:18 | Автор: admin

Всем привет. Меня зовут Дмитрий, и я типичный представитель касты гребцов на галере X. Основной ЯП, который я использую - PHP, но иногда приходится писать на других.

Предыстория

Как-то пришел очередной, немного не доделанный, проект "убийца" продукта Y. Все бы ничего, если бы его не делали изначально индусы. Первоначальный анализ кода и все оценкой фич занимался другой разработчик. В итоге было решено взять наследие индусов, немного подправить и в прод.

Немного подправить и в продакшн, растянулось где-то на пол года. Все уже к этому времени поняли что лучше бы выбрали вариант - переписать заново, но было уже поздно, а отступать назад не хотелось. В итоге проект выехал live, но был чертовски страшный и неудобный со стороны UI/UX. Поэтому начался плавный редизайн всего и вся. Все что описано выше, было до меня, я пришел на проект только через пол года.

Постановка задачи и начальные условия

Новый спринт, новые тикеты. Одна из задач звучит как: "Редизайн текущего механизма видео/аудио звонков". Для данного функционала мы использовали Agora Web SDK 3.4.0v. Почему именно Agora - потому что ее выбрали индусы (скорее всего из-за 10000 бесплатных минут). Возможно еще подкупило то что есть SDK под различные платформы:

Поехали

Первый делом я глянул последнюю версию Web SDK. Как оказалось - уже вышла абсолютно новая мажорная версия Agora Web SDK 4.x. Ну, если мы все равно полностью редизайним - то почему бы и не взять новую версию и использовать ее. Все равно будет полный прогон со стороны QA, в добавок - поменялся сам флоу созвона. Сказано - сделано, только насторожила запись:

Вроде разговор идет только про несовместимость Web SDK (у нас еще используется React Native SDK для мобильных устройств), но осадок остался.

На новый дизайн и сервер ушло где-то 3 - 4 дня (не люблю верстать, но что поделать). Настало время самого интересного - запуск процесса интервью. В итоге была взята Agora Web SDK 4.4.0. В течение следующего дня получилось сделать всю JS часть для созвона по видео и ауди (со всеми плюшками). За основу был взят пример из их же гитхаба: https://github.com/AgoraIO/API-Examples-Web/blob/main/Demo/basicVideoCall/basicVideoCall.js (если что, то в архиве с самой либой лежат похожие примеры интеграции)

Еще через день все было уже на testing энвайронменте. Первые пол дня тестов прошли хорошо, на web портале все более-менее работало. Настало время проверки звонков с разных платформ (один человек с мобильного девайса - второй с сайта).

Первый звоночек

При звонке с мобилы на web, и наоборот - звонок не устанавливается. Проверили на проде тот же кейс - все огонь. Значит что-то пошло не так. Так как код на мобильных девайсах не менялся вообще (на их стороне изначально подумали над дизайном и все продумали) - значит проблема на моей стороне. Первое действие - нужно подписаться на все события от SDK что доступны - https://docs.agora.io/en/Voice/API%20Reference/web_ng/interfaces/iagorartcclient.html и смотреть что, где всплывает. Каково же было мое удивление, когда я увидел пустоту в консоле хромиума. Да это же не может быть, что бы Agora Web SDK 4.4.0 была не совместима с Agora React Native API 3.х!

После многих попыток, хоть как-то это дело завести - пришло "Принятие". Что поделать, придется брать все же Agora Web SDK 3.x.

Новый день начинается со скачивания последней версии (в рамках 3.x) и переделкой существующего функционала под другой SDK. К счастью, дело пошло быстро и за первую половину дня - все было готово. На локальной машине все работало отлично. Решил протестировать с коллегой по офису (он должен открыть ветку с прода и попробовать созвонится). И хоп, мы получаем ту же проблему - звонок не устанавливается, но что обнадеживает - в консоле проскакивают логи, что оппонент по звонку выходит из румы (в терминах агоры - это channel). Ну хоть какой прогресс, по сравнению с 4.x.

После первых двух часов дебага - было решено взять код с прода и попробовать его запустить. К черту текущее решение, просто берем код с прода, вcтавляем в HTML, пробрасываем пару конфигов и запускаем. О чудо, все работает. Все друг друга видят и слышат. Первая хорошая новость за день. Значит минорные версии 3.x совместимы между собой (это вселяет надежду с мобилами). Быстро подчищаем код с HTML и переносим его в JS модули. Запускаем и получаем дырку от бублика. Да что ж за день то сегодня такой. Откатываемся назад на вариант с кодом в HTML - работает. Ладно, теперь это уже личное...

Переносим "строчку за строчкой" из HTML в JS модули и почти каждый раз проверяем локально и с коллегой. Хм... Почему же оно все еще работает? Когда была перенесена последняя строчка, я очень удивился. Код был почти один-в-один как после миграции на 3.x, который я получил пол дня назад и он РАБОТАЛ. А давай-ка я попробую запустить старый свой вариант на 3.x. Оп-па не работает. Истина где-то рядом. Как хорошо что есть гит и можно сравнить. Отбросив различия в кодстайле я был очень удивлен увиденным:

Да не может же быть, что бы я так лоханулся. Быстро открываем документацию по либе (uid используется для подключения к руме). Нас интересует метод join:

Все! Я больше не могу. Поеду я домой, рабочий день уже давно закончен.

Второй звоночек

Новый день - новые силы. Как оказалось - на проде используется number, потому что со стороны мобил (с их слов) было жесткое требование на int и ничего более. Успокаиваемся и работаем... Проверяем локально, потом с коллегой, все хорошо - едем на testing. Проверяем web - хорошо, мобилы - черный экран у того кто остался на вебе, но звук работает отлично. Хоть какой прогресс...

После пары часы дебага и проверки различный гипотез, я был готов сдаться, но здесь произошло чудо. Разраб со стороны мобил говорит что у него все работает. После того, как мы созвонились, через один из мессенджеров, и расшарили экран - я действительно убедился, все условия соблюдены и оно действительно работает. Начали искать отличия. Мы с коллегой в офисе. QA на уделенке дома. Разработчик мобильной версии в Латвии. Просим QA пере-проверить кейс со звонком на разных платформа, только пусть звонит на мне в офис, а кому-то вне офиса. О-да, работает... Мы где-то близко...

Услышав наши "горячие" споры в комнате офиса, глава департамента встает и говорит: "Парни, а вы используете STUN / TURN сервера?". Такие слова я слышал впервые, поэтому пришлось гуглить: https://medium.com/nuances-of-programming/webrtc-%D1%84%D1%80%D0%B5%D0%B9%D0%BC%D0%B2%D0%BE%D1%80%D0%BA-ice-stun-%D0%B8-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80%D0%B0-turn-f835b11d9dde

В общем, все сошлись на том что для текущей версии проекта - никто не будет пока подключать STUN / TURN сервера (ибо если бесплатные STUN еще можно найти, то бесплатных TURN нет).

Почему не догадались, что из-за NAT в офисе - ловим проблемы? Да потому что звук работал. И видео на одной стороне работало отлично. А как раз черный экран в видео мы уже получали, когда ловили кейсы, где один клиент инициировал созвон по связке rtc/vp8, а второй live/h264.

Вот так и закончилось мое увлекательное приключение в мир WebRTC.

PS. Если кто-то меня попросит еще раз сделать звонки на проекта - я первым делом пойду искать другую библиотеку.

Подробнее..

История одной интеграции Agora SDK

11.06.2021 20:19:02 | Автор: admin

Всем привет. Меня зовут Дмитрий, и я типичный представитель касты гребцов на галере X. Основной ЯП, который я использую - PHP, но иногда приходится писать на других.

Предыстория

Как-то пришел очередной, немного не доделанный, проект "убийца" продукта Y. Все бы ничего, если бы его не делали изначально индусы. Первоначальный анализ кода и все оценкой фич занимался другой разработчик. В итоге было решено взять наследие индусов, немного подправить и в прод.

Немного подправить и в продакшн, растянулось где-то на пол года. Все уже к этому времени поняли что лучше бы выбрали вариант - переписать заново, но было уже поздно, а отступать назад не хотелось. В итоге проект выехал live, но был чертовски страшный и неудобный со стороны UI/UX. Поэтому начался плавный редизайн всего и вся. Все что описано выше, было до меня, я пришел на проект только через пол года.

Постановка задачи и начальные условия

Новый спринт, новые тикеты. Одна из задач звучит как: "Редизайн текущего механизма видео/аудио звонков". Для данного функционала мы использовали Agora Web SDK 3.4.0v. Почему именно Agora - потому что ее выбрали индусы (скорее всего из-за 10000 бесплатных минут). Возможно еще подкупило то что есть SDK под различные платформы:

Поехали

Первый делом я глянул последнюю версию Web SDK. Как оказалось - уже вышла абсолютно новая мажорная версия Agora Web SDK 4.x. Ну, если мы все равно полностью редизайним - то почему бы и не взять новую версию и использовать ее. Все равно будет полный прогон со стороны QA, в добавок - поменялся сам флоу созвона. Сказано - сделано, только насторожила запись:

Вроде разговор идет только про несовместимость Web SDK (у нас еще используется React Native SDK для мобильных устройств), но осадок остался.

На новый дизайн и сервер ушло где-то 3 - 4 дня (не люблю верстать, но что поделать). Настало время самого интересного - запуск процесса интервью. В итоге была взята Agora Web SDK 4.4.0. В течение следующего дня получилось сделать всю JS часть для созвона по видео и ауди (со всеми плюшками). За основу был взят пример из их же гитхаба: https://github.com/AgoraIO/API-Examples-Web/blob/main/Demo/basicVideoCall/basicVideoCall.js (если что, то в архиве с самой либой лежат похожие примеры интеграции)

Еще через день все было уже на testing энвайронменте. Первые пол дня тестов прошли хорошо, на web портале все более-менее работало. Настало время проверки звонков с разных платформ (один человек с мобильного девайса - второй с сайта).

Первый звоночек

При звонке с мобилы на web, и наоборот - звонок не устанавливается. Проверили на проде тот же кейс - все огонь. Значит что-то пошло не так. Так как код на мобильных девайсах не менялся вообще (на их стороне изначально подумали над дизайном и все продумали) - значит проблема на моей стороне. Первое действие - нужно подписаться на все события от SDK что доступны - https://docs.agora.io/en/Voice/API%20Reference/web_ng/interfaces/iagorartcclient.html и смотреть что, где всплывает. Каково же было мое удивление, когда я увидел пустоту в консоле хромиума. Да это же не может быть, что бы Agora Web SDK 4.4.0 была не совместима с Agora React Native API 3.х!

После многих попыток, хоть как-то это дело завести - пришло "Принятие". Что поделать, придется брать все же Agora Web SDK 3.x.

Новый день начинается со скачивания последней версии (в рамках 3.x) и переделкой существующего функционала под другой SDK. К счастью, дело пошло быстро и за первую половину дня - все было готово. На локальной машине все работало отлично. Решил протестировать с коллегой по офису (он должен открыть ветку с прода и попробовать созвонится). И хоп, мы получаем ту же проблему - звонок не устанавливается, но что обнадеживает - в консоле проскакивают логи, что оппонент по звонку выходит из румы (в терминах агоры - это channel). Ну хоть какой прогресс, по сравнению с 4.x.

После первых двух часов дебага - было решено взять код с прода и попробовать его запустить. К черту текущее решение, просто берем код с прода, вcтавляем в HTML, пробрасываем пару конфигов и запускаем. О чудо, все работает. Все друг друга видят и слышат. Первая хорошая новость за день. Значит минорные версии 3.x совместимы между собой (это вселяет надежду с мобилами). Быстро подчищаем код с HTML и переносим его в JS модули. Запускаем и получаем дырку от бублика. Да что ж за день то сегодня такой. Откатываемся назад на вариант с кодом в HTML - работает. Ладно, теперь это уже личное...

Переносим "строчку за строчкой" из HTML в JS модули и почти каждый раз проверяем локально и с коллегой. Хм... Почему же оно все еще работает? Когда была перенесена последняя строчка, я очень удивился. Код был почти один-в-один как после миграции на 3.x, который я получил пол дня назад и он РАБОТАЛ. А давай-ка я попробую запустить старый свой вариант на 3.x. Оп-па не работает. Истина где-то рядом. Как хорошо что есть гит и можно сравнить. Отбросив различия в кодстайле я был очень удивлен увиденным:

Да не может же быть, что бы я так лоханулся. Быстро открываем документацию по либе (uid используется для подключения к руме). Нас интересует метод join:

Все! Я больше не могу. Поеду я домой, рабочий день уже давно закончен.

Второй звоночек

Новый день - новые силы. Как оказалось - на проде используется number, потому что со стороны мобил (с их слов) было жесткое требование на int и ничего более. Успокаиваемся и работаем... Проверяем локально, потом с коллегой, все хорошо - едем на testing. Проверяем web - хорошо, мобилы - черный экран у того кто остался на вебе, но звук работает отлично. Хоть какой прогресс...

После пары часы дебага и проверки различный гипотез, я был готов сдаться, но здесь произошло чудо. Разраб со стороны мобил говорит что у него все работает. После того, как мы созвонились, через один из мессенджеров, и расшарили экран - я действительно убедился, все условия соблюдены и оно действительно работает. Начали искать отличия. Мы с коллегой в офисе. QA на уделенке дома. Разработчик мобильной версии в Латвии. Просим QA пере-проверить кейс со звонком на разных платформа, только пусть звонит на мне в офис, а кому-то вне офиса. О-да, работает... Мы где-то близко...

Услышав наши "горячие" споры в комнате офиса, глава департамента встает и говорит: "Парни, а вы используете STUN / TURN сервера?". Такие слова я слышал впервые, поэтому пришлось гуглить: https://medium.com/nuances-of-programming/webrtc-%D1%84%D1%80%D0%B5%D0%B9%D0%BC%D0%B2%D0%BE%D1%80%D0%BA-ice-stun-%D0%B8-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80%D0%B0-turn-f835b11d9dde

В общем, все сошлись на том что для текущей версии проекта - никто не будет пока подключать STUN / TURN сервера (ибо если бесплатные STUN еще можно найти, то бесплатных TURN нет).

Почему не догадались, что из-за NAT в офисе - ловим проблемы? Да потому что звук работал. И видео на одной стороне работало отлично. А как раз черный экран в видео мы уже получали, когда ловили кейсы, где один клиент инициировал созвон по связке rtc/vp8, а второй live/h264.

Вот так и закончилось мое увлекательное приключение в мир WebRTC.

PS. Если кто-то меня попросит еще раз сделать звонки на проекта - я первым делом пойду искать другую библиотеку.

Подробнее..

Web Sip клиент на основе JsSIP FreePBX

21.06.2020 12:22:54 | Автор: admin
У FreePBX есть Web Sip клиент. Мне было интересно как он работает. Начав изучать технологию WebRTC я наткнулся на 3 библиотеки это JsSIP, его fork SIP.js, а также sipml5.

JsSIP и SIP.js не удалось запустить с первого раза, не хватало опыта. А вот sipml5 оказался самым простым в применении. Скачав его тестовую версию, получилось совершить первый звонок.

Все 3 библиотеки работоспособны. У каждой есть свои сильные и слабые стороны. Протестировав все 3 библиотеки, остановил выбор на JsSIP.


Основные плюсы и минусы с которыми я столкнулся:

1. JsSIP
Плюсы:
  • легковесная;
  • активно поддерживается разработчиками.

Минусы:
  • на начальном этапе более сложная для старта;
  • пришлось править исходники чтобы заработал с asterisk.


2. Sip.js
Плюсы:
  • легковесная;
  • упор делается для работы с asterisk.

Минусы:
  • не удалось запустить звук для Early media (не подключается звук для предответных сообщении оператора).


3. Sipml5
Плюсы:
  • относительно простая для новичков.

Минусы:
  • сложный и запутанный код, не было желания разбираться в нем.


Подготовка


Веб клиент работает только на странице с поддержкой шифрования. Нам нужно создать самоподписанный сертификат. Как это делается есть готовые статьи в Интернете.

Firefox разрешает работать с самоподписанными сертификатами. Для работы в Хроме придется делать сначала корневой сертификат, а на его основе уже подписывать сертификат для работы телефона. Корневой сертификат должен быть импортирован в хранилище сертификатов.

Включение поддержки web sip клиента на FreePBX


Администратор -> User Managment -> Вкладка Groups -> Вкладка UCP -> Вкладка Phone
Для свойства Enable Phone выбираем значение Да.

Если у вас в FreePBX были внутренние номера, допустим 200, то при активации web sip клиента появятся дополнительные номера с префиксом 99, т.е. 99200.

На клиентском браузере, после того как дали разрешение на использование самоподписанного сертификата, нужно еще дать разрешение для сертификата asterisk. Для этого наберите в браузере
https://192.168.1.70:8089/ws
IP адрес нужно указать тот на котором запущен asterisk.

Правка исходника JsSIP


В исходниках библиотеки JsSIP необходимо заменить transport=ws на transport=wss. Без этой правки клиент не заработает. В Sip.js править ничего не нужно, там этот момент предусмотрели.

Код web sip клиента на основе JsSIP


Для демонстрации работы клиента скопируйте нижеприведенный код, сохраните и запустите страницу. Страница демонстрирует исходящий звонок. Код 100% работоспособный, проверил прежде чем выложить.

<!DOCTYPE HTML><html><head><script type="text/javascript" src="http://personeltest.ru/aways/habr.com/lib/jquery-3.4.1.js"></script><script type="text/javascript" src="http://personeltest.ru/aways/habr.com/lib/jsSIP/jssip.min.js"></script></head><body><style>#callControl {    display:flex;    flex-direction:column;    justify-content:flex-start;    align-items: center;}#num {    margin-top: 20px;    padding-left: 10px;    font-size: 1em;    font-family:'Open Sans', sans-serif;    width: 300px;    height: 40px;    border:0px;    border-bottom: 1px solid #B8B8B8;}#call {    display:flex;    background-color: #389400;}#hangup {    display:none;    background-color:#A90002;}#call, #hangup {    flex-direction:column;    justify-content:center;    align-items: center;    color: #FFF;    width:260px;    height: 40px;    font-size: 20px;    margin: 20px;    border-radius: 20px;    cursor: pointer;}</style><div id="callControl">  <h2>Web Sip клиент на основе JsSIP</h2>  <div id="to">    <input id="num" type="text" placeholder="Введите номер телефона"/>  </div>  <div id="call">Вызов</div>  <div id="hangup">Завершить</div></div></body><script>JsSIP.debug.enable('JsSIP:*');var socket = new JsSIP.WebSocketInterface('wss://192.168.1.70:8089/ws');var configuration = {sockets  : [ socket ],// внутренний номерuri      : '99XXX@192.168.1.70',// парольpassword : 'passw'};var remoteAudio = new window.Audio();remoteAudio.autoplay = true;var ua = new JsSIP.UA(configuration);// События регистрации клиентаua.on('connected', function(e) { /* Ваш код */ });ua.on('disconnected', function(e) { /* Ваш код */ });ua.on('registered', function(e) { /* Ваш код */ });ua.on('unregistered', function(e) { /* Ваш код */ });ua.on('registrationFailed', function(e) { /* Ваш код */ });// Запускаем ua.start();// Обработка событии исх. звонкаvar eventHandlers = {'progress': function(e) {console.log('call is in progress');session.connection.ontrack = function(e) {console.log(e);remoteAudio.srcObject = e.streams[0];};},'failed': function(e) {console.log('call failed with cause: ' + e.cause);$('#call').css({'display' : 'flex'});$('#hangup').css({'display' : 'none'});},'ended': function(e) {console.log('call ended with cause: ' + e.cause);$('#call').css({'display' : 'flex'});$('#hangup').css({'display' : 'none'});},'confirmed': function(e) {console.log('call confirmed');console.log(e);}};var options = {'eventHandlers'    : eventHandlers,'mediaConstraints' : { 'audio': true, 'video': false }};// Кнопка для звонка$('#call').click(function(e) {session = ua.call($('#num').val(), options);$('#call').css({'display' : 'none'});$('#hangup').css({'display' : 'flex'});});// Кнопка для отбоя звонка$('#hangup').click(function() {if (session) {session.terminate();}$('#call').css({'display' : 'flex'});$('#hangup').css({'display' : 'none'});});</script></html>


Должна получится такая страница

Теперь можно пробовать совершить первый звонок. Если все сделали правильно, то услышите гудок, при ответе должен пойти звук в обе стороны и начаться полноценная беседа.

Итоги


За последние 2 года использования Web sip клиента были проблемы, которые появлялись при обновлении браузера.

Например в 63 версии Firefox пришлось менять SDP протокол, чтобы сессия не отбивалась. Браузер перестал держать связь без параметра a=mid:0. В последних версиях уже не актуально, ошибку исправили, в более поздних версиях уже нет такой проблемы.

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

В основном пользователи работают в Firefox, и по этой причине пришлось в библиотеке убрать повторное согласование SDP протокола. Проблемы с пропаданием звука после ответа перестали беспокоить.

Перспективы


В данный момент на js был написана программа Автообзвон, которая способна по первым нескольким секундам распознавать предответные сообщения оператора такие как: тел. не существует, абон. занят, тел. отключен, тел. доступен. Точность распознавания 95% и выше. Если будет время напишу статью и опишу алгоритм.
Подробнее..
Категории: Javascript , Asterisk , Jssip , Sip.js , Webrtc , Web sip client

Пишем видеочат для локальной сети, или осваиваем WebRTC в 2020 году

04.08.2020 02:04:14 | Автор: admin
На фоне известных событий делать было нечего в рамках профессионального роста пытаюсь освоить WebRTC. Как известно, лучший способ освоения сделать что-нибудь хотя бы потенциально полезное. А заодно и поделиться-обменяться опытом создания и набивания шишек.

В качестве задачи было решено сделать простое приложение, позволяющее проводить аудиовидеозвонки между двумя (пока что) стационарными или мобильными устройствами в локальной сети без необходимости подключаться к Интернету. Установка и первоначальная настройка такого приложения должны быть простыми настолько, чтобы сколь-нибудь продвинутый эникей без проблем с этим справился и показал пользователям, как делать звонки, а при наличии соответствующих навыков мог бы сделать небольшие доработки в части дизайна и возможностей. Клиентом должно выступать любое устройство, оснащённое устройствами ввода-вывода мультимедиа и позволяющее запустить подходящий браузер (Firefox или Chrome тестил на майских, кажется, версиях).


Как это сделано


Как известно, технология WebRTC для связи между двумя абонентами предлагает использовать объект типа RTCPeerConnection, а главной задачей разработчика является организация обмена текстовой информацией (SDP-offer, SDP-answer, ICE-кандидат) между вызывающим и вызываемым абонентами. Другими словами, разработчику нужно сначала сделать текстовый чат с API для браузерного JavaScript и далее прикрутить к нему мультимедиа-часть события RTCPeerConnection и методы передачи и обработки приёма данных.

Выбор технологий для реализации и API текстового чата остаётся за разработчиком. Многие (и в частности Mozilla в своём официальном примере работы RTCPeerConnection) предпочитают использовать WebSocket API и соответствующий сервер например, на Node.JS. Но, учитывая нашу задачу сделать максимально просто для развёртывания, я решил для начала не переусложнять серверное приложение, тем более для доставки клиентскому устройству страниц и скриптов нужен был дополнительно Web-сервер. Поэтому API решил сделать на xmlHttpRequest с периодическими обращениями клиентов к тому же Web-серверу. Не могу сказать, что это работает идеально с точки зрения расхода ресурсов (и батарей) клиентского устройства и отсутствия тормозов, но работает точно, если при разработке учитывать некоторые нюансы. Возможно, в какой-нибудь следующей версии добавлю сервер WebSocket и переделаю соответствующим образом API, но не всё сразу.

Серверную часть было решено сделать на Lazarus под Windows; сетевые возможности обеспечивает пакет Synapse. В чём-то это, наверное, извращение, и пришлось серьёзно повозиться и набить несколько шишек, чтобы заставить всё работать так, как задумано. Но один exe, две dll (библиотеки OpenSSL), файлы самоподписанного SSL-сертификата и ключа к нему и немножко файлов конфигурации (плюс статика) позволяют сильно не заморачиваться с уровнем техники под сервер и способом запуска приложения. Первую версию этого сервера в 32-битном билде я тестил даже на Asus Eee PC 900 2009 года выпуска под Windows XP, хоть и не обошлось без чита в виде недавней замены штатного супермедленного SSD на более современный и объёмный. Это если говорить за производительность. А установка сервера представляет собой распаковку скачанного zip-архива в любую подходящую папку, редактирование JSON-файла конфигурации учётных записей пользователей и запуск exe-файла программы (в окне есть ещё кнопка, но можно указать параметр в командной строке, чтобы Web-сервер запустился сразу). Так или иначе, раздумываю и над более серьёзной серверной частью, благо опыт такой имеется. Всему своё время.

Помимо собственно организации API наш сервер отдаёт статические файлы для браузеров (Web-страницы логина и чата, стили, изображения, скрипты, рингтон). Вообще я старался по возможности обойтись без сторонних библиотек, но из-за того, что с дизайном и html-вёрсткой у меня не ахти, решил всё же воспользоваться jQuery.UI и соответственно jQuery, которые Web-сервер также отдаёт как статику. Все файлы статики лежат в отдельной подпапке папки программы; их, естественно, можно смотреть и даже менять при желании и наличии соответствующих навыков. В JavaScript код прокомментирован, по нему при необходимости можно и учиться.

Как организовать связь


Для организации связи главное подобрать и объединить в общую сеть клиентские устройства (компьютеры, ноуты, смартфоны, планшеты) и Windows-машинку с сервером (она же может выступать и клиентом). Из клиентских устройств я тестил несколько недорогих смартфонов выпуска последних нескольких лет на Android начиная с 7-й версии, а также компьютер и ноут на Windows 10, в т. ч. с двумя подключенными Web-камерами; они показали себя нормально. Первую версию я шутки ради протестил даже на Orange Pi One с Lubuntu (или Kubuntu, не помню уже с ходу) от производителя. На удивление, оно даже работало, хоть видео и тормозило, а страница чата открывалась невесть сколько времени (про загрузку системы и открытие браузера даже говорить не хочется).

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

Работает всё так. Пользователи заходят на серверную машинку браузером по протоколу https, используя её IP-адрес или доменное имя. Там они вводят свои логин-пароль и заходят на страницу чата со списком контактов. При щелчке по контакту открывается окно диалога с историей текстовых сообщений (к слову, сервер хранит её только в ОЗУ, в файл пока не умеет), полем для чата и формой аудиовидеозвонка с флажками для выбора аудио и (или) видео. Для совершения видеозвонка пользователь отмечает соответствующие флажки, нажимает кнопку звонка и подтверждает разрешение браузеру. У вызываемого абонента начинает пищать рингтон и открывается форма ответа с такими же флажками. После щелчка по кнопке ответа браузер также спросит разрешение на доступ к мультимедиа-устройствам. Затем открывается окно звонка.

Не могу сказать, что у меня большой опыт работы с ПО для видеоконференций, видеоконсультаций и т. д., но, например, в Google Hangouts на компьютере (как на мобильных устройствах, не знаю) я не увидел возможности включить себялюбимого на полный экран, что в теории может потребоваться на удалённых консультациях, когда тебе нужно хорошо видеть то, что ты показываешь своему собеседнику (например, через заднюю камеру смартфона). В этом чате в диалоге звонка я решил сделать две вкладки с видео собеседника и самого пользователя. С текущей версии на вкладке пользователя, помимо самого видео, есть поля для выбора камеры и микрофона; с менять их значения можно на лету в ходе беседы. Возможно, это окажется кому-то полезным.

Теперь вкратце опишу набитые при разработке шишки; возможно, это поможет кому-то при разработке и отладке своих решений.

Современные особенности работы и реализации WebRTC и вообще работы с мультимедиа на JavaScript


Здесь кратко; подробности можно посмотреть в комментариях в javascript-файле static/js/videoChat.js

  1. Chrome точно, плюс, возможно, и другие браузеры позволяют работать с getUserMedia только на сайтах, доступных по HTTPS
  2. Перечень устройств ввода аудио и видео можно получить только после успешного вызова getUserMedia
  3. Автоматический запуск проигрывания звука средствами JavaScript (через метод play() html-элемента video или audio) возможен только после того, как пользователь покажет активность на сайте например, щёлкнет по какому-то элементу управления.
  4. Окончание выполнения promise после setLocalDescription у вызывающего абонента нужно придержать хотя бы до того момента, как сервер чата отдаст вызываемому абоненту на обработку отправленный offer. Без этого RTCPeerConnection сразу начнёт отдавать ICE-кандидатов, которых вызываемый абонент до обработки полученного оффера добавить к себе не сможет.
  5. Для переключения устройств ввода на лету перед вызовом getUserMedia необходимо остановить все старые треки на RTCPeerConnection. Без этого, что бы пользователь ни выбрал в качестве устройства ввода, оно выбрано не будет.
  6. Во многих описаниях для мобильных устройств говорится про свойство facingMode для выбора фронтальной или задней камер. На самом деле не знаю, как в старых устройствах, но в этом чате на протестированных смартфонах переключение работает и без использования этого свойства. Но строго с учётом п. 5.


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

Шишка разработчика сетевых приложений на Lazarus


Пакет Synapse на текущий момент поддерживает только библиотеки OpenSSL версий 1.0.x; в 1.1 много чего уже реализовано по-другому, другие даже имена библиотек. Кроме того, просто поместить dll-ки в папку с программой недостаточно. Нужен также файл конфигурации (openssl.cnf), путь к которому задаётся через переменную окружения OPENSSL_CONF.

Где скачать


Дистрибутивы программы под Win32 и Win64 и исходники серверной части на Lazarus доступны на странице программы по ссылке www.lubezniy.ru/soft/videochat

P. S.: Кстати, кто-нибудь знает, каким образом можно с помощью Lazarus автоматизировать сборку из одних и тех же исходников двух разных exe под Win32 и Win64? Кросскомпилятор есть, но менять каждый раз опции в проекте не то чтобы правильно.
Подробнее..

О первоклашках, дистанционке и асинхронном программировании

15.12.2020 10:11:51 | Автор: admin
image

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

Пока я увлеченно занимался веб разработкой, где-то на фоне жена периодически жаловалась на проблемы выбора школы для ребёнка. И тут (вдруг) ребёнок подрос и школьный вопрос встал ребром. Ладно, значит, время пришло. Давайте вместе разберёмся, что же все-таки не так с системой образования в бывшей 1/6 части суши, и что мы с вами можем с этим сделать?

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

Внесу ясность: дистанционное обучение подразумевает занятия в обычной школе с помощью дистанционных образовательных технологий (ДОТ), а семейное означает добровольный уход из школы и обучение только силами семьи (по сути, это старый добрый экстернат). Впрочем, в любом случае ребёнка нужно прикрепить к какой-либо из доступных школ, как минимум, для сдачи промежуточных аттестаций.

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

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

Ближе к делу. Выбираем государственную школу


Пожалуй, семейное образование мне нравится больше из-за возможности выбрать программу и график обучения. Да и физически посещать школу можно реже. Но выбрать государственную школу, поговорить с директором о прикреплении ребёнка и получить приказ о зачислении в первый класс нужно уже в конце зимы, чтобы в сентябре не было сюрпризов. Хотя, с юридической точки зрения, закон об образовании вроде бы не требует ежегодных аттестаций, дедлайны, по моему опыту, отлично мотивируют, поэтому пусть будут аттестации. Вряд ли любая школа примет нас с распростертыми объятьями, но найти достойный вариант в ближайшем городе мы сможем, я уверен.

Выбираем учебную программу


Именно выбираем. Пытаться составить программу самостоятельно, не имея профильного образования, не разумно. Хотя существуют государственные образовательные ресурсы, такие как Российская Электронная Школа (РЭШ) и Московская Электронная Школа (МЭШ), которых в теории могло было бы хватить, но Оба варианта предоставляют планы уроков, видеозаписи, тесты и учебные пособия. Вот чего мне не удалось найти, так это самих учебников, даже по обязательной программе.

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

Выбираем онлайн школу


Мы почти вернулись к тому, с чего начали. Дистанционка? Ладно, присмотримся к ней повнимательней. Как вообще можно организовать учебный процесс удаленно? Тут возникает много вопросов, я подниму только ключевые:

* Живое общение. Что предлагают школы? Скайп, в лучшем случае Тимс. Уроки по Скайпу? Серьёзно? Если я не ошибаюсь, на дворе 2020-й. Открыть перед первоклашкой несколько окон с красивыми разноцветными кнопочками и ждать, что он на них не нажмет, а будет пол-дня послушно слушать скучного дядю или тетю? Ни разу таких детей не видел. А вы?

* Домашка. Точнее, как она попадает к учителю на проверку? На самом деле, это действительно сложный вопрос, возможно, даже не решаемый в принципе. Существующие варианты:

  1. Написать в тетрадке, сфоткать и отправить учителю. Бр-р-р, не хочу заставлять учителей ломать глаза в попытках прочесть мутные фотки с мобильников, сделанные, как правило, по какому-то неписанному закону в темноте.
  2. Отправить скан. Полумера, в общем случае невозможная из-за отсутствия у родителей нужного оборудования.
  3. Оцифровать рукописный ввод с помощью дигитайзера или планшета. Так себе вариант, но об этом чуть позже.
  4. Напечатать текст. В принципе, допустимо, но вот как ребёнок введёт с клавиатуры, например, математическую или химическую формулу? Никак. Плюс, для более продвинутых деток, проблема с плагиатом.
  5. Выполнить онлайн тест. Это, безусловно, самый популярный вариант. Полагаю, большинство школ, включая РЭШ и МЭШ, ориентируются на него. На практике это означает скорее дрессировку, чем обучение. Дети учатся ставить галочки в правильном месте. За бортом остаются предметы, требующие любой формы творчества, например, сочинения, а также диктанты и непопулярное теперь по неведомой мне причине чистописание. Сюда же можно отнести умение отстаивать своё мнение.

* Оценки. Очевидно, выставленные на уроке и при проверке домашних заданий оценки должны попадать в электронный дневник, доступный родителям. И они туда попадают. Вот только не сразу. Я поинтересовался у старших детей, закончивших один из престижных лицеев златоглавой (по иронии судьбы, с информационным уклоном), почему так? Ответ, честно сказать, меня удивил. Оказывается, учителя записывают оценки на бумажку, а после уроков вбивают их в этот самый электронный дневник на государственном портале. И это в то время, как Теслы Илона Маска бороздят просторы космоса

Ладно, пора провести небольшое техническое исследование и проверить, может существуют объективные причины такого положения дел?

Давайте определим требования к гипотетической идеальной платформе для обучения. На самом деле, все просто: дети должны оставаться на уроке, сосредоточившись на том, что говорит и показывает учитель, при необходимости отвечая на вопросы и при желании поднимая руку. По сути, нам нужно окно на полный экран с потоком с учительской камеры, презентацией или интерактивной доской. Самый простой способ добиться этого использовать технологию WebRTC (real-time communications, коммуникации в реальном времени). Эта штука работает в любом более-менее современном браузере, не требует покупки дополнительного оборудования и, к тому же, обеспечивает хорошее качество связи. И да, этот стандарт требует асинхронного программирования как минимум потому, что необходимый JS метод navigator.mediaDevices.getUserMedia() возвращает промис. Вроде все понятно, приступаю к реализации.

Лирическое отступление о выборе фреймворка
В последнее время я все чаше слышу мнение о том, что чистый JavaScript шагнул далеко вперед, проблем с кроссбраузерностью уже нет и фреймворки на фронтэнде не нужны вообще. Особенно часто критике подвергается jQuery. Хорошо, стряхнем пыль со старого доброго JS и сравним:

// Выбрать элементelement = $(selector);element = document.querySelector(selector);// Выбрать элемент внутри элементаelement2 = element.find(selector2);element2 = element.querySelector(selector2);// Скрыть элементelement.hide();  // добавляет стиль display: noneelement.classList.add('hidden');

Тут нужно пояснить, что CSS классу hidden, при желании, можно прописать свойства opacity и transition, что даст эффект fadeIn/fadeOut на чистом CSS. Отлично, давно хотел отказаться от JS анимации!

// Слушать событие onClickelement.click(e => { ... });element.onclick = (e) => { ...  }// Переключить классelement.toggleClass(class_name);element.classList.toggle(class_name);// Создать divdiv = $("<div>");div = document.createElement("div");// Вставить созданный div в element// (это не опечатка, можно писать одинаково)element.append(div);element.append(div);

И т.д и т.п. В целом, код на чистом JS получается немного более многословным, но такова цена повышения производительности и снижения трафика. Нет, я никого не агитирую, но для эксперимента использую нативный JS с удовольствием!

WebRTC предназначен для связи между браузерами напрямую, по технологии точка-точка (p2p). Однако, чтобы установить эту связь, браузеры должны сообщить друг другу о своем намерении общаться. Для этого понадобится сервер сигнализации.

Пример базовой реализации простого видеочата, использующего топологию full mesh
'use strict';(function () {    const selfView = document.querySelector('#self-view'),        remoteMaster = document.querySelector('#remote-master'),        remoteSlaves = document.querySelector('#remote-slaves');    let localStream,        selfStream = null,        socket = null,        selfId = null,        connections = {};    // ***********************    // UserMedia & DOM methods    // ***********************    const init = async () => {        try {            let stream = await navigator.mediaDevices.getUserMedia({                audio: true, video: {                    width: { max: 640 }, height: { max: 480 }                }            });            localStream = stream;            selfStream = new MediaStream();            stream.getVideoTracks().forEach(track => {                selfStream.addTrack(track, stream); // track.kind == 'video'            });            selfView.querySelector('video').srcObject = selfStream;        } catch (e) {            document.querySelector('#self-view').innerHTML =                '<i>Веб камера и микрофон не найдены</i>';            console.error('Local stream not found: ', e);        }        wsInit();    }    const createRemoteView = (id, username) => {        let iDiv = document.querySelector('#pc' + id);        if (!iDiv) {            iDiv = document.createElement('div');            iDiv.className = 'remote-view';            iDiv.id = 'pc' + id;            let iVideo = document.createElement('video');            iVideo.setAttribute('autoplay', 'true');            iVideo.setAttribute('playsinline', 'true');            let iLabel = document.createElement('span');            iDiv.append(iVideo);            iDiv.append(iLabel);            if (!remoteMaster.querySelector('video')) {                remoteMaster.append(iDiv);                iLabel.textContent = 'Ведущий';            } else {                remoteSlaves.append(iDiv);                iLabel.textContent = username;            }            remoteMaster.style.removeProperty('display');        }    }    // *******************************    // Signaling (Web Socket) methods    // *******************************    const wsInit = () => {        socket = new WebSocket(SIGNALING_SERVER_URL);        socket.onopen = function (e) {            log('[socket open] Соединение установлено');        }        socket.onmessage = function (event) {            log('[socket message] Данные получены с сервера', event);            wsHandle(event.data);        }        socket.onclose = function (event) {            if (event.wasClean) {                log('[close] Соединение закрыто чисто, ' +                    `код=${event.code} причина=${event.reason}`);            } else {                log('[socket close] Соединение прервано', event);            }            clearInterval(socket.timer);        }        socket.onerror = function (error) {            logError('[socket error]', error);        }        socket.timer = setInterval(() => {            socket.send('heartbeat');        }, 10000);    }    const wsHandle = async (data) => {        if (!data) {            return;        }        try {            data = JSON.parse(data);        } catch (e) {            return;        }        switch (data.type) {            case 'handshake':                selfId = data.uid;                if (!Object.keys(data.users).length) {                    createRemoteView(selfId, 'Ведущий');                    remoteMaster.querySelector('video').srcObject =                        selfStream;                    selfView.remove();                    break;                } else {                    selfView.style.removeProperty('display');                }                for (let id in data.users) {                    await pcCreate(id, data.users[id]);                }                break;            case 'offer':                await wsHandleOffer(data);                break;            case 'answer':                await wsHandleAnswer(data)                break;            case 'candidate':                await wsHandleICECandidate(data);                break;            default:                break;        }    }    const wsHandleOffer = async (data) => {        let pc = null;        if (!connections[data.src]) {            await pcCreate(data.src, data.username);        }        pc = connections[data.src].pc;        // We need to set the remote description to the received SDP offer        // so that our local WebRTC layer knows how to talk to the caller.        let desc = new RTCSessionDescription(data.sdp);        pc.setRemoteDescription(desc).catch(error => {            logError('handleOffer', error);        });        await pc.setLocalDescription(await pc.createAnswer());        wsSend({            type: 'answer',            target: data.src,            sdp: pc.localDescription        });        connections[data.src].pc = pc; // ???    }    const wsHandleAnswer = async (data) => {        log('*** Call recipient has accepted our call, answer:', data);        let pc = connections[data.src].pc;        // Configure the remote description,        // which is the SDP payload in our 'answer' message.        let desc = new RTCSessionDescription(data.sdp);        await pc.setRemoteDescription(desc).catch((error) => {            logError('handleAnswer', error);        });    }    const wsHandleICECandidate = async (data) => {        let pc = connections[data.src].pc;        let candidate = new RTCIceCandidate(data.candidate);        log('*** Adding received ICE candidate', candidate);        pc.addIceCandidate(candidate).catch(error => {            logError('handleICECandidate', error);        });    }    const wsSend = (data) => {        if (socket.readyState !== WebSocket.OPEN) {            return;        }        socket.send(JSON.stringify(data));    }    // ***********************    // Peer Connection methods    // ***********************    const pcCreate = async (id, username) => {        if (connections[id]) {            return;        }        try {            let pc = new RTCPeerConnection(PC_CONFIG);            pc.onicecandidate = (event) =>                pcOnIceCandidate(event, id);            pc.oniceconnectionstatechange = (event) =>                pcOnIceConnectionStateChange(event, id);            pc.onsignalingstatechange =  (event) =>                pcOnSignalingStateChangeEvent(event, id);            pc.onnegotiationneeded = (event) =>                pcOnNegotiationNeeded(event, id);            pc.ontrack = (event) =>                pcOnTrack(event, id);            connections[id] = {                pc: pc,                username: username            }            if (localStream) {                try {                    localStream.getTracks().forEach(                        (track) => connections[id].pc.addTransceiver(track, {                            streams: [localStream]                        })                    );                } catch (err) {                    logError(err);                }            } else {                // Start negotiation to listen remote stream only                pcOnNegotiationNeeded(null, id);            }            createRemoteView(id, username);        } catch (error) {            logError('Peer: Connection failed', error);        }    }    const pcOnTrack = (event, id) => {        let iVideo = document.querySelector('#pc' + id + ' video');        iVideo.srcObject = event.streams[0];    }    const pcOnIceCandidate = (event, id) => {        let pc = connections[id].pc;        if (event.candidate && pc.remoteDescription) {            log('*** Outgoing ICE candidate: ' + event.candidate);            wsSend({                type: 'candidate',                target: id,                candidate: event.candidate            });        }    }    const pcOnNegotiationNeeded = async (event, id) => {        let pc = connections[id].pc;        try {            const offer = await pc.createOffer();            // If the connection hasn't yet achieved the "stable" state,            // return to the caller. Another negotiationneeded event            // will be fired when the state stabilizes.            if (pc.signalingState != 'stable') {                return;            }            // Establish the offer as the local peer's current            // description.            await pc.setLocalDescription(offer);            // Send the offer to the remote peer.            wsSend({                type: 'offer',                target: id,                sdp: pc.localDescription            });        } catch(err) {            logError('*** The following error occurred while handling' +                ' the negotiationneeded event:', err);        };    }    const pcOnIceConnectionStateChange = (event, id) => {        let pc = connections[id].pc;        switch (pc.iceConnectionState) {            case 'closed':            case 'failed':            case 'disconnected':                pcClose(id);                break;        }    }    const pcOnSignalingStateChangeEvent = (event, id) => {        let pc = connections[id].pc;        log('*** WebRTC signaling state changed to: ' + pc.signalingState);        switch (pc.signalingState) {            case 'closed':                pcClose(id);                break;        }    }    const pcClose = (id) => {        let remoteView = document.querySelector('#pc' + id);        if (connections[id]) {            let pc = connections[id].pc;            pc.close();            delete connections[id];        }        if (remoteView) {            remoteView.remove();        }    }    // *******    // Helpers    // *******    const log = (msg, data) => {        if (!data) {            data = ''        }        console.log(msg, data);    }    const logError = (msg, data) => {        if (!data) {            data = ''        }        console.error(msg, data);    }    init();})();


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

Пример реализации сервера сигнализации
import jsonfrom aiohttp.web import WebSocketResponse, Responsefrom aiohttp import WSMsgTypefrom uuid import uuid1from lib.views import BaseViewclass WebSocket(BaseView):    """ Process WS connections """    async def get(self):        username = self.request['current_user'].firstname or 'Аноним'        room_id = self.request.match_info.get('room_id')        if room_id != 'test_room' and            self.request['current_user'].is_anonymous:            self.raise_error('forbidden')  # @TODO: send 4000        if (self.request.headers.get('connection', '').lower() != 'upgrade' or            self.request.headers.get('upgrade', '').lower() != 'websocket'):            return Response(text=self.request.path)  # ???        self.ws = WebSocketResponse()        await self.ws.prepare(self.request)        self.uid = str(uuid1())        if room_id not in self.request.app['web_sockets']:            self.request.app['web_sockets'][room_id] = {}        self.room = self.request.app['web_sockets'][room_id]        users = {}        for id, data in self.room.items():            users[id] = data['name']        ip = self.request.headers.get(            'X-FORWARDED-FOR',            self.request.headers.get('X-REAL-IP',            self.request.remote))        msg = {            'type': 'handshake',            'uid': str(self.uid),            'users': users, 'ip': ip}        await self.ws.send_str(json.dumps(msg, ensure_ascii=False))        self.room[self.uid] = {'name': username, 'ws': self.ws}        try:            async for msg in self.ws:                if msg.type == WSMsgType.TEXT:                    if msg.data == 'heartbeat':                        print('---heartbeat---')                        continue                    try:                        msg_data = json.loads(msg.data)                        if 'target' not in msg_data or                            msg_data['target'] not in self.room:                            continue                        msg_data['src'] = self.uid                        if 'type' in msg_data and 'target' in msg_data:                            if msg_data['type'] == 'offer':                                msg_data['username'] = username                        else:                            print('INVALID DATA:', msg_data)                    except Exception as e:                        print('INVALID JSON', e, msg)                    try:                        await self.room[msg_data['target']]['ws'].send_json(                            msg_data);                    except Exception as e:                        if 'target' in msg_data:                            self.room.pop(msg_data['target'])        finally:            self.room.pop(self.uid)        return self.ws


Технология WebRTC, кроме видеосвязи, позволяет предоставить браузеру разрешение на захват содержимого дисплея или отдельного приложения, что может оказаться незаменимым при проведении онлайн уроков, вебинаров или презентаций. Отлично, используем.

Я так увлекся современными возможностями видеосвязи, что чуть не забыл о самом главном предмете в классе интерактивной доске. Впрочем, базовая реализация настолько тривиальна, что я не стану загромождать ею эту статью. Просто добавляем canvas, слушаем события перемещения мыши onmousemove (ontouchmove для планшетов) и отправляем полученные координаты всем подключенным точкам через тот же сервер сигнализации.

Тестируем интерактивную доску


Тут понадобится планшет, дигитайзер и живой ребёнок. Заодно проверим возможность оцифровки рукописного ввода.

Для начала я взял старенький планшет Galaxy Tab на андроиде 4.4, самодельный стилус и первые попавшиеся прописи в качестве фона для canvas. Дополнительные программы не устанавливал. Результат меня обескуражил: мой планшет абсолютно не пригоден для письма! То есть водить по нему пальцем без проблем, а вот попасть стилусом в контур буквы, даже такой огромной, как на картинке ниже, уже проблема. Плюс гаджет начинает тупить в процессе рисования, в результате чего линии становятся ломанными. Плюс мне не удалось заставить ребёнка не опирать запястье на экран, отчего под рукой остается дополнительная мазня, а сам планшет начинает тормозить еще больше. Итог: обычный планшет для письма на доске не подходит. Максимум его возможностей двигать пальцем по экрану достаточно крупные фигуры. Но предлагать это школьникам поздновато.

Ладно, у нас ведь чисто теоретическое исследование, верно? Тогда берем дигитайзер (он же графический планшет) Wacom Bamboo формата A8, и наблюдаем за ребёнком.

Замечу, что мой подопытный шести лет от роду получил ноутбук, да еще с графическим пером первый раз в жизни. На получение базовых навыков обращения с пером у нас ушло минут десять, а уже на втором уроке ребёнок пользовался планшетом вполне уверенно, самостоятельно стирал с доски, рисовал рожицы, цветочки, нашу собаку и даже начал тыкать доступные в эпсилон окрестности кнопки, попутно задавая вопросы типа А зачем в школе поднимают руку?. Вот только результат неизменно оставлял желать лучшего. Дело в том, что дизайнеры и художники для прорисовки элемента максимально увеличивают фрагмент изображения, что и делает линии точными. Здесь же мы должны видеть доску целиком, в масштабе 1:1. Тут и взрослый не попадет в линию. Вот что получилось у нас:

image

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

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

Так или иначе, в результате экспериментов я фактически получил MVP минимально жизнеспособный продукт (minimum viable product), почти пригодный для проведения онлайн уроков, с видео/аудио конференцией, общим экраном, интерактивной доской, простым текстовым чатом и кнопкой поднять руку. Это на случай, если у ребёнка вдруг не окажется микрофона. Да, такое бывает, особенно у детей, не выучивших уроки.

Но в этой бочке меда, к сожалению, есть пара ложек дёгтя.

Тестируем WebRTC


Ложка 1. Поскольку наша видеосвязь использует прямые подключения между клиентами, нужно первым делом проверить масштабируемость такого решения. Для теста я взял старенький ноутбук с двухядерным i5-3230M на борту, и начал подключать к нему клиентов с отключенными веб камерами, то есть эмулируя режим один-ко-многим:

image

Как видите, подопытный ноут в состоянии более-менее комфортно вещать пяти клиентам (при загрузке CPU в пределах 60%). И это при условии снижения разрешения исходящего видеопотока до 720p (640x480px) и frame rate до 15 fps. В принципе, не так уж и плохо, но при подключении класса из нескольких десятков учеников от фулл меша придется отказаться в пользу каскадирования, то есть каждый из первых пяти клиентов проксирует поток следующим пяти и так далее.

Ложка 2. Для создания прямого интерактивного подключения (ICE) между клиентами им нужно обойти сетевые экраны и преобразователи NAT. Для этого WebRTC использует сервер STUN, который сообщает клиентам внешние параметры подключения. Считается, что в большинстве случаев этого достаточно. Но мне почти сразу же повезло:

Как видите, отладчик ругается на невозможность ICE соединения и требует подключения TURN сервера, то есть ретранслятора (relay). А это уже ДОРОГО. Простым сервером сигнализации тут не обойтись. Вывод придётся пропускать все потоки через медиа сервер.

Медиа сервер


Для тестирования я использовал aiortc. Интересная разработка, позволяет подключить браузер напрямую к серверу через WebRTC. Отдельная сигнализация не нужна, можно использовать канал данных самого соединения. Это работает, все мои тестовые точки подключились без проблем. Но вот с производительностью беда. Простое эхо видео/аудио потока с теми же ограничениями 720p и 15fps съели 50% моего виртуального CPU на тестовом VDS. Причём, если увеличить нагрузку до 100%, видео поток не успевает выгружаться клиентам и начинает забивать память, что в итоге приводит к остановке сервера. Очевидно, Питон, который мы любим использовать для задач обработки ввода/вывода, не очень подходит для CPU bound. Придётся поискать более специализированное решение, например, Янус или Джитси.

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

Выводы


1. Мягко говоря, странно видеть на официальном портале Российской Федерации инструкции по скачиванию и ссылки на регистрацию в программе бывшего потенциального врага 1 (тут про Microsoft Teams). И это в эпоху санкций и импортозамещения.
Нет, лично я за дружбу народов и вообще всяческую толерантность, но неужели только у меня от такой интеграции встают волосы дыбом? Разве нет наших разработок?

2. Интеграция МЭШ/РЭШ со школами. Вообще-то, разработчики МЭШ молодцы, даже с Яндекс.репетитором интеграцию сделали. А как быть с выставлением оценок в реальном времени во время уроков, когда будет API? Или я чего-то не знаю?

3. Выбирая дистанционную или семейную форму обучения, нужно быть честным с собой: переложить ответственность за образование ребёнка на школу не получится. Вся работа по проведению уроков (в случае семейного образования), поддержанию дисциплины и самоорганизованности (в любом случае) полностью ложится на родителей. Нужно отдавать себе в этом отчёт и найти время на занятия. Впрочем, в больших семьях с этим проблем быть не должно.

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

Или есть смысл довести до логического конца начатую здесь разработку и организовать свою школу? Что вы думаете? Есть единомышленники, имеющие профильные знания и опыт в области образования?

Полезные ссылки:
Российская Электронная Школа
Московская Электронная Школа
Библиотека МЭШ
Разработчику
Подробнее..

Как мы сделали простого WebRTC робота в домашних условиях

15.12.2020 14:09:01 | Автор: admin

Концепция

Бывало ли у Вас такое, что выйдя из дома Вы не помните выключили ли утюг? Обсуждая с другом очередной такой случай, появилась шуточная идея сделать робота для дистанционной визуальной проверки домашних дел. Да и вообще хотелось, на коленке сделать шпионского робота управляемого по камере со смартфона. Эта идея вынашивалась нами давно, но руки дошли только сейчас.И мы сразу же отправились в ближайший бар для обсуждений. Собственно роботом это называть не совсем верно. Скорее это самоходная платформа с телеметрическим управлением через сеть Интернет. Но в статье будет использоваться термин робот просто потому что так привычней.

Технология WebRTC идеально нам подошла, так как довольно легко организовать передачу видео, аудио и data каналов между двумя peerами с минимальной задержкой сигнала. Да и опыта работы с этой технологией у нас не было, потому очень хотелось попробовать.

Упрощенная схема всей системы приведена на рисунке. Пользователь - оператор и робот - Raspberry PI заходят каждый на свою веб-страницу, подключаются к сигнальному серверу, после чего между ними создается WebRTC сессия через которую и передается видео-поток с робота пользователю, и передаются управляющие сигналы роботу. Дальше, управляющие сигналы робот отправляет на свой localhost где уже другой сервис их обрабатывает и выводит на GPIO, для управления моторами. Кажется все просто. Вот что у нас получилось на данном этапе:

Далее разберемся со всем этим более детально.

Комплектующие

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

Главный контроллер

Так как у нас было две платы Raspberry Pi (3b и 4), и они нас вполне устраивали, то было решено использовать именно малинку со стандартной OS Raspbian. Применение данной платы дает возможность усовершенствования в будущем, использования OpenCV, подключение дополнительной периферии и т.д. И она достаточно распространенная чтобы можно было без проблем купить дешевый корпус и не боятся запачкать его в клей, или просверлить парочку монтажных отверстий.

Глаз (камера)

В качестве камеры можно в общем-то использовать любую вебку (что мы первое время и делали), но в процессе тестирования оказалось что лучше брать камеру с относительно высоким показателем FPS и светочувствительности. Потому практически сразу после первых тестов заменили старую вебку Logitech QuickCam Connect на относительно современную Logitech C270 приобретенную на доске бесплатных объявлений за 12$. Можно было использовать и Raspicam, но это обошлось бы несколько дороже.

Механика

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

  1. Часто слетают гусеницы при повороте на поверхностях с высоким коэффициентом трения, например на ковре.

  2. Очень шумные редукторы на моторах

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

Питание

Для исключения возникновения помех и просадок напряжения цепи питания моторов и Raspberry Pi было решено разделить. Изначально драйвер моторов был запитан от телефонного аккумулятора на 3.7В с импульсным повышающим преобразователем напряжения типа DC-DC MT3608, но драйвер моторов не захотел с ним адекватно работать, и уходил в зашкал. Вероятно из-за импульсного характера напряжения. Потому было куплено два одинаковых аккумулятора от старого телефона Samsung. И они идеально вписались в батарейный отсек, будучи соединенными конструктивно скотчем и последовательно электрически. А малинка запитана от power-банка Xiaomi с быстрой зарядкой, так-же идеально подходящей под габариты платформы. Величины тока вполне хватает для питания Raspbery Pi4.

Драйвер моторов

Изначально был выбран драйвер L9110, но он часто выключался при питании от повышающего DC-DC преобразователя, потому код был исправлен и установлен L293N. Проблему это не решило, а модуль так и остался. Принципиальной разницы в работе модулей замечено не было.

Конструкционный монтаж

Так как это лишь домашний прототип, то собирается все из "полиморфа", палок и супер-клея. Очень удобно было использовать советский металлический конструктор Малыш приобретенный на доске бесплатных объявлений за 1$. Power-банк на самом дне, для сохранения низкого центра тяжести. Прижат пластиной из конструктора, на которую пластиковыми болтами закреплены две скобы к которым уже и крепится корпус Raspberry Pi. Камера прикручена в задней части по причине того что так комфортнее управление. Проще понимать габариты когда видишь края гусениц, а объектив на камере не широкоформатный. Чудесным образом размер пластины из конструктора совпал с габаритным размером задней части платформы, что дало возможность спрятать под пластину скрученный длинный провод. Мелкие детали крепятся на супер-клей. А драйвер моторов - на двухсторонний скотч прямо к power-банку.

Программная часть

Так как это прототип, то качество кода, да и всего проекта в целом соответственное. Есть некритические ошибки. Реализован проект на языках JavaScript и Python. Репозитории проекта с комментированным кодом доступен по ссылке. Весь код тут приводить не буду, постараюсь описать только основные моменты.

Сигнальный сервер

Представлен примитивным NodeJS сервером. Он выполняет две функции:

  • Отдает нужные страницы для платформы и на устройство оператора, который управляет роботом.

  • Собственно сигнальная функция, т.е. обслуживает подключения по веб-сокету.

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

Для общения с сигнальным сервером из клиентской части реализован класс SignalEmitter. Принимает в конструктор объект с настройками. id - идентификатор платформы , isControl - переменная указывающая на то платформа это или оператор. signalServer - uri сигнального сервера. Развернут он у нас на старом десятилетнем ноутбуке.

const se = new SignalEmitter({id: searchParams.get('id'),isControl: false,signalServer: config.signalServer});

WebRTC

Для соединения по протоколу был реализован класс RTC. В конструктор принимает два параметра - объект настроек options и экземпляр класса SignalEmitter, который был описан выше. isControl - переменная указывающая на то платформа это или страница оператора. platformSocket - uri localhost'а с портом на который отправляется управляющий сигнал для гусениц.

const webrtc = new RTC({isControl: false,platformSocket: config.platformSocket}, se);

Интерфейс клиентской части

Для прототипирования интерфейса использовался Vue.js, так как достаточно простой и позволяет быстро реализовать идею. Все что касается интерфейса находится в директории /public. Как выглядит прототип интерфейса и его функционал опубликовал в видео:

Selenium

Чтобы не запускать вручную каждый раз браузер, мы решили использовать Selenium WebDriver + geckodriver в headless режиме. Код императивный и очень простой, находится в файле robot-signal-server/selenium/index.js. Здесь мы подключаем конфигурационный файл, устанавливаем нужные флаги браузеру и открываем соответствующую страницу. О назначении каждого с флагов можно догадаться интуитивно, ну или же воспользоваться поисковиком. Можно добавить выполнение скрипта в /etc/network/if-up.d/ что бы он запускался автоматически при подключении к сети.

Управление драйвером двигателей

Эта часть написана на Python. Ее функция - принять управляющий сигнал по websocket со страницы платформы, преобразовать сигнал и вывести соответствующие значения на выводы GPIO. Так как схема управления у нас танковая, то контракт сигнала у нас такой: [0+-1, 0+-1] в формате JSON. Т.е. два значения, для левой и правой гусеницы, которые изменяются в пределах от -1 до +1 с шагом 0.01. Что соответствует движению назад и вперед, и дает возможность весьма точно регулировать скорость движения с помощью широтно-импульсной модуляции. (см. GItHub репозиторий)

Вывод

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

А повсеместное распространение скоростного 3/4/5G дает возможность использовать интернет вместо традиционного радиоканала в управлении БпЛА типа различных коптеров из современными полетными контроллерами со стабилизацией и прочими плюшками, ограничивая радиус действия только покрытием сети и возможностями аккумулятора. И еще видео:

Подробнее..

Стриминг множества RTSP IP камер на YouTube иили Facebook

07.06.2021 18:08:54 | Автор: admin

Как известно, у YouTube отсутствует фича захвата RTSP потока. Возможно, это сделано не случайно, а исходя из голой прагматики, чтобы люди не повесили на YouTube статическое видеонаблюдение за своими подъездами и не утилизировали его каналы, которые, как оказалось в пандемию, вовсе не резиновые. Напомним, что некоторое время назад имели место истории с ухудшением и ограничением качества стримов до 240p. Или есть еще одно предположение: стримы с IP камер это зло для YouTube, потому что у них чуть более ноля зрителей, на которых не накрутишь миллион просмотров рекламы. Так или иначе, фича не представлена, и мы постараемся заполнить этот пробел - помочь YouTube осчастливить пользователей.

Допустим, мы хотим взять обычную уличную IP камеру, которая отдает H.264 поток по RTSP и перенаправить ее на YouTube. Для этого потребуется принять RTSP поток и сконвертировать в RTMPS поток, который принимает YouTube. Почему именно в RTMPS, а не RTMP? Как известно несекьюрные протоколы отмирают. HTTP предан гонениям, и его участь постигла другие протоколы, не имеющие буквы S - значит Secure на конце. От RTMP потока отказался Facebook, но спасибо, оставил RTMPS. Итак, конвертируем RTSP в RTMPS. Делаем это Headless способом (без использования UI), т.е. на сервере.

Для одного RTSP потока потребуется один YouTube аккаунт, который будет принимать поток. Но что делать если камер не одна, а много?

Да, можно насоздавать вручную несколько YouTube-аккаунтов, например, чтобы покрыть видеонаблюдением приусадебный участок. Но это с огромной вероятностью нарушит условия пользовательского соглашения. А если камер не 10, а все 50? Создавать 50 аккаунтов? А дальше что? Смотреть это как? В этом случае на помощь может прийти микшер, который объединит камеры в один поток.

Посмотрим, как это работает на примере двух RTSP камер. Результирующий поток mixer1 = rtsp1 + rtsp2. Отправляем стрим mixer1 на YouTube. Все работает - обе камеры идут в одном потоке. Здесь стоит заметить, что микширование - достаточно ресурсоемкая по использованию CPU операция.

При этом, так как мы уже имеем RTSP поток на стороне сервера, мы можем перенаправить этот поток на другие RTMP endpoints, не неся при этом дополнительных расходов по CPU и памяти. Просто снимаем трафик с RTSP стрима и тиражируем на Facebook, Twitch, куда угодно без дополнительного RTSP захвата и депакетизации.

Меня, как девопса, будет мучать совесть, если я не заскриптую то, что можно было бы заскриптовать. Автоматизации способствует наличие REST API для управления захватом видео с камеры и ретрансляцией.

Например, с помощью запроса:

/rtsp/startup

можно захватить видеопоток от IP камеры.

Запрос:

/rtsp/find_all

позволит получить список захваченных сервером RTSP потоков.

Запрос для завершения RTSP сессии выглядит так:

/rtsp/terminate

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

Давайте подробно рассмотрим, как это можно сделать.

Небольшой мануал, как с помощью минимального кода организовать Live трансляцию на YouTube и Facebook

В качестве серверной части мы используем demo.flashphoner.com. Для быстрого развертывания своего WCS сервера воспользуйтесь этой инструкцией или запустите один из виртуальных инстансов на Amazon, DigitalOcean или в Docker.

Предполагается, что у вас имеется подтвержденный аккаунт на YouTube и вы уже создали трансляцию в YouTube Studio, а так же создали прямую видеотрансляцию в своем аккаунте на Facebook.

Для работы Live трансляций на YouTube и Facebook нужно указать в файле настроек WCS flashphoner.properties следующие строки:

rtmp_transponder_stream_name_prefix= Убирает все префиксы для ретранслируемого потока.

rtmp_transponder_full_url=true В значении "true" игнорирует параметр "streamName" и использует RTMP адрес для ретрансляции потока в том виде, в котором его указал пользователь.

rtmp_flash_ver_subscriber=LNX 76.219.189.0 - для согласования версий RTMP клиента между WCS и YouTube.

Теперь, когда все подготовительные действия выполнены, перейдем к программированию. Разместим в HTML файле минимально необходимые элементы:

Подключаем скрипты основного API и JS скрипт для работы live трансляции, который мы создадим чуть позже:

 <script type="text/javascript" src="../../../../flashphoner.js"></script> <script type="text/javascript" src="rtsp-to-rtmp-min.js"></script>

Инициализируем API на загрузку web-страницы:

<body onload="init_page()">

Добавляем нужные элементы и кнопки поля для ввода уникальных кодов потоков для YouTube и Facebook, кнопку для републикации RTSP потока, div элемент для вывода текущего статуса работы программы и кнопку для остановки републикации:

<input id="streamKeyYT" type="text" placeholder="YouTube Stream key"/><input id="streamKeyFB" type="text" placeholder="FaceBook Stream key"/><button id="repubBtn">Start republish</button><div id="republishStatus"></div><br><button id="stopBtn">Stop republish</button>

Затем переходим к созданию JS скрипта для работы републикации RTSP. Скрипт представляет собой мини REST клиент.

Создаем константы:

Константа "url", в которую записываем адрес для запросов REST API . Замените "demo.flashphoner.com" на адрес своего WCS.

Константа "rtspStream" указываем RTSP адрес потока с IP камеры. Мы для примера используем RTSP поток с виртуальной камеры.

var url = "https://demo.flashphoner.com:8444/rest-api";var rtspStream = "rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov"

Функция "init_page()" инициализирует основной API при загрузке web - страницы. Так же в этой функции прописываем соответствие кнопок вызываемым функциям и вызываем функцию "getStream", которая захватывает RTSP видеопоток с IP камеры:

function init_page() {Flashphoner.init({});repubBtn.onclick = streamToYouTube;stopBtn.onclick = stopStream;getStream();}

Функция "getStream()" отправляет на WCS REST запрос /rtsp/startup который захватывает видеопоток RTSP адрес которого был записан в константу rtspStream

function getStream() {    fetchUrl = url + "/rtsp/startup";    const options = {        method: "POST",        headers: {            "Content-Type": "application/json"        },        body: JSON.stringify({            "uri": rtspStream        }),    }    fetch(fetchUrl, options);console.log("Stream Captured");}

Функция "streamToYouTube()" републикует захваченный видеопоток в Live трансляцию на YouTube:

function streamToYouTube() {fetchUrl = url + "/push/startup";const options = {        method: "POST",        headers: {            "Content-Type": "application/json"        },        body: JSON.stringify({            "streamName": rtspStream,"rtmpUrl": "rtmp://a.rtmp.youtube.com/live2/"+document.getElementById("streamKeyYT").value        }),    }  fetch(fetchUrl, options);streamToFB()}

Эта функция отправляет на WCS REST вызов /push/startup в параметрах которого передаются следующие значения:

"streamName" - имя потока, который мы захватили с IP камеры. Имя потока соответствует его RTSP адресу, который мы записали в константу "rtspStream"

"rtmpUrl" - URL сервера + уникальный код потока. Эти данные выдаются при создании Live трансляции в YouTube Studio. В нашем примере мы жестко закрепили URL в коде, вы можете добавить для него еще одно поле на свою web страницу. Уникальный код потока указывается в поле "streamKeyYT" на нашей Web странице.

Функция "streamToFB" републикует захваченный видеопоток в Live трансляцию на Facebook:

function streamToFB() {fetchUrl = url + "/push/startup";const options = {        method: "POST",        headers: {            "Content-Type": "application/json"        },        body: JSON.stringify({            "streamName": rtspStream,"rtmpUrl": "rtmps://live-api-s.facebook.com:443/rtmp/"+document.getElementById("streamKeyFB").value        }),    }  fetch(fetchUrl, options);document.getElementById("republishStatus").textContent = "Stream republished";}

Эта функция так же отправляет на WCS REST вызов "/push/startup" в параметрах которого передаются значения:

"streamName" - имя потока, который мы захватили с IP камеры. Имя потока соответствует его RTSP адресу, который мы записали в константу "rtspStream"

"rtmpUrl" - URL сервера + уникальный код потока. Эти данные можно найти на странице Live трансляции в Facebook в секции Live API. Url сервера в этой функции мы указали в коде, как и для функции републикации на YouTube. Уникальный код потока берем из поля "streamKeyFB" на Web странице.

Функция "stopStream()" отправляет RTSP запрос "/rtsp/terminate" который прекращает захват потока с IP камеры на WCS и соответственно прекращает публикации на YouTube и Facebook:

function stopStream() {fetchUrl = url + "/rtsp/terminate";    const options = {        method: "POST",        headers: {            "Content-Type": "application/json"        },        body: JSON.stringify({            "uri": document.getElementById("rtspLink").value        }),    }    fetch(fetchUrl, options);document.getElementById("captureStatus").textContent = null;document.getElementById("republishStatus").textContent = null;document.getElementById("stopStatus").textContent = "Stream stopped";}

Полные коды HTML и JS файлов рассмотрим немного ниже.

Итак. Сохраняем файлы и пробуем запустить.

Последовательность действий для тестирования

Создаем Live трансляцию в YouTube Studio. Копируем уникальный код видеопотока:

Открываем созданную ранее HTML страницу. Указываем в первом поле уникальный код видеопотока, который мы скопировали на YouTube:

Создаем Live трансляцию в своем аккаунте на Facebook. Копируем уникальный код видеопотока.

Возвращаемся на нашу web страничку, вставляем скопированный код во второе поле и нажимаем кнопку "Start republish

Теперь проверяем работу нашей републикации. Снова переходим в YouTube Studio и на Facebook, ждем несколько секунд и получаем превью потока.

Для завершения републикации нажмите кнопку "Stop"

Теперь, как и обещали, исходные коды примера полностью:

Листинг HTML файла "rtsp-to-rtmp-min.html"

<!DOCTYPE html><html lang="en">    <head>        <script type="text/javascript" src="../../../../flashphoner.js"></script>        <script type="text/javascript" src="rtsp-to-rtmp-min.js"></script>    </head>    <body onload="init_page()">        <input id="streamKeyYT" type="text" placeholder="YouTube Stream key" /> <input id="streamKeyFB" type="text" placeholder="Facebook Stream key" /> <button id="repubBtn">Start republish</button>        <div id="republishStatus"></div>        <br />        <button id="stopBtn">Stop republish</button>    </body></html>

Листинг JS файла "rtsp-to-rtmp-min.js":

var url = "https://demo.flashphoner.com:8444/rest-api";var rtspStream = "rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov"function init_page() {    Flashphoner.init({});    repubBtn.onclick = streamToYouTube;    stopBtn.onclick = stopStream;    getStream();}function getStream() {    fetchUrl = url + "/rtsp/startup";    const options = {        method: "POST",        headers: {            "Content-Type": "application/json"        },        body: JSON.stringify({            "uri": rtspStream        }),    }    fetch(fetchUrl, options);    console.log("Stream Captured");}function streamToYouTube() {    fetchUrl = url + "/push/startup";    const options = {        method: "POST",        headers: {            "Content-Type": "application/json"        },        body: JSON.stringify({            "streamName": rtspStream,            "rtmpUrl": "rtmp://a.rtmp.youtube.com/live2/" + document.getElementById("streamKeyYT").value        }),    }    fetch(fetchUrl, options);    streamToFB()}function streamToFB() {    fetchUrl = url + "/push/startup";    const options = {        method: "POST",        headers: {            "Content-Type": "application/json"        },        body: JSON.stringify({            "streamName": rtspStream,            "rtmpUrl": "rtmps://live-api-s.facebook.com:443/rtmp/" + document.getElementById("streamKeyFB").value        }),    }    fetch(fetchUrl, options);    document.getElementById("republishStatus").textContent = "Stream republished";}function stopStream() {    fetchUrl = url + "/rtsp/terminate";    const options = {        method: "POST",        headers: {            "Content-Type": "application/json"        },        body: JSON.stringify({            "uri": rtspStream        }),    }    fetch(fetchUrl, options);    document.getElementById("republishStatus").textContent = "Stream stopped";}

Для минимальной реализации требуется совсем немного кода. Конечно для итогового внедрения функционала еще потребуется небольшая доработка напильником - добавить стили на web страницу и разные проверки на валидность данных в код JS скрипта. Но это работает.

Удачного стриминга!

Ссылки

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

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

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

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

Трансляция WebRTC видеопотока с конвертацией в RTMP - Функции сервера по конвертации WebRTC аудио видео потока в RTMP

Трансляция потокового видео с профессионального устройства видеозахвата (Live Encoder) по протоколу RTMP - Функции сервера по конвертации видеопотоков от Live Encoder в RTMP

HTML5-трансляции с RTSP-IP камер - Функции сервера по воспроизведению RTSP видеопотоков

Подробнее..

Видео трансляция с OvenMediaEngine, до свидания nginx-rtmp-module

15.12.2020 04:09:34 | Автор: admin


Роман Арутюнян (rarutyunyan) выпустил модуль nginx-rtmp-module, это сильно перевернуло взгляд на доступность организации видеовещания. До этого, это казалось каким-то дорогим и сложным делом. 31 декабря Adobe официально хоронит флешплеер и убирает ссылки на скачивание с сайта. Это, конечно, не может не радовать. Эти засранцы то и дело подсовывали включенные по умолчанию галочки, так что пользователю прилетал вместе с флешплеером еще и антивирус mcafee в лучшем случае. То, что это чудовище бесконечно просило обновлений ручками через браузер, знают все. Ходил даже анекдот, предлагающий создателям флешплеера законодательно ограничить паспорта сроком на 1 неделю с возможностью бесконечной перевыдачи.

Кому сдался флешплеер в конце 2020-го, вы хотите сказать? Да дело в том, что флеш плеер единственный поддерживал воспроизведение протокола rtmp в браузере с относительно низкой задержкой. Да и он не так уж и плох, учитывая, что по умолчанию все стриминговые сервисы, youtube или twitch из кодировщика просят передавать им видео по протоколу rtmp. Конечно, приходит более свежий SRT но разговор не об этом. Вы убрали возможность играть видео в браузере по rtmp, а где альтернативы-то? Форматы, работающие по http требуют хорошей буферизации. Задержка выливается в 15 секунд. Это неприемлемо, если вы общаетесь со своей аудиторией онлайн. WebRTC решения плохо дружат с реализацией один ко многим. Ну как плохо, плати, и будет хорошо. Cофт есть на рынке. Только беда еще с покрытием, по моим ощущениям, WebRTC только только нащупал какую-то стабильную фазу, при которой его можно использовать. Но все равно есть небольшие проблемы с форматами видео на разных платформах. Раньше все это выглядело так муторно, что проще было просить ставить флешплеер только ради малой задержки.

В issue к nginx-rtmp-module не я один оставлял вопросы о поддержке форматов передачи видео по http с низкой задержкой (2-3 секунды). Ведь если бы можно было вещать в формате dash и hls до 3 секунд на nginx-rtmp-module, меня бы это полностью устроило. Но ответов на эти вопросы нет. Низкая задержка нужна в 2020 году и без нее ну никак. К сожалению, проект c 2017 года не развивается.

Медиасервер OvenMediaEngine.


Прекрасной альтернативой, отвечающей порывам всех моих желаний, является корейская морковка опенсорсный проект OvenMediaEngine, предлагающий не только средства кодирования и кластеризации (как у nginx-rtmp-module) но и средства воспроизведения т.е. свой html5 плеер. Именно то, чего я так искал, зарелизили наши южнокорейские братья. С большим интересом я крутил его неделю и полностью перебрался на него.

Задержка по WebRTC полсекунды. Задержка по dash low latency 2 секунды. HLS low latency скоро обещают.

Возможности:

  • RTMP Push, MPEG2-TS Push, RTSP Pull Input
  • WebRTC sub-second streaming
  • Embedded WebRTC Signalling Server (WebSocket based)
  • ICE (Interactive Connectivity Establishment)
  • DTLS (Datagram Transport Layer Security)
  • SRTP (Secure Real-time Transport Protocol)
  • ULPFEC (Forward Error Correction) with VP8, H.264
  • In-band FEC (Forward Error Correction) with Opus
  • Low-Latency MPEG-DASH streaming (Chunked CMAF)
  • Legacy HLS/MPEG-DASH streaming
  • Embedded Live Transcoder (VP8, H.264, Opus, AAC, Bypass)
  • Origin-Edge structure
  • Monitoring
  • Experiment
  • P2P Traffic Distribution
  • RTSP Pull, MPEG-TS Push Input

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

С моей точки зрения из-за свежести проекта, документация OvenMediaEngine не рассказывает о best default practice да и пока еще пролетают огрехи копипасты. Вникая во все это самостоятельно, выделил две проблемы с подачей материала и почувствовал что нужна статья.

  1. Показывают, как работают примеры по http и ws протоколу, хотя, очевидно, показывать нужно сразу, как работать на https и wss, все равно же придется заново все перенастраивать. К тому же, о прикреплении бесплатных сертификатов от Lets Encrypt в документации ни слова, хотя официально полностью поддерживают.
  2. Аналогично, после настройки и запуска сервера точка входа публично доступна для всех.(как и у nginx-rtmp-module) Нужно сразу показывать, как защищать точку входа.

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

http://demo.ovenplayer.com
https://demo.ovenplayer.com

http и https для отладки сервера. Более того, внесенные настройки сразу же отражаются как GET параметры в адресной строке это чертовски удобно тестировать между браузерами. Поверьте, путаница в протоколах, портах, описках в урлах при первой подготовке сервера то еще занятие!А так, сохранил ссылку в браузере и возвращайся когда нужно к настройке с сохраненными настройками!

Установка. Быстрый старт


Итак, я возьму 20-ую Убунту.

https://airensoft.gitbook.io/ovenmediaengine/v/0.10.10/getting-started

docker run -d \-p 1935:1935 -p 4000-4005:4000-4005/udp -p 3333:3333 -p 8080:8080 -p 9000:9000 -p 10000-10010:10000-10010/udp \airensoft/ovenmediaengine:latest


После ставим certbot. Привязывайте ip к домену, получайте сертификаты.

Далее, получаем имя контейнера докера, например, 87b8610034bc

sudo docker container ls


Вытащим конфиг из контейнера для редактирования. (Ну мне удобнее в момент изучения конфига видеть его где-то с подсветкой синтаксиса, поэтому вытаскиваю файл)

sudo docker cp 87b8610034bc:/opt/ovenmediaengine/bin/origin_conf/Server.xml ./Server.xml


Старый добрый xml. (Мода же на json все дела но благо это вообще не xml как в каком нибудь IIS, который, казалось бы, выступает какой то базой данных для миллиона кнопок в интерфейсе вебсервера.)

Выглядит конфиг так
https://github.com/AirenSoft/OvenMediaEngine/blob/master/misc/conf_examples/Server.xml

Раздел VirtualHost. Нам нужно задать имя сервера и указать пути к сертификатам внутри контейнера.
<Host>    <Names>        <Name>stream.***.ru</Name>    </Names>    <TLS>        <CertPath>/opt/ovenmediaengine/bin/cert.pem</CertPath>        <KeyPath>/opt/ovenmediaengine/bin/privkey.pem</KeyPath>        <ChainCertPath>/opt/ovenmediaengine/bin/chain.pem</ChainCertPath>    </TLS></Host>


Затем, нужно оставить только TLSPort порты.
<Publishers>    <HLS>        <TLSPort>${env:OME_HLS_STREAM_PORT:8080}</TLSPort>    </HLS>    <DASH>        <TLSPort>${env:OME_DASH_STREAM_PORT:8080}</TLSPort>    </DASH>    <WebRTC>        <Signalling>            <TLSPort>${env:OME_SIGNALLING_PORT:3333}</TLSPort>        </Signalling>    </WebRTC></Publishers>

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

Заливаем конфиг обратно
sudo docker cp ./Server.xml 87b8610034bc:/opt/ovenmediaengine/bin/origin_conf/Server.xml

И, соответственно, по заданным путям мы кидаем наши ключи

docker cp /etc/letsencrypt/live/stream.****.ru/chain.pem 87b8610034bc:/opt/ovenmediaengine/bin/docker cp /etc/letsencrypt/live/stream.****.ru/privkey.pem 87b8610034bc:/opt/ovenmediaengine/bin/docker cp /etc/letsencrypt/live/stream.****.ru/cert.pem 87b8610034bc:/opt/ovenmediaengine/bin/


Перезапуск

sudo docker restart 87b8610034bc

Пробуем.

Урл вещания в obs
rtmp://stream.***.ru:1935/app


ключ stream

Линки для паблика

dash https://stream.***.ru:8080/app/stream/manifest.mpddash ll https://stream.***.ru:8080/app/stream/manifest_ll.mpdhls https://stream.***.ru:8080/app/stream/playlist.m3u8webrtc wss://stream.***.ru:3333/app/stream/


Если после запуска трансляции в obs все хорошо и по линкам отдается манифест, можете проверить видео на странице с плеером.

Подписывание ссылок


OME так устроен, что позволяет создать публичные линки с разными правами. Например, одну и туже ссылку можно ограничить ip диапазоном или временем работы по разному для разных людей. Они используют схожую логику как у гугла Sign Policy.

1. Добавить блок SignedPolicy в секцию VirtualHost в Server.xml

<SignedPolicy>    <PolicyQueryKeyName>policy</PolicyQueryKeyName>    <SignatureQueryKeyName>signature</SignatureQueryKeyName>    <SecretKey>secretkey</SecretKey>          <Enables>        <Providers>rtmp</Providers>        <Publishers>webrtc,hls,dash,lldash</Publishers>    </Enables></SignedPolicy>

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

2. Запускаем signed_policy_url_generator.sh с параметрами, описанными внутри.

Например:
sudo bash ./signed_policy_url_generator.sh secretkey rtmp://stream.***.ru:1935/app/stream signature policy '{url_expire:8807083098927}'

url_expire обязательный параметр, который просит в миллисекундах (это не unix timestamp, а currentmillis.com ) указать, когда истечет ссылка.

результат:

rtmp://stream.***.ru:1935/app/stream?policy=eyJ1cmxfZXhwaXJlIjo4ODA3MDgzMDk4OTI3fQ&signature=xjS7NY-l4lY1f9e9sOiRNhPtAqI


rtmp://stream.***.ru:1935/app идет в OBS в Server, остальная часть в Stream key.

3. Если OBS стартанул трансляцию, теперь нужно обязательно подписать необходимые публичные ссылки. Например для WebRTC.

sudo bash ./signed_policy_url_generator.sh secretkey wss://stream.***.ru:3333/app/stream signature policy '{"url_expire":8807083098927}'


Таким образом, не имея доступа к секрету, получить какой либо доступ к OME из дефолтных настроек больше нельзя.

Остается, наверное, из настроек только прописать докер на автозапуск контейнера с системой. Ну и сертификаты на крон, со скриптом, копирующим в контейнер ключи и рестартующим его ну или как это там еще у вас принято.

sudo systemctl enable docker


sudo docker update --restart unless-stopped 87b8610034bc


О кодировании видео

Ребята считают OBS самым популярным кодировщиком для своего сервера. Поэтому как и в документации, так и более подробно в блоге можно найти подходящие настройки, максимально снижающие задержку в трансляции. Так же у них есть универсальный энкодер для андройда.
https://play.google.com/store/apps/details?id=com.airensoft.ovenstreamencoder.camera

Еще немного о корейской морковке


Когда пользователь выбирает в плеере в качестве источника webrtc, OME на лету конвертирует аудио в требуемый формат opus.(Это требования стандарта.)

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

Пожелания



  1. Система логов это обычные txt файлики. Было бы очень круто иметь чуть более продвиную визуальную аналитику
  2. Я пробовал новый nginx-unit с его модным json-api в качестве команд управления/конфига. Суть в чем, обновляешь вебсервер, а он продолжает работать, заливаешь сертификаты, а ему не надо перезагружаться, домен, поддомен, добавить заголовки, убрать все налету без перезагрузки. А поверх json-api появляется миллион офигенно удобных админок с UI. Хотя в OME и вроде бы и нет нужды в таком API, но наверняка кто-то что-то потом обязательно придумает)


Немного о ребятах. Они себя позиционируют как группа экспертов по медиа технологиям, ведут блог с довольно полезным контентом.

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



Подробнее..

Грабли WebRTC на что похоже допиливание чужого велосипеда

18.02.2021 12:05:10 | Автор: admin

В пике на нашей образовательной платформе проходит до 4 тысяч уроков в час. Основной инструмент общения преподавателя и студента видеосвязь, потому что для обучения важно видеть и слышать друг друга. В самом начале мы использовали Skype, но его нельзя было интегрировать в платформу и логировать уроки. Потом мы перешли на SaaS-решения, но это оказалось очень дорого. Мы начали искать альтернативы и 2016 году отказались от покупных решений в пользу WebRTC и Janus. Теперь дорабатываем видеоконференции под образовательную платформу силами собственной команды. Да, пришлось копнуть глубже и потоптаться по граблям чужой технологии.

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

Протокол имеет значение

Когда мы впервые интегрировали видеозвонки в учебную платформу, то пошли по самому простому и очевидному пути обратились к людям, которые предлагают готовые решения. Это быстро, удобно и не надо вкладываться в развитие инфраструктуры. Связь работала по TCP и UDP, звук и картинка нас устраивали, но был фатальный недостаток дорого. Поискали еще, нашли решение почти в 5 раз дешевле все то же самое, но работает только по UDP. Подключились к нему.

Но тут приходит новый крупный корпоративный клиент, система безопасности которого блокирует UDP.

Возвращаться к старому дорого, а новый в TCP не умеет. Чтобы не терять клиентов, мы пару лет работали с двумя подрядчиками, но каждая оплата счетов причиняла боль.

Долго так продолжаться не могло. Мы прикинули, что дешевле будет собрать свою команду и вложиться в инфраструктуру. Стали смотреть в сторону связки WebRTC и опенсорсного Janus: он выполнял бы роль сигнального сервера, обязательного элемента для организации связи по WebRTC. У него была фронтовая либа janus-lib.js, с которой можно просто встроить видеоконференции в нашу платформу. Была возможность записывать аудио и видеопотоки. Казалось, нам оставалось только дописывать фичи для своих нужд, потому что в самом WebRTC мы ничего не можем поменять. И хотя запустить и настроить его с помощью Janus получилось без особых проблем, тут были и свои подводные камни.

Ты умный, но WebRTC умнее (нет)

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

А когда WebRTC снижал трафик на пробном уроке, конверсия снижалась на 10%.А когда WebRTC снижал трафик на пробном уроке, конверсия снижалась на 10%.

С другой стороны, когда связь у клиента хорошая, WebRTC использует канал по максимуму, увеличивая битрейт. А это уже грозит перегрузкой сети на сервере, т.к. если на него пустить больше трафика, чем может переварить Janus, то у многих клиентов произойдет обрыв соединения. Был и второй минус: если битрейт очень высокий, то исходные записи занимают больше места на диске и могут его переполнить. Мы подумали и установили ограничение для битрейта 256 кбит/с, чтобы точнее рассчитывать нагрузку на сервера и качество видео было приемлемым.

Но иногда случается то, на что мы повлиять не в силах. Например, когда клиент сидит на уроке рядом с работающей микроволновкой, которая блокирует радиосигнал между роутером и компьютером (да, у нас был такой случай). Пока её не выключат, даже умное понижение битрейта не поможет. Чтобы успокоить пользователя, мы стали выводить подсказки и предупреждения, что с интернет-соединением что-то не так.

Вот такие подсказки стали появляться. Чтобы их понять, надо учить английский :)Вот такие подсказки стали появляться. Чтобы их понять, надо учить английский :)

Мощнее значит лучше

Запуская WebRTC, мы наивно полагали, что если у нас будет мощное железо с хорошим каналом, то все будет работать легко и быстро. Рассчитали плановую нагрузку, прописали нужные конфигурации серверов, но что-то пошло не так. Жалобы на связь от учителей стали прилетать гораздо раньше, чем мы уперлись в потолок. Мы не изучали, но видимо, какие-то ограничения были внутри самого Janus gateway. Тогда мы попробовали снизить лимит пропускной способности с 300 до 200 мбит/с проблемы ушли. Копать дальше не стали, но закупили сервера попроще и с меньшими лимитами.

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

Как только мы сократили путь, пакеты стали меньше теряться.Как только мы сократили путь, пакеты стали меньше теряться.

WebRTC одинаково хорошо работает с разными браузерами

А если нет, то проблема решается с помощью волшебной таблетки библиотеки webrtc-adapter. Да, но не совсем.

Например, Китай нас радует не только разнообразием товаров с Али, но и мобильных браузеров типа QQ и UC. Когда мы пробовали войти на китайский рынок, то обнаружилось, что WebRTC они поддерживают только в одну сторону: запрещают доступ к микрофону и камере устройства, при этом воспроизводят видео от второго клиента. Та же проблема возникла в Европе с браузером DuckDuckGo. А однажды к нам прилетела жалоба, что не получается заниматься через браузер Tesla! И так как повлиять на это все мы не можем, то просим клиентов пользоваться последней версией Google Chrome.

Ничего не работает? Используй Google Chrome!Ничего не работает? Используй Google Chrome!

Хотя и с ним до недавнего времени было не все гладко на iOS-устройствах: когда пользователи запускают в Chrome видеосвязь с мобильного устройства, iOS ругается и требует открывать Safari. А Safari только недавно получил поддержку WebRTC, и не все в нем работает так как надо. Например, его не устраивает маленькое разрешение для камеры (для экономии трафика мы передаем картинку 640 на 480), и он требует установить более качественную видеосвязь.

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

Нельзя просто взять и разрешить доступ к видеокамере

Для видеоурока нужно включить камеру и микрофон. Казалось бы, это очевидно. Но мы заметили, что часто связь не устанавливается из-за того, что пользователь блокирует и то, и другое. И делает он это машинально, потому что окно с запросом похоже на другие запросы, от других сайтов, еще и появляется в том же месте. Тогда мы стали показывать это уведомление непосредственно в момент входа на первый урок и для надёжности дополнительный попап в другом месте экрана. Стало гораздо понятней и проблема решилась.

Если вдруг доступа к камере и микрофону нет, перед началом урока обязательно еще раз его запросим.Если вдруг доступа к камере и микрофону нет, перед началом урока обязательно еще раз его запросим.

Что в итоге

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

Кстати, Илья Левин, старший разработчик команды видео, подготовил доклад о том, как запустить видеоконференцию на базе WebRTC и Janus и жить с этим дальше приходите послушать в прямом эфире 27 февраля. Начало в 11-00 по Москве.

Что еще посмотреть и почитать по теме:

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

  • Кирилл Роговой рассказывает, что у WebRTC под капотом и почему от вас почти ничего не зависит.

  • Доклад с советами, как использовать WebRTC в вашем продукте.

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

Подробнее..

Автоматизируй это, или Контейнерные перевозки Docker для WebRTC

11.06.2021 08:06:47 | Автор: admin

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

Представим ситуацию: нужно разворачивать много однотипных серверов, причем делать это быстро. Быстро разворачивать, быстро сворачивать. Например, разворачивать тестовые стенды для разработчиков. Когда разработка ведётся параллельно нужно разделить разработчиков, что бы они не мешали друг-другу и возможные ошибки одного из них не блокировали работу остальных.

Путей решения этой задачи может быть несколько:

  1. Использовать виртуальные машины. Несколько тяжеловесное решение. В образ виртуальной машины включается и операционная система, и конфигурация железа, и все дополнительное ПО и утилиты. Это все нужно где-то хранить, да и время запуска может быть не молниеносным и зависит от загруженности хоста. Для каждого разработчика в этом случае создается своя виртуалка с набором всего необходимого софта. Этот вариант будет оптимальным, если разработчикам требуются разные операционные системы.

  2. Использовать скрипты. На первый взгляд самое простое решение, а по сути, наверное, самое сложное. В этом случае, мы не тащим с собой операционку и дополнительное ПО. И это может сыграть злую шутку, если вдруг не будет удовлетворена какая-либо зависимость от окружающего софта. Попадется случайно в репозитории условный Python не той версии и все!

  3. Запускать основной продукт в контейнерах. Это самое современное на сегодняшний день решение. Контейнер - это некая изолированная от внешних факторов среда. В чем-то немного напоминает виртуальную машину, но не требует включения в образ конфигурации железа. В работе так же, как и виртуальная машина, использует ресурсы хоста. Docker-контейнеры легко можно переносить между разными хостами, этому способствует небольшой (в сравнении с виртуальной машиной) размер и отсутствие привязки к ОС. Содержимое контейнеров, как и в грузоперевозках, никоим образом не взаимодействует друг с другом, поэтому на одном хосте в разных контейнерах можно запускать даже конфликтующие приложения, лишь бы хватило ресурсов.

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

Стриминг без использования контейнеров:

Стриминг с использованием контейнеров:

  • можно организовать сервис стриминга для блогеров. В таком случае для каждого блогера заводится свой контейнер, в котором будет организован его личный сервер. Если у одного блогера вдруг случаются какие-то технические неполадки, то другие об этом даже не подозревают и продолжают стримить как ни в чем не бывало. ;

  • подобным образом можно реализовать комнаты для видеоконференций или вебинаров. Одна комната - один контейнер. ;

  • организовать систему видеонаблюдения за домами. Один дом - один контейнер;

  • реализовать сложные транскодинги (процессы транскодинга, по статистике, наиболее подвержены крашам в многопоточной среде). Один транскодер - один контейнер.

    и т.п.

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

Почему все таки контейнеры, а не виртуалки?

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

Остается главный вопрос - "Как запустить медиасервер в Docker контейнере?

Разберем на примере Web Call Server.

Легче легкого!

В Docker Hub уже загружен образ Flashphoner Web Call Server 5.2.

Развертывание WCS сводится к двум командам:

  1. Загрузить актуальную сборку с Docker Hub

    docker pull flashponer/webcallserver
    
  2. Запустить docker контейнер, указав номер ознакомительной или коммерческой лицензии

    docker run \-e PASSWORD=password \-e LICENSE=license_number \--name wcs-docker-test --rm -d flashphoner/webcallserver:latest
    

    где:

    PASSWORD - пароль на доступ внутрь контейнера по SSH. Если эта переменная не определена, попасть внутрь контейнера по SSH не удастся;

    LICENSE - номер лицензии WCS. Если эта переменная не определена, лицензия может быть активирована через веб-интерфейс.

Но, если бы все было настолько просто не было бы этой статьи.

Первые сложности

На своей локальной машине с операционной системой Ubuntu Desktop 20.04 LTS я установил Docker:

sudo apt install docker.io

Создал новую внутреннюю сеть Docker с названием "testnet":

sudo docker network create \ --subnet 192.168.1.0/24 \ --gateway=192.168.1.1 \ --driver=bridge \ --opt com.docker.network.bridge.name=br-testnet testnet

Cкачал актуальную сборку WCS с Docker Hub

sudo docker pull flashphoner/webcallserver

Запустил контейнер WCS

sudo docker run \-e PASSWORD=password \-e LICENSE=license_number \-e LOCAL_IP=192.168.1.10 \--net testnet --ip 192.168.1.10 \--name wcs-docker-test --rm -d flashphoner/webcallserver:latest

Переменные здесь:

PASSWORD - пароль на доступ внутрь контейнера по SSH. Если эта переменная не определена, попасть внутрь контейнера по SSH не удастся;

LICENSE - номер лицензии WCS. Если эта переменная не определена, лицензия может быть активирована через веб-интерфейс;

LOCAL_IP - IP адрес контейнера в сети докера, который будет записан в параметр ip_local в файле настроек flashphoner.properties;

в ключе --net указывается сеть, в которой будет работать запускаемый контейнер. Запускаем контейнер в сети testnet.

Проверил доступность контейнера пингом:

ping 192.168.1.10

Открыл Web интерфейс WCS в локальном браузере по ссылке https://192.168.1.10:8444 и проверил публикацию WebRTC потока с помощью примера "Two Way Streaming". Все работает.

Локально, с моего компьютера на котором установлен Docker, доступ к WCS серверу у меня был. Теперь нужно было дать доступ коллегам.

Замкнутая сеть

Внутренняя сеть Docker является изолированной, т.е. из сети докера доступ "в мир" есть, а "из мира" сеть докера не доступна.

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

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

Отлично! Список портов известен. Пробрасываем:

docker run \-e PASSWORD=password \-e LICENSE=license_number \-e LOCAL_IP=192.168.1.10 \-e EXTERNAL_IP=192.168.23.6 \-d -p8444:8444 -p8443:8443 -p1935:1935 -p30000-33000:30000-33000 \--net testnet --ip 192.168.1.10 \--name wcs-docker-test --rm flashphoner/webcallserver:latest

В этой команде используем следующие переменные:

PASSWORD, LICENSE и LOCAL_IP мы рассмотрели выше;

EXTERNAL_IP IP адрес внешнего сетевого интерфейса. Записывается в параметр ip в файле настроек flashphoner.properties;

Так же в команде появляются ключи -p это и есть проброс портов. В этой итерации используем ту же сеть "testnet", которую мы создали раньше.

В браузере на другом компьютере открываю https://192.168.23.6:8444 (IP адрес моей машины с Docker) и запускаю пример "Two Way Streaming"

Web интерфейс WCS работает и даже WebRTC трафик ходит.

И все было бы прекрасно, если бы не одно но!

Ну что ж так долго!

Контейнер с включенным пробросом портов запускался у меня около 10 минут. За это время я бы успел вручную поставить пару копий WCS. Такая задержка происходит из-за того, что Docker формирует привязку для каждого порта из диапазона.

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

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

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

Запускаем контейнер в сети хоста (на это указывает ключ --net host)

docker run \-e PASSWORD=password \-e LICENSE=license_number \-e LOCAL_IP=192.168.23.6 \-e EXTERNAL_IP=192.168.23.6 \--net host \--name wcs-docker-test --rm -d flashphoner/webcallserver:latest

Отлично! Контейнер запустился быстро. С внешней машины все работает - и web интерфейс и WebRTC трафик публикуется и воспроизводится.

Потом я запустил еще пару контейнеров. Благо на моем компьютере несколько сетевых карт.

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

Рабочий вариант

Начиная с версии 1.12 Docker предоставляет два сетевых драйвера: Macvlan и IPvlan. Они позволяют назначать статические IP из сети LAN.

  • Macvlan позволяет одному физическому сетевому интерфейсу (машине-хосту) иметь произвольное количество контейнеров, каждый из которых имеет свой собственный MAC-адрес.

    Требуется ядро Linux v3.93.19 или 4.0+.

  • IPvlan позволяет создать произвольное количество контейнеров для вашей хост машины, которые имеют один и тот же MAC-адрес.

    Требуется ядро Linux v4.2 + (поддержка более ранних ядер существует, но глючит).

Я использовал в своей инсталляции драйвер IPvlan. Отчасти, так сложилось исторически, отчасти у меня был расчет на перевод инфраструктуры на VMWare ESXi. Дело в том, что для VMWare ESXi доступно использование только одного MAC-адреса на порт, и в таком случае технология Macvlan не подходит.

Итак. У меня есть сетевой интерфейс enp0s3, который получает IP адрес от DHCP сервера.

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

Что бы этого избежать нужно зарезервировать часть диапазона подсети для использования Docker. Это решение состоит из двух частей:

  1. Нужно настроить службу DHCP в сети таким образом, чтобы она не назначала адреса в некотором определенном диапазоне.

  2. Нужно сообщить Docker об этом зарезервированном диапазоне адресов.

В этой статье я не буду рассказывать, как настраивать DHCP сервер. Думаю, каждый айтишник в своей практике сталкивался с этим не единожды, в крайнем случае, в сети полно мануалов.

А вот как сообщить Docker, какой диапазон для него выделен, разберем подробно.

Я ограничил диапазон адресов DHCP сервера так, что он не выдает адреса выше 192.168.23. 99. Отдадим для Docker 32 адреса начиная с 192.168.23.100.

Создаем новую Docker сеть с названием "new-testnet":

docker network create -d ipvlan -o parent=enp0s3 \--subnet 192.168.23.0/24 \--gateway 192.168.23.1 \--ip-range 192.168.23.100/27 \new-testnet

где:

ipvlan тип сетевого драйвера;

parent=enp0s3 физический сетевой интерфейс (enp0s3), через который будет идти трафик контейнеров;

--subnet подсеть;

--gateway шлюз по умолчанию для подсети;

--ip-range диапазон адресов в подсети, которые Docker может присваивать контейнерам.

и запускаем в этой сети контейнер с WCS

docker run \-e PASSWORD=password \-e LICENSE=license_number \-e LOCAL_IP=192.168.23.101 \-e EXTERNAL_IP=192.168.23.101 \--net new-testnet --ip 192.168.23.101 \--name wcs-docker-test --rm -d flashphoner/webcallserver:latest

Проверяем работу web интерфейса и публикацию/воспроизведение WebRTC трафика с помощью примера "Two-way Streaming":

Есть один маленький минус такого подхода. При использовании технологий Ipvlan или Macvlan Docker изолирует контейнер от хоста. Если, например, попробовать пропинговать контейнер с хоста, то все пакеты будут потеряны.

Но для моей текущей задачи запуска WCS в контейнере это не критично. Всегда можно запустить пинг или подключиться по ssh с другой машины.

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

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

Ссылки

WCS в Docker

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

Образ WCS на DockerHub

Подробнее..

WebRTC CDN на Google Cloud Platform с балансировкой и автоматическим масштабированием

18.06.2021 10:22:12 | Автор: admin

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

Кратко напомним основные тезисы:

  • В CDN низкая задержка в трансляциях обеспечивается использованием технологии WebRTC для передачи видеопотока от Origin сервера к Edge серверам, которые, в свою очередь, позволяют подключить большое количество зрителей.

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

Google Cloud Platform - это стек облачных сервисов, которые выполняются на той же самой инфраструктуре, которую Google использует для своих продуктов. То есть, получается, что пользовательские приложения запущенные в среде Google Cloud Platform, крутятся на тех же серверных мощностях, что и сам "великий и ужасный" Google. Поэтому, можно, с большой вероятностью, ожидать бесперебойной работы серверной инфраструктуры.

Инфраструктура Google Cloud Platform, как и в случае с AWS, поддерживает автоматическое масштабирование и балансировку распределения нагрузки, поэтому не приходится беспокоиться о лишних расходах на оплату виртуальных серверов вы платите только за то, что используете.

На момент написания статьи образа для быстрого развертывания WCS в GCP Marketplace не было. Но есть возможность автоматического развертывания WCS из образа виртуальной машины, который был подготовлен вручную. В этом случае, для WCS используется стандартная ежемесячная лицензия.

Теперь рассмотрим, как развернуть WCS CDN с балансировщиком и автоматическим масштабированием и процесс тестирования развернутой системы.

Разворачиваем WebRTC CDN с балансировщиком и автоматическим масштабированием на Google Cloud Platform

Конфигурация CDN будет следующей:

  • один Origin сервер;

  • один Edge в составе балансировщика нагрузки, который позволяет во время пиковой нагрузки увеличить число Edge серверов до трех.

Для развертывания потребуется настроить следующие компоненты в консоли Google Cloud Platform:

  • глобальный файрволл на уровне проекта Google Cloud;

  • виртуальные машины WCS CDN Origin и WCS CDN Edge;

  • шаблон развертывания на основе образа диска WCS CDN Edge;

  • группу масштабирования;

  • балансировщик нагрузки.

Итак, приступим.

Настраиваем глобальный файрволл на уровне проекта Google Cloud для прохождения WebRTC трафика

Настройка межсетевого экрана действует на все запущенные в вашем проекте сервера, поэтому начнем развертывание с нее.

В основном меню консоли Google Cloud откройте раздел "VPC networks" и выберите пункт "Firewall":

На открывшейся странице нажмите кнопку "Create Firewall Rule" :

В открывшемся мастере задайте имя правила:

Ниже на странице разрешите входящий трафик с любых компьютеров:

Еще ниже в секции "Protocols and ports" укажите порты для работы WCS и нажмите кнопку "Create":

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

Разворачиваем WCS сервер с ролью Origin для WebRTC CDN

В консоли Google Cloud откройте раздел "Compute Engine" и выберите из меню в левой части пункт "VM instances". Нажмите кнопку "Create" в диалоге создания нового экземпляра сервера:

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

Ниже на странице в секции "Boot disk" нажмите кнопку "Change" и выберите образ "CentOS 7":

Разверните секцию "Management, security, disks, networking, sole tenancy":

На вкладке "Security" добавьте публичный ключ для доступа к серверу по SSH:

На вкладке "Networking" в секции "Network interfaces" настройте внешний и внутренний IP адреса для сервера. Для работы в составе CDN серверу нужно назначить статический внутренний IP адрес:

После всех настроек нажмите кнопку "Create" для создания нового экземпляра WCS сервера с ролью CDN Origin:

Спустя пару минут сервер будет создан и запущен. Подключаемся к нему по ssh и устанавливаем WCS. Все действия - установка, изменение настроек, запуск или перезапуск WCS - должны выполняться с root правами, либо через sudo.

1.Установите Wget, Midnight Commander и дополнительные инструменты и библиотеки

sudo yum -y install wget mc tcpdump iperf3 fontconfig

2.Установите JDK. Для работы в условиях больших нагрузок рекомендуется JDK 12 или 14. Удобнее провести установку при помощи скрипта на bash. Текст скрипта:

#!/bin/bashsudo rm -rf jdk*curl -s https://download.java.net/java/GA/jdk12.0.2/e482c34c86bd4bf8b56c0b35558996b9/10/GPL/openjdk-12.0.2_linux-x64_bin.tar.gz | tar -zx[ ! -d jdk-12.0.2/bin ] && exit 1sudo mkdir -p /usr/java[ -d /usr/java/jdk-12.0.2 ] && sudo rm -rf /usr/java/jdk-12.0.2sudo mv -f jdk-12.0.2 /usr/java[ ! -d /usr/java/jdk-12.0.2/bin ] && exit 1sudo rm -f /usr/java/defaultsudo ln -sf /usr/java/jdk-12.0.2 /usr/java/defaultsudo update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-12.0.2/bin/java" 1sudo update-alternatives --install "/usr/bin/jstack" "jstack" "/usr/java/jdk-12.0.2/bin/jstack" 1sudo update-alternatives --install "/usr/bin/jcmd" "jcmd" "/usr/java/jdk-12.0.2/bin/jcmd" 1sudo update-alternatives --install "/usr/bin/jmap" "jmap" "/usr/java/jdk-12.0.2/bin/jmap" 1sudo update-alternatives --set "java" "/usr/java/jdk-12.0.2/bin/java"sudo update-alternatives --set "jstack" "/usr/java/jdk-12.0.2/bin/jstack"sudo update-alternatives --set "jcmd" "/usr/java/jdk-12.0.2/bin/jcmd"sudo update-alternatives --set "jmap" "/usr/java/jdk-12.0.2/bin/jmap"

3.Загрузите архив для установки самой свежей стабильной версии WebCallServer:

sudo wget https://flashphoner.com/download-wcs5.2-server.tar.gz

4.Распакуйте архив и запустите скрипт установки WCS и следуйте указаниям мастера установки. Для запуска WCS вам потребуется действительная лицензия. Для правильной работы команды, укажите номер сборки скачанного вами архива:

sudo tar -xvzf FlashphonerWebCallServer-5.2.714.tar.gz && cd FlashphonerWebCallServer-5.2.714 && ./install.sh

5.Для активации лицензии запустите скрипт "./activation.sh" из каталога установки WCS. Этот шаг, при желании, можно пропустить и активировать лицензию позже через веб-интерфейс:

sudo cd /usr/local/FlashphonerWebCallServer/bin && sudo ./activation.sh

6.Отключите firewalld и SELinux. Сетевой экран мы ранее настроили на уровне Google Cloud Platform, поэтому нет необходимости закрывать порты в операционной системе:

sudo systemctl stop firewalld && systemctl disable firewalld && setenforce 0

7.Откройте любым удобным редактором файл flashphoner.properties, который можно найти по пути:

/usr/local/FlashphonerWebCallServer/conf/flashphoner.properties

и внесите в него настройки для запуска CDN. В параметре "cdn_ip" укажите внутренний IP адрес вашей виртуальной машины с ролью CDN Origin:

cdn_enabled=truecdn_ip=10.128.0.3 # Local IP address CDN Origincdn_nodes_resolve_ip=falsecdn_role=origin

На скриншоте ниже примерный вид файла flashphoner.properties для WCS с ролью CDN Origin:

После изменения настроек запустите (или перезапустите) Web Call Server:

systemctl start webcallserver

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

Запускаем балансировщик нагрузки и автоматическое масштабирование в Google Cloud для WebRTC CDN

Для запуска балансировщика и автоматического масштабирования нужны следующие компоненты:

  • образ диска, который будет использоваться в шаблоне при создании нового экземпляра WCS;

  • шаблон, на основе которого будут создаваться новые экземпляры сервера при масштабировании;

  • группа масштабирования;

  • балансировщик нагрузки;

  • настройки контроля активности сервера.

Создаем образ диска WCS сервера с ролью Edge для WebRTC CDN

Начнем настройку с развертывания WCS сервера с ролью CDN Edge, для того, что бы создать на его основе образ диска для шаблона создания новых экземпляров WCS в балансировщике.

Повторите инструкцию по подготовке сервера Origin до пункта о внесении настроек в файл flashphoner.properties. Для роли Edge внесите в этот файл следующие настройки:

cdn_enabled=truecdn_ip=10.128.0.4cdn_nodes_resolve_ip=falsecdn_point_of_entry=10.128.0.3cdn_role=edgehttp_enable_root_redirect=false

После внесения и сохранения настроек, остановите в консоли Google Cloud виртуальную машину WCS CDN Edge, выберите из меню в левой части пункт "Images" и нажмите кнопку "Create Image":

В открывшемся мастере укажите имя нового образа, выберите в качестве источника диск виртуальной машины WCS CDN Edge и нажмите кнопку "Create":

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

Создаем шаблон развертывания Edge сервера

Выберите из меню в левой части окна консоли Google Cloud пункт "Instance templates" и нажмите кнопку "Create Instance template":

В открывшемся мастере создания шаблона развертывания укажите имя шаблона и выберите конфигурацию виртуальной машины:

Ниже на странице в секции "Boot disk" нажмите кнопку "Change". в Открывшемся окне перейдите на вкладку "Custom Images" и выберите образ диска WCS CDN Edge, который мы создали ранее. Нажмите кнопку "Select":

Разверните секцию "Management, security, disks, networking, sole tenancy". На вкладке "Security" добавьте публичный ключ для доступа к серверу по SSH и нажмите кнопку "Create":

Шаблон развертывания для WCS с ролью CDN Edge создан. Теперь перейдем к созданию группы масштабирования.

Создаем группы масштабирования для Edge серверов

Из меню в левой части окна консоли Google Cloud выберите пункт "Instance groups" и нажмите кнопку "Create Instance group":

На открывшейся странице выберите регион и зону расположения группы и укажите шаблон развертывания WCS Edge, который мы создали ранее:

В секции "Autoscaling" на этой же странице настройте триггер запуска дополнительных серверов Edge. В качестве триггера будем использовать загрузку процессора более 80% . В поле "Maximum number of instances" укажите максимальное количество виртуальных машин, которые будут запущены при срабатывании триггера:

Затем включите проверку состояния виртуальной машины в секции "Autohealing". Для того, что бы создать настройку проверки сервера выберите из списка в поле "Health check" пункт "Сreate a health check":

В открывшемся мастере создания проверки состояния сервера укажите имя проверки, протокол TCP, порт 8081 и запрос /health-check. Настройте критерии проверки и нажмите кнопку "Save and continue":

Разверните секцию "Advanced creation options" и активируйте чекбокс "Do not retry machine creation". После чего нажмите "Create":

Будет создана группа масштабирования и запущен один WCS с ролью CDN Edge. Последним этапом настройки нашей CDN с балансировщиком нагрузки и автоматическим масштабированием будет настройка балансировщика.

Создаем балансировщик нагрузки

Сначала зарезервируем для балансировщика внешний IP адрес. В главном меню Google Cloud Platform в секции "VPC network" выберите пункт "External IP addresses" и нажмите кнопку "Reserve static address":

На открывшейся странице в поле "Name" задаем имя для зарезервированного IP адреса. Выбираем уровень качества сетевых услуг для адреса и тип распространения. После завершения всех настроек нажимаем кнопку "Reserve":

Затем переходим к настройке балансировщика.

Выбираем пункт "Load balancing" в разделе "Network services" секции "Networking" основного меню Google Cloud Platform:

Нажимаем кнопку "Create load balancer":

Затем выберите тип балансировщика "TCP Load Balancing" и нажмите кнопку "Start configuration":

На открывшейся странице укажите внешний балансировщик "From Internet to my VMs" и регион размещения серверов балансировщика. После выбора настроек нажмите кнопку "Continue":

На следующей странице задайте имя балансировщика, Затем перейдите в раздел настроек "Backend configuration" и укажите в каком регионе будут созданы сервера входящие в состав балансировщика. На вкладке "Select existing instance groups" выберите группу масштабирования Edge серверов, которую мы создали ранее. Затем в поле "Health check"выберите из выпадающего списка пункт "Сreate a health check":

На открывшейся странице укажите параметры для проверки состояния работы балансировщика порт 8081 и запрос /, после чего нажмите кнопку "Save and continue":

Затем перейдите к настройкам раздела "Frontend configuration". В этом разделе нужно создать привязку портов к внешнему IP адресу. Укажите внешний IP адрес для балансировщика, который мы зарезервировали выше и создайте конфигурации для TCP портов 8081, 8080, 8443, 8444 для HTTP(S) и WS(S). После создания необходимых портов нажмите кнопку "Create":

Балансировщик будет запущен. На этом развертывание CDN с балансировщиком и масштабированием можно считать завершенным. Переходим к тестированию.

Тестирование WebRTC CDN с балансировщиком и масштабированием на базе Google Cloud Platform

Методика тестирования

Для проведения нагрузочного тестирования, при создании группы масштабирования мы выставили порог загрузки процессора для срабатывания триггера на 20%. Тестирование будем проводить с использованием браузера Google Chrome и виртуальной вебкамеры для организации трансляции видеопотока. Что бы сымитировать повышение нагрузки на процессор запустим воспроизведение потока с транскодированием с помощью примера "Media Devices".

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

Тестирование

В браузере Google Chrome открываем web интерфейс WCS с ролью CDN Origin Авторизуемся, открываем пример "Two-way Streaming", устанавливаем соединение с сервером по WebSocket и публикуем видеопоток.

Затем, запускаем web интерфейс WCS CDN Edge сервера по IP адресу, который был зарезервирован при создании балансировщика.

Авторизуемся, открываем пример "Media Devices" и устанавливаем соединение с балансировщиком по WebSocket. В правом столбце настроек снимаем чек бокс "default" для параметра "Size" и задаем значения для транскодирования видеопотока. Например, если поток на Origin сервере опубликован с размерами 320х240 задаем значение 640х480. Повторите действия в нескольких вкладках браузера, для имитации большого количества зрителей.

В консоли Google Cloud видим, что были запущены две дополнительные виртуальные машины:

Для того, что бы проверить, что балансировщик распределяет соединения между активными ВМ можно использовать страницу статистики, которая доступна по адресу:

http://<WCS instance IP address>:8081/?action=stat

Откройте страницу статистики для каждой виртуальной машины, запущенной балансировщиком. Значение "connection_websocket" показывает количество активных WebSocket сессий на каждой виртуальной машине.

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

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

Ссылки

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

CDN для стриминга WebRTC с низкой задержкой - CDN на базе WCS

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

Документация по развертыванию WCS в Google Cloud Platform

Документация по настройке балансировки нагрузки с масштабированием в GCP

Подробнее..

Перевод WebRTC для любопытных (часть 1)

02.06.2021 08:23:30 | Автор: admin

Данный материал - это вольный перевод онлайн-книги webrtcforthecurious.com. Вся книга будет разбита на отдельные статьи, которые будут выходить в разное время. В основном я это сделал для себя в будущем, так как в свободное от работы время почти целиком и полностью занимаюсь проектом в основе которого именно WebRTC.

Книга довольно поверхностно объясняет как работает WebRTC "под капотом", для подробностей надо читать RFC. Ссылки на RFC различных используемых протоколов буду приводить. Стоит особо отметить главу "Отладка", где неплохо описываются идеи того, как отлаживать различные проблемы с сетью, задержками и прочим.

Итак, часть 1 - вводная.


Что такое Webrtc?

Web Real-Time Communication или сокращенно WebRTC, является и протоколом и API одновременно. WebRTC как протокол - это множество правил для безопасного обмена информацией (обычно медиа) в режиме дуплекс между двумя агентами в сети. WebRTC как API в свою очередь позволяет разработчикам работать с протоколом. API формально определено только для JavaScript.

Такое же разделение и в отношении HTTP и Fetch API: WebRTC-протокол - это как HTTP, а WebRTC API - это как Fetch API в данном случае.

Помимо JavaScript протокол WebRTC реализован также и на других языках програмирования. Можно найти множество реализаций серверов, библиотек, реализующих протокол, примером может стать реализация на go pion/webrtc. Пишется реализация и на rust: https://github.com/webrtc-rs/webrtc (проект довольно интересный, потому что это переписываение pion/webrtc на rust).

Протокол WebRTC поддерживается в IETF в группе rtcweb. API WebRTC задокументировано в W3C как webrtc-pc.

Приемущества WebRTC

Далее приводится список того, что даст вам WebRTC. Список не полный, это просто примеры того, что заслуживает внимания. Не волнуйтесь, если какие-то слова вам не знакомы, в течение следующих частей разберем их подробнее.

Итак, приемущества WebRTC:

  • Открытый стандарт

  • Множество различных реализаций

  • Можно работать прямо из браузера

  • Обязательное шифрование

  • NAT Traversal

  • Перепрофилированная существующая технология, то есть не изобретали колес, когда делали WebRTC

  • Контроль за перегруженностью

  • Задержка (latency, имеется в виду задержка аудио и/или видеопотока) в пределах 1 секунды

WebRTC это набор разных технологий

Это тема, для объяснения которой потребуется целая книга. Для начала разобъем ее на четыре части:

  • Сигналинг

  • Соединение пиров

  • Безопасность

  • Общение пиров

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

Интересный факт в WebRTC это то, что каждый шаг использует множество других протоколов!

WebRTC объединияет множество существующих технологий. В этом смысле WebRTC - это скорее комбинация в различных конфигурациях хорошо изученных технологий, которые существуют с начала 2000-х годов.

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

Сигналинг или как агенты находят друг друга в сети

Когда запускается WebRTC-агент, он не знает с кем ему соединиться, и какого рода информацией он будет обмениваться. Сигналинг (Signaling) решает эту проблему! Сигналинг нужен для того, чтобы два агента могли найти и вызвать друг друга в сети перед тем, как начать обмен информацией.

Сигналинг использует существующий протокол SDP (Session Description Protocol). SDP это простой текстовый протокол. Каждое SDP-сообщение состоит из пар ключ-значение, расположенных в строгом порядке (rfc4566), которые в свою очередь составляют набор медиа-секций. SDP-сообщения, которыми обмениваются WebRTC-агенты содержит такую информацию как:

  • адреса IP и порты агентов, по которым можно соединиться с агентом (это т.н. ICE-кандидаты)

  • сколько аудио и видео треков агент желает отправить

  • какие аудио и видео кодеки поддерживает каждый из агентов

  • значения используемые во время соединения (uFrag/uPwd).

  • значения используемые для безопасности (отпечаток сертификата)

Отметим, что сигналинг обычно работает как бы в сторонке; то есть приложения не используют WebRTC для обмена SDP сообщениями. Тут подходит любой способ обмена этими сообщениями: REST, Websocket, да хоть письмом по почте можно отправить другому пиру SDP-сообщение, а тот в свою очередь отправит свое. В своем приложении для тестов я вообще использовал firebase для сигналинга.

Установка соединения и NAT Traversal с помощью STUN/TURN

Теперь у обоих сторон WebRTC агентов достаточно информации о том, чтобы соединиться друг с другом. Далее используется другая устоявшаяся технология под названием ICE.

ICE (Interactive Connectivity Establishment) - это протокол предваряющий WebRTC. ICE позволяет установить связь между двумя агентами. Эти агенты могуть быть в одной и той же сети, а могут быть и на противоположных концах света. ICE решает проблему установления прямой связи без использования промежуточного сервера.

Настоящая магия здесь это т.н. NAT Traversal и STUN/TURN сервера. Обе эти концепции необходимы для соединения с ICE агентом из другой сетки. Далее мы изучим этот вопрос глубже.

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

Шифрование передачи информации с помощью DTLS и SRTP

После того как мы установили дуплексную связь между двумя пирами через ICE, нам необходимо установить шифрованное соединение для обеспечения безопасности при передаче данных. Это обеспечиватся двумя протоколами, которые также предваряют WebRTC: DTLS (Datagram Transport Layer Security), который просто добавляет слой TLS над UPD. (TLS - криптографический протокол используемый для безопасного обмена через https). Второй протокол - это SRTP (Secure Real-time Transport Protocol).

Сначала WebRTC выполняет DTLS-"рукопожатие" через соединение установленное ранее через ICE. В отличие от HTTPS WebRTC не использует CA для сертификатов. Вместо этого просто сверяет отпечатки сертификатов, полученных в ходе обмена SDP-сообщениями на этапе сигналинга. Установленное DTLS соединение далее используется для DataChannel, для обмена простыми данными - бинарными или текстовыми, например сообщения в чате.

Для видео/аудио в WebRTC используется другой протокол: RTP. Для шифрования RTP-пакетов используется протокол SRTP. SRTP сессия инициализируется с помощью ключей шифрования полученных в ходе DTLS сессии (rfc5764). Далее мы обсудим, почему для медиа-данных используется свой собственный протокол.

Теперь все готово! У нас есть двунаправленный и безопасный канал. Если у вас стабильное соединение между вашими WebRTC-агентами, то вышеописанный комплекс процедур достаточен чтобы начать им (агентам) общаться. Однако в жизни все не так идеально, как кажется: мы постоянно будем сталкиваться с потерей пакетов в сети, ограниченной пропускной способностью сети. Дальше мы подумаем, как справляться со всеми этими проблемами.

Общение между пирами через RTP и SCTP

Сейчас мы имеем два WebRTC-агента с безопасным двунаправленным соединением. Давайте начнем взаимодействие! И снова мы используем уже существующие протоколы: RTP (Real-time Transport Protocol), и SCTP (Stream Control Transmission Protocol). Используйте RTP для обмена аудио/видео шифрованным по протоколу SRTP и SCTP для обмена DataChannel-сообщениями, шифрованными с помощью DTLS.

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

Последний протокол в стеке протоколов WebRTC - это SCTP. SCTP дает множество возможностей для управления доставкой сообщений. Например, это может быть ненадежная неупорядоченная доставка сообщений, для того, чтобы получить задержку, удовлетворительную для систем реального времени.

WebRTC это набор протоколов

WebRTC решает множество проблем коммуникаций в реальном времени. На первый взгляд этот протокол может показаться овер-инжинирингом. Гениальность WebRTC - это так сказать скромность. При создании протокола не было цели решить все идеально. Вместо этого WebRTC взял множество существующих технологий, направленных на решение конкретной задачи, и объединил их вместе. На рисунке 1 ниже представлена диаграмма WebRTC агента. Как видно из схемы, WebRTC-агент на самом деле представляет собой оркестрацию различных протоколов.

Рис.1. WebRTC Agent DiagramРис.1. WebRTC Agent Diagram

Кратко: как работает WebRTC (API)

В этой части показано как JavaScript API отображается на протокол. Это не демонстрация WebRTC API, а скорее некоторый набросок для создания у вас ментальной модели, как все работает вместе. Если вы не знакомы с каким-либо из пунктов, не переживайте, можете вернуться сюда когда узнаете больше!

new RTCPeerConnection

RTCPeerConnection - это основа для установления WebRTC-сессии. Объект RTCPeerConnection реализует "под капотом" все протоколы, упомянутые выше. Здесь инициализируются все подсистемы, и пока что ничего больше не происходит.

addTrack

Метод addTrack создает новый RTP-поток. Для потока генерируется случайный Synchronization Source (SSRC). Созданный RTP поток будет затем описан в Session Description-сообщении внутри медиа-секции после вызова createOffer метода. Каждый вызов addTrack создает новый SSRC и добавляет медиа-секцию в SDP-сообщение.

Сразу после того, как SRTP сессия установлена, зашифрованные медиа-пакеты начнут отправляеться через ICE.

createDataChannel

createDataChannel создает новый SCTP-поток, если еще не был добавлен. По умолчанию SCTP выключен, но инициализируется как только одна из сторон потребует data channel.

Сразу после того, как DTLS сессия установлена, SCTP пакеты начнут отправляться через ICE.

createOffer

createOffer генерирует Session Description для отправки удаленному пиру.

Вызов createOffer ничего не меняет на локальном пире.

setLocalDescription

setLocalDescription фиксирует все, что менялось в созданном RTCPeerConnection для локального пира. Методы addTrack, createDataChannel и другие осуществляют временные изменения до тех пор, пока метод setLocalDescription не будет вызван. В этот метод нужно передавать строку session description сгенерированную методомcreateOffer.

После вызова setLocalDescription сгенерированное SDP-сообщение также отправляется на удаленный пир (выше обусждалось, что это можно делать любым способом), и далее на удаленном пире SDP-сообщение (offer) передается в метод setRemoteDescription. Удаленный пир в свою очередь отправляет свой локальный SDP в ответ (answer), который также нужно передать локально в setRemoteDescription.

addIceCandidate

addIceCandidate позволяет WebRTC-агенту добавить больше удаленных ICE-кандидатов.

ontrack

ontrack - это колбек (функция обратного вызова), который срабатывает как только RTP-пакет был получен от удаленного пира.

oniceconnectionstatechange

oniceconnectionstatechange - это также колбек, который отражает состояние ICE агента. Любые проблемы с сетью отражаются через этот колбек.

onstatechange

onstatechange - этот колбек служит для отслеживания окончания сбора всех ICE-кандидатов. Как только аргумент этого обратного вызова станет null, все ICE-кандидаты собраны.

В следующей части разберем Signaling и SDP.

Подробнее..

Перевод RED Улучшение качества звука с помощью резервирования

04.09.2020 14:06:16 | Автор: admin

Еще в апреле 2020 года Citizenlab сообщил о довольно слабом шифровании Zoom и заявил, что Zoom использует аудиокодек SILK. К сожалению, статья не содержала исходных данных, чтобы это подтвердить и дать мне возможность обращаться к ней в дальнейшем. Однако благодаря Натали Сильванович из Google Project Zero и инструменту трассировки Frida я смог получить дамп некоторых необработанных кадров SILK. Их анализ вдохновил меня взглянуть на то, как WebRTC обрабатывает звук. Что касается восприятия качества вызова в целом, больше всего на него влияет качество звука, поскольку мы склонны замечать даже небольшие сбои. Всего десяти секунд анализа было достаточно, чтобы отправиться в настоящее приключение на поиски возможных улучшений качества звука, обеспечиваемых WebRTC.

Я имел дело с нативным клиентом Zoom еще в 2017 году (до поста DataChannel) и обратил внимание, что его аудиопакеты иногда были очень большими в сравнении с пакетами решений на базе WebRTC:

На приведенном выше графике показано количество пакетов с определенной длиной полезной нагрузки UDP. Пакеты длиной от 150 до 300 байт необычное явление, если сравнивать с типичным вызовом WebRTC. Они намного длиннее, чем пакеты, которые мы обычно получаем от Opus. Мы заподозрили наличие упреждающего контроля ошибок (FEC) или резервирования, но без доступа к незашифрованным кадрам было трудно сделать еще какие-то выводы или что-то предпринять.

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

packet 7:e9e4ab17ad8b9b5176b1659995972ac9b63737f8aa4d83ffc3073d3037b452fe6e1ee5e6e68e6bcd73adbd59d3d31ea5fdda955cbb7fpacket 8: e790ba4908639115e02b457676ea75bfe50727bb1c44144d37f74756f90e1ab926ef930a3ffc36c6a8e773a780202af790acfbd6a4dff79698ea2d96365271c3dff86ce6396203453951f00065ec7d26a03420496fpacket 9:e93997d503c0601e918d1445e5e985d2f57736614e7f1201711760e4772b020212dc854000ac6a80fb9a5538741ddd2b5159070ebbf79d5d83363be59f10efe790ba4908639115e02b457676ea75bfe50727bb1c44144d37f74756f90e1ab926ef930a3ffc36c6a8e773a780202af790acfbd6a4dff79698ea2d96365271c3dff86ce6396203453951f00065ec7d26a03420496fe9e4ab17ad8b9b5176b1659995972ac9b63737f8aa4d83ffc3073d3037b452fe6e1ee5e6e68e6bcd73adbd59d3d31ea5fdda955cbaef

Пакет 9 содержит два предыдущих пакета, пакет 8 1 предыдущий пакет. Такая избыточность вызвана использованием формата LBRR Low Bit-Rate Redundancy, как показало глубокое изучение декодера SILK (который можно найти в интернет-проекте, представленном командой Skype или в репозитории GitHub):


Zoom использует SKP_SILK_LBRR_VER1, но с двумя резервными пакетами. Если каждый UDP пакет содержит не только текущий аудиокадр, но и два предыдущих, он будет устойчивым, даже если вы потеряете два из трех пакетов. Так, может быть, ключ к качеству звука Zoom это секретный рецепт бабушкиного Skype?

Opus FEC


Как добиться того же с помощью WebRTC? Следующим очевидным шагом было рассмотрение Opus FEC.

LBRR, низкоскоростное резервирование, от SILK, также есть и в Opus (помните, что Opus это гибридный кодек, который использует SILK для нижнего конца диапазона битрейта). Тем не менее Opus SILK сильно отличается от оригинального SILK, исходный код которого когда-то открыл Skype, как и часть LBRR, которая используется в режиме контроля ошибок.

В Opus контроль ошибок не просто добавляется после исходного аудиокадра, он предшествует ему и кодируется в потоке битов. Мы пробовали экспериментировать с добавлением нашего собственного контроля ошибок с помощью Insertable Streams API, но это требовало полного перекодирования для вставки информации в битовый поток перед фактическим пакетом.


Хотя усилия и не увенчались успехом, они позволили собрать некоторые статистические данные о влиянии LBRR, которые продемонстрированы на рисунке выше. LBRR использует битрейт до 10 кбит/с (или две трети скорости передачи данных) при больших потерях пакетов. Репозиторий доступен по ссылке. Данная статистика не отображается при вызове WebRTC getStats() API, поэтому результаты оказались весьма занимательными.

Помимо проблемы с необходимостью перекодировки, Opus FEC в WebRTC, как выяснилось, настроен несколько бесполезно:

  • Он активируется только при потере пакетов, а мы хотели бы, чтобы резервная информация хранилась и до возникновения каких-либо проблем. Ребята из Slack писали об этом еще в 2016 году. Это означает, что мы не можем включить его по умолчанию и защитить себя от случайных нерегулярных потерь.
  • Объем прямого исправления ошибок ограничен 25%. Выше этого значения оно неэффективно.
  • Битрейт для FEC вычитается из целевого максимального битрейта (см. здесь).

Вычитание битрейта FEC из целевого максимального битрейта совершенно не имеет смысла FEC активно снижает битрейт основного потока. Поток с более низким битрейтом обычно приводит к снижению качества. Если нет потери пакетов, которую можно исправить с помощью FEC, то FEC только ухудшит качество, а не улучшит его. Почему так происходит? Основная теория состоит в том, что одной из причин потери пакетов является перегрузка. Если вы столкнулись с перегрузкой, вы не захотите отправлять больше данных, потому что это только усугубит проблему. Однако, как описывает Эмиль Ивов в своем замечательном выступлении на KrankyGeek от 2017 года, перегрузка не всегда является причиной потери пакетов. Кроме того, этот подход также игнорирует любые сопутствующие видеопотоки. Стратегия FEC на основе перегрузок для аудио Opus не имеет особого смысла, когда вы отправляете сотни килобит видео вместе с относительно небольшим потоком Opus со скоростью 50 кбит/с. Возможно, в будущем мы увидим какие-то изменения в libopus, а пока хотелось бы попробовать отключить его, ведь в настоящее время он включен в WebRTC по умолчанию.

Делаем вывод, что это не работает

RED


Если нам нужно реальное резервирование, у RTP есть решение под названием RTP Payload for Redundant Audio Data, или RED. Оно довольно старое, RFC 2198 был написан в 1997 году. Решение позволяет помещать несколько полезных нагрузок RTP с различными временными метками в один и тот же RTP пакет при относительно небольших затратах.

Использование RED для помещения одного или двух резервных аудиокадров в каждый пакет дало бы гораздо большую устойчивость к потере пакетов, чем Opus FEC. Но это возможно лишь путем удвоения или утроения битрейта аудио с 30 кбит/с до 60 или 90 кбит/с (с дополнительными 10 кбит/с для заголовка). Хотя по сравнению с более чем 1 мегабитом видеоданных в секунду это не так уж плохо.

Библиотека WebRTC включала в себя второй кодер и декодер для RED, что теперь стало излишним! Несмотря на попытки удалить неиспользуемый audio-RED-code, мне удалось применить этот кодер, прилагая относительно небольшие усилия. Полная история решения есть в системе отслеживания багов WebRTC.

И оно доступно в виде пробной версии, включаемой при запуске Chrome со следующими флагами:
--force-fieldtrials=WebRTC-Audio-Red-For-Opus/Enabled/

Затем RED может быть включен через SDP согласование; он отобразится следующим образом:
a=rtpmap:someid red/48000/2

По умолчанию он не включен, поскольку есть среды, где использование дополнительной пропускной способности не очень хорошая идея. Чтобы использовать RED, измените порядок следования кодеков так, чтобы он был перед кодеком Opus. Это можно сделать, используя API RTCRtpTransceiver.setCodecPreferences, как показано здесь. Очевидно, что другой альтернативой является изменение SDP вручную. Формат SDP также мог бы обеспечить способ настройки максимального уровня резервирования, но семантика предложение-ответ в RFC 2198 была не до конца ясна, поэтому я решил отложить это на время.

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


По умолчанию битрейт полезной нагрузки (красная линия) почти в два раза выше, чем без резервирования на скорости почти 60 кбит/сек. DTX (прерывистая передача) это механизм сохранения пропускной способности, который посылает пакеты только при обнаружении голоса. Как и ожидалось, при использовании DTX влияние битрейта несколько смягчается, как мы видим в конце вызова.



Проверка длины пакетов демонстрирует ожидаемый результат: пакеты в среднем в два раза длиннее (выше) по сравнению с нормальным распределением длины полезной нагрузки, показанным ниже.

Это все еще немного отличается от того, что делает Zoom, где мы наблюдали частичное резервирование. Давайте повторим график длины пакетов Zoom, приведенный ранее, чтобы посмотреть на них в сравнении:

Добавление поддержки обнаружения голосовой активности (VAD)


Opus FEC отправляет резервные данные только в том случае, если в пакете присутствует голосовая активность. То же самое должно быть применено и к реализации RED. Для этого кодировщик Opus должен быть изменён для отображения корректной информации о VAD, которая определяется на уровне SILK. При такой настройке битрейт достигает 60 кбит/с только при наличии речи (в сравнении с постоянными 60+ кбит/с):


и спектр становится больше похож на то, что мы видели у Zoom:


Изменение, позволяющее этого достичь, еще не появилось.

Поиск правильного расстояния


Расстояние это количество резервных пакетов, то есть количество предыдущих пакетов в текущем. В процессе работы над поиском правильного расстояния мы обнаружили, что если RED с расстоянием 1 это круто, то RED с расстоянием 2 еще круче. Наша лабораторная оценка моделировала случайную потерю пакетов = 60%. В этой среде Opus + RED воспроизводил отличный звук, в то время как Opus без RED показывал себя сильно хуже. WebRTC getStats() API дает очень полезную возможность измерить это, сравнивая процент скрытых сэмплов, получаемый путем деления concealedSamples на totalSamplesReceived.

На странице аудиосэмплов эти данные легко получить с помощью данного фрагмента кода JavaScript, вставленного в консоль:
(await pc2.getReceivers()[0].getStats()).forEach(report => {  if(report.type === "track") console.log(report.concealmentEvents, report.concealedSamples, report.totalSamplesReceived, report.concealedSamples / report.totalSamplesReceived)})

Я провел пару тестов с потерей пакетов, используя не очень известный, но очень полезный флаг WebRTCFakeNetworkReceiveLossPercent:
--force-fieldtrials=WebRTC-Audio-Red-For-Opus/Enabled/WebRTCFakeNetworkReceiveLossPercent/20/

При 20% потерях пакетов и включенном по умолчанию FEC не было большой разницы в качестве звука, но была небольшая разница в метрике:
сценарий процент потерь
без red 18%
без red, FEC отключен 20%
red с расстоянием 1 4%
red с расстоянием 2 0.7%

Без RED или FEC метрика почти совпадает с запрошенной потерей пакетов. Есть эффект от FEC, но он невелик.

Без RED при потере 60% качество звука стало довольно плохим, немного металлическим, а слова трудными для понимания:
сценарий процент потерь
без red 60%
red с расстоянием 1 32%
red с расстоянием 2 18%

Были некоторые слышимые артефакты при RED с расстоянием = 1, но почти идеальный звук с расстоянием 2 (что является количеством избыточности, которое используется в настоящее время).
Есть ощущение, что человеческий мозг может выдержать какой-то определенный уровень тишины, возникающей нерегулярно. (А Google Duo, судя по всему, использует алгоритм машинного обучения, чтобы чем-то тишину заполнить).

Измерение производительности в реальном мире


Мы надеемся, что включение RED в Opus улучшит качество звука, хотя в отдельных случаях может сделать и хуже. Эмиль Ивов вызвался провести пару тестов прослушивания по методу POLQA-MOS. Это было сделано в прошлом для Opus, так что у нас есть исходные данные для сравнения.
Если первоначальные тесты покажут многообещающий результат, то мы проведем масштабный эксперимент на основной развертке Jitsi Meet, применяя процентные метрики потерь, которые мы использовали выше.

Обратите внимание, что для медиасерверов и SFU включение RED происходит немного сложнее, поскольку серверу может потребоваться управлять RED ретрансляцией для выбора клиентов, как в случае, если не у всех клиентов поддерживаются конференции RED. Также некоторые клиенты могут находиться на канале с ограниченной пропускной способностью, где RED не требуется. Если конечная точка не поддерживает RED, SFU может удалить ненужное кодирование и отправить Opus без обертки. Аналогичным образом он может реализовать сам RED и использовать его при повторной отправке пакетов от конечной точки, передающей Opus, на конечную точку, поддерживающую RED.

Большое спасибо Jitsi/88 Inc за спонсирование этого приключения и ребятам из Google, которые рассмотрели/предоставили фидбек о необходимых изменениях.

А без Натали Сильванович я бы так и застрял, глядя на зашифрованные байты!
Подробнее..

Перевод Облачный гейминг с открытым исходным кодом на WebRTC p2p, мультиплеер, zero latency

20.07.2020 14:14:41 | Автор: admin

ПО как услуга, инфраструктура как услуга, платформа как услуга, коммуникационная платформа как услуга, видеоконференции как услуга, а что насчет облачных игр как услуги? Уже было предпринято несколько попыток создания облачных игр (Cloud Gaming), например, Stadia, недавно запущенная компанией Google. Stadia не новичок в WebRTC, но могут ли другие использовать WebRTC так же?

Тхань Нгуен (Thanh Nguyen) решил проверить эту возможность на своем опенсорсном проекте CloudRetro. CloudRetro основан на Pion, популярной WebRTC библиотеке на базе Go (спасибо Шону из группы разработчиков Pion за помощь в подготовке этой статьи). В данной статье Тхань делает обзор архитектуры своего проекта, а также рассказывает, что полезного он узнал и с какими челленджами столкнулся во время работы.

Вступление


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

TLDR: короткая слайд-версия с основными моментами

Почему за облачными играми будущее


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

Google Stadia, по сути, позволяет играть в AAA-игры (т.е. высококлассные игры-блокбастеры) на интерфейсе вроде YouTube. Та же методология может быть применена и к другим тяжелым оффлайновым приложениям, таким как операционная система или 2D/3D графический дизайн и т.д. чтобы мы могли стабильно запускать их на устройствах с низкими техническими характеристиками на разных платформах.


Будущее этой технологии: представляете, если бы Microsoft Windows 10 работал в браузере Chrome?

Облачные игры технически сложны


Гейминг одна из тех редких областей, где требуется постоянная быстрая реакция пользователя. Если изредка мы встречаемся с задержкой в 2 секунды при клике на странице, это приемлемо. Видеопотоки в прямом эфире, как правило, отстают на несколько секунд, но все равно предлагают достаточное удобство в использовании. Однако, если игра часто задерживается на 500 мс, играть просто невозможно. Наша цель достичь чрезвычайно низкой задержки, чтобы разрыв между вводом и медиа был как можно меньше. Поэтому традиционный подход к потоковому видео здесь неприменим.


Общий шаблон облачной игры

Опенсорсный проект CloudRetro


Я решил создать тестовый образец облачной игры, чтобы проверить, возможно ли всё это при таких жестких сетевых ограничениях. Для проверки концепции я выбрал Golang, поскольку это наиболее знакомый мне язык и хорошо подходящий для данной реализации по многим другим причинам, как выяснилось позже. Go простой и развивается очень быстро; каналы в Go отлично подходят для управления многопоточностью.

Проект CloudRetro.io облачный игровой сервис с открытым исходным кодом для ретро-игры. Цель проекта привнести в традиционные ретро-игры наиболее комфортные игровые ощущения и добавить мультиплеер.
Подробно ознакомиться с проектом можно здесь: https://github.com/giongto35/cloud-game.

Функциональность CloudRetro


Для демонстрации всей мощи облачных игр в CloudRetro используются ретро-игры. Что позволяет получить множество уникальных игровых впечатлений.

  • Портативность игры
    • Мгновенное воспроизведение при открытии страницы; загрузка и установка не нужны
    • Работает в мобильном браузере, так что для запуска не нужно никакое программное обеспечение

  • Игровые сеансы можно совместно использовать на нескольких устройствах и хранить в облаке для следующего входа
  • Игру можно стримить, а можно играть в нее сразу несколькими пользователями:
    • Crowdplay типа TwitchPlayPokemon, только более кросплатформенный и более риалтаймовый
    • Оффлаин игры в онлаине. Играть могут много пользователеи без настроики сети. В Samurai Shodown теперь можно играть 2 игрокам по сети CloudRetro


    Демо-версия многопользовательской онлайн-игры на разных устройствах

    Инфраструктура


    Требования и стек технологий


    Ниже приведен список требований, которые я установил перед началом проекта.

    1. Один игрок
    Это требование может показаться не слишком важным и очевидным здесь, но это один из моих ключевых выводов, это позволяет облачным играм держаться как можно дальше от традиционных потоковых сервисов. Если мы сосредоточимся на однопользовательской игре, мы сможем избавиться от централизованного сервера или CDN, потому что нам не нужно делать потоковую передачу в массы. Вместо того, чтобы загружать потоки на поглощающий сервер или передавать пакеты на централизованный сервер WebSocket, сервисные потоки передаются пользователю напрямую через одноранговое соединение WebRTC.

    2. Медиапоток с низкой задержкой
    Читая про Stadia, я часто встречаю в некоторых статьях упоминание WebRTC. Я понял, что WebRTC выдающаяся технология, и она прекрасно подходит для использования в облачных играх. WebRTC это проект, который предоставляет веб-браузерам и мобильным приложениям связь в реальном времени через простой API. Он обеспечивает одноранговое соединение, оптимизирован для медиа и имеет встроенные стандартные кодеки, такие как VP8 и H264.

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

    3. Распределенная инфраструктура с географической маршрутизацией
    Вне зависимости от того, насколько оптимизирован алгоритм сжатия и код, сеть все равно является решающим фактором, который больше всего способствует задержке. Архитектура должна иметь механизм сопряжения ближайшего к пользователю сервера для сокращения времени приема-передачи (RTT). Архитектура должна иметь 1 координатора и несколько потоковых серверов, распределенных по всему миру: Запад США, Восток США, Европа, Сингапур, Китай. Все потоковые серверы должны быть полностью изолированы. Система может регулировать свое распределение, когда сервер присоединяется к сети или выходит из нее. Таким образом, при большом трафике, добавление дополнительных серверов позволяет осуществлять горизонтальное масштабирование.

    4. Браузерная совместимость
    Облачные игры предстают в наилучшем свете, когда требует от пользователей по минимуму. Это значит, что есть возможность запуска в браузере. Браузеры помогают сделать игровой процесс максимально комфортным для пользователей, избавив их от установки программного и аппаратного обеспечения. Браузеры также помогают обеспечить кросс-платформенность для мобильных и десктопных версий. К счастью, WebRTC отлично поддерживается в различных браузерах.

    5. Четкое разделение игрового интерфейса и сервиса
    Я рассматриваю сервис облачных игр как платформу. У каждого должна быть возможность подключать к платформе что угодно. Сейчас я интегрировал LibRetro с сервисом облачных игр, потому что LibRetro предлагает красивый интерфейс игрового эмулятора для ретро-игр, таких как SNES, GBA, PS.

    6. Комнаты для мультиплеера, crowd play и внешнее связывание (deep-link) с игрой
    CloudRetro поддерживает множество новых геймплеев, таких как CrowdPlay и Online MultiPlayer для ретро-игр. Если несколько пользователей откроют один и тот же deep-link на разных компьютерах, они увидят одну и ту же запущенную игру и даже смогут присоединиться к ней.

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

    7. Горизонтальное масштабирование
    Как и любой SAAS в настоящее время, облачные игры должны быть спроектированы так, чтобы быть горизонтально масштабируемыми. Конструкция координатор-воркер позволяет добавлять больше воркеров, чтобы обслуживать больший трафик.

    8. Нет привязки к одному облаку
    Инфраструктура CloudRetro размещается на различных облачных провайдерах (Digital Ocean, Alibaba, пользовательский провайдер) для различных регионов. Я активирую запуск в контейнере Docker для инфраструктуры и настраиваю сетевые параметры с помощью bash-скрипта, чтобы избежать зависимости от одного облачного провайдера. Сочетая это с NAT Traversal в WebRTC, мы можем получить гибкость для развертывания CloudRetro на любой облачной платформе и даже на машинах любого пользователя.

    Архитектурный дизайн


    Воркер: (или потоковый сервер, упомянутый выше) множит игры, запускает кодирующий пайплайн и передает закодированное медиа пользователям. Инстансы воркера распространяются по всему миру, и каждый воркер может обрабатывать несколько пользовательских сессий одновременно.

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

    Хранилище игровых состояний: центральное удаленное хранилище для всех состояний игры. Это хранилище обеспечивает такие важные функции, как, например, удаленное сохранение/загрузка.


    Верхнеуровневая архитектура CloudRetro

    Пользовательский сценарий


    Когда новый пользователь открывает CloudRetro на шагах 1 и 2, показанных на рисунке ниже, координатор вместе со списком доступных воркеров запрашивается на первую страницу. После этого на шаге 3 клиент рассчитывает задержки для всех кандидатов с помощью HTTP запроса ping. Этот список задержек затем отправляется обратно координатору, чтобы он мог определить наиболее подходящего воркера для обслуживания пользователя. На шаге 4 ниже создается игра. Между пользователем и назначенным воркером устанавливается потоковое соединение WebRTC.

    Пользовательский сценарий после получения доступа

    Что внутри воркера


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


    Взаимодействие компонентов воркера

    Основные составляющие:

    • WebRTC: клиентский компонент, принимающий пользовательский ввод и выводящий закодированное медиа с сервера.
    • Игровой эмулятор: игровой компонент. Благодаря библиотеке Libretro система способна запускать игру внутри одного и того же процесса и внутренне перехватывать медиа и поток ввода.
    • Внутриигровые кадры захватываются и отправляются в кодировщик.
    • Изображение/аудио кодировщик: кодирующий пайплайн, который принимает медиакадры, кодирует их в фоновом режиме и выводит закодированные изображения/аудио.

    Реализация


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

    WebRTC


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

    NAT Traversal


    WebRTC известен своей функциональностью NAT Traversal. WebRTC предназначен для одноранговой коммуникации. Его цель найти наиболее подходящий прямой маршрут, избегая NAT-шлюзов и брандмауэров для одноранговой связи через процесс под названием ICE. В рамках этого процесса API WebRTC находят ваш публичный IP-адрес с помощью серверов STUN и переадресовывают его на сервер ретрансляции (TURN), когда прямое соединение не может быть установлено.

    Однако CloudRetro не полностью использует эту возможность. Его одноранговые соединения существуют не между пользователями, а между пользователями и облачными серверами. Серверная часть модели имеет меньше ограничений на прямую связь, чем обычное пользовательское устройство. Это позволяет делать предварительное открытие входящих портов или использование публичных IP-адресов напрямую, так как сервер не находится за NAT.

    Раньше я хотел превратить проект в платформу распространения игр для Cloud Gaming. Идея заключалась в том, чтобы позволить создателям игр предоставлять игры и потоковые ресурсы. А пользователи взаимодействовали бы с провайдерами напрямую. В такой децентрализованной манере CloudRetro является всего лишь средой для подключения сторонних потоковых ресурсов к пользователям, что делает его более масштабируемым, когда на нем больше не висит хостинг. Роль WebRTC NAT Traversal здесь очень важна для облегчения инициализации однорангового соединения на сторонних потоковых ресурсах, что упрощает подключение создателя к сети.

    Сжатие видео


    Сжатие видео это незаменимая часть пайплайна, которая в значительной степени способствует плавности потока. Несмотря на то, что не обязательно знать все детали кодирования видео в VP8/H264, понимание концепции помогает разбираться в параметрах скорости потокового видео, отлаживать неожиданное поведение и настраивать задержку.

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

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


    Сравнение видеокадров на примере Pacman

    Сжатие аудио


    Аналогичным образом, алгоритм сжатия звука опускает данные, которые не могут быть восприняты человеком. Opus на данный момент является аудиокодеком с наилучшей производительностью. Он разработан для передачи аудиоволны по протоколу упорядоченной датаграммы, такому как RTP (Real Time Transport Protocol протокол передачи трафика реального времени). Его задержка меньше, чем у mp3 и aac, а качество выше. Задержка обычно составляет около 5~66,5 мс.

    Pion, WebRTC в Golang


    Pion это проект с открытым исходным кодом, который перетаскивает WebRTC на Golang. Вместо обычного врапинга нативных C++ библиотек WebRTC, Pion является нативной Golang-реализацией WebRTC с лучшей производительностью, интеграцией с Go, а также контролем версий на протоколах WebRTC.

    Библиотека также обеспечивает потоковую передачу данных с большим количеством отличных встроенных модулей с задержкой менее секунды. Она имеет свою собственную реализацию STUN, DTLS, SCTP и т.д. и некоторые эксперименты с QUIC и WebAssembly. Сама по себе эта опенсорсная библиотека является действительно хорошим источником обучения с отличной документацией, реализацией сетевых протоколов и классными примерами.

    Комьюнити Pion, возглавляемое очень страстным создателем, довольно оживленное, там ведется много качественных дискуссий о WebRTC. Если вас интересует эта технология, присоединяйтесь к http://pion.ly/slack вы узнаете много нового.

    Написание CloudRetro на Golang



    Реализация воркера на Go

    Каналы Go в действии


    Благодаря красивому дизайну каналов Go, проблемы потоковой передачи событий и параллелизма значительно упрощаются. Как и на диаграмме, в разных GoRoutines параллельно работают несколько компонентов. Каждый компонент управляет своим состоянием и общается по каналам. Выборочное утверждение Golang заставляет обработать по одному атомарному событию каждый момент времени в игре (game tick). Это означает, что для такого дизайна блокировка не нужна. Например, когда пользователь сохраняется, требуется полный снэпшот состояния игры. Это состояние должно оставаться непрерывным, выполняя вход до тех пор, пока сохранение не будет завершено. Во время каждого game tickа бэкэнд может обрабатывать только операцию сохранения или ввода, что делает процесс потокобезопасным.

    func (e *gameEmulator) gameUpdate() {for {select {case <-e.saveOperation:e.saveGameState()case key := <-e.input:e.updateGameState(key)case <-e.done:e.close()return}    }}
    

    Fan-in / Fan-out


    Этот шаблон Golang отлично подходит для моего сценария использования CrowdPlay и Multiple Player. Следуя этому шаблону, все пользовательские входы в одной комнате встраиваются в центральный входной канал. Игровые медиа затем разворачиваются на всех пользователей в одной комнате. Таким образом, мы достигаем разделения состояния игры между несколькими игровыми сессиями разных пользователей.


    Синхронизация между различными сеансами

    Недостатки Golang


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

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

    CGO


    В проекте используется существующая VP8/H264 библиотека Golang с открытым исходным кодом для сжатия медиа и Libretro для игровых эмуляторов. Все эти библиотеки являются просто обертками библиотеки C в Go с использованием CGO. Некоторые из недостатков перечислены в этом посте Dave Cheney. Проблемы, с которыми я столкнулся:

    • невозможность поймать краш в CGO, даже с помощью Golang RecoveryCrash;
    • невозможность определить узкое место в производительности, когда мы не можем обнаружить детализированные проблемы в CGO.

    Заключение


    Я достиг своей цели разобрался в облачных игровых сервисах и создал платформу, которая помогает играть в ностальгические ретро-игры с моими друзьями онлайн. Создание этого проекта было бы невозможным без библиотеки Pion и поддержки сообщества Pion. Я чрезвычайно благодарен за его интенсивное развитие. Простые API, предоставленные WebRTC и Pion, обеспечили плавную интеграцию. Мое первое доказательство концепции было выпущено на той же неделе, несмотря на то, что я заранее не знал об одноранговой связи (P2P).

    Несмотря на простоту интеграции, P2P-потоковое вещание действительно является очень сложной областью в компьютерной науке. Ей приходится иметь дело со сложностью многолетних сетевых архитектур, таких как IP и NAT для создания одноранговой сессии. За время работы над этим проектом я накопил много ценных знаний о сети и оптимизации производительности, поэтому рекомендую всем попробовать построить P2P-продукты с помощью WebRTC.

    CloudRetro обслуживает все сценарии использования, которые я ожидал, с моей точки зрения, как ретро-геймера. Тем не менее, я думаю, что есть много областей в проекте, которые я могу улучшить, например, сделать сеть более надежной и производительной, обеспечить более высокое качество графики игр, или возможность делиться играми между пользователями. Я упорно работаю над этим. Пожалуйста, следите за проектом и поддержите его, если он вам нравится.
Подробнее..

Архитектура любительского стримингового сервиса DOS игр

29.12.2020 22:09:15 | Автор: admin
Недавно я написал небольшую статью о стриминге DOS игр в браузере. Настало время сделать небольшой технический обзор. Проект ведется исключительно мной, поэтому я его позиционирую как любительский. Среди общедоступных технологий позволяющих сделать стриминг игр можно выделить только WebRTC на нём и построен мой сервис. Как вы уже наверное догадались он состоит из браузерной и серверной части.


Браузерная часть


Основной компонент сервиса WebRTC сервер Janus. Из коробки он предоставляет простое API для подключения к серверу, и поддержки WebRTC протокола. Поэтому, браузерная часть получилось максимально простой, в виде обертки поверх Janus API.

Серверная часть


На стороне сервера используются dosbox, ffmpeg и Janus. Все они собраны вместе в docker контейнер.

Текущая версия сервиса использует:

  • Последнюю версию dosbox
  • Последнюю версию ffmpeg, скомпилированную с поддержкой кодеков vp9 и opus
  • Последнюю версию janus с небольшими дополнениями (о них ниже)


Стриминг звука и видео


Когда docker стартует, супервизор запускает все три программы. Dosbox запускает игру и начинает непрерывно генерировать кадры и звуки. Эти данные перенаправляются в ffmpeg, который создает два RTP стрима (звук, видео). Плагин для стриминга Janus (стандартный компонент), слушает эти стримы и генерирует WebRTC данные для браузера.

{dosbox} --> {ffmpeg} --> {janus streaming plugin} --> {browser}


Поддержка клавиатуры


Для поддержки игровых действий (нажатие кнопок) используются каналы данных WebRTC. Каждый раз когда игрок нажимает кнопку, срабатывает обработчик клавиатуры в браузере. Он отправляет текстовые команды через дата канал на сервер.

  • pipe kdown когда кнопка нажата
  • pipe kup когда кнопка отпущена


На сервере эти данные обрабатываются моим расширением (исходный код). Создается именованный пайп в который записываются поступающие команды. С другой стороны, dosbox постоянно читает этот пайп и добавляет события клавиатуры в очередь для обработки.

{browser} --> {janus data text channel} --> {pipe} --> {dosbox}


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

Инфраструктура


Сервис запущен на платформе Amazon. Для каждого клиента создается новая задача Fargate. После старта задача получает публичный IP, который отправляется в браузер. При получении IP браузер инициирует WebRTC соединение с Janus сервером. Когда dosbox заканчивает работу, задача Fargate автоматически останавливается. Технически нет никаких ограничений на количество одновременных игроков.

{browser} --> {+fargate} --> {ip} --> {browser}
...
{browser} --> {stop} --> {-fargate}


Вместо заключения


Получилось достаточно поверхностно,

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

Обзорная статья: DOS Cloud Gaming
Подробнее..

Стандарт WebRTC получил официальный статус рекомендованного W3C

29.01.2021 16:08:57 | Автор: admin
Источник

Технология WebRTC (Web Real-Time Communications), которая описывает передачу аудио-, видеоданных и контента между браузерами без установки дополнительных расширений, получила статус рекомендованного стандарта. Об этом объявил консорциум W3C, который разрабатывает и внедряет технологические стандарты для сети интернет.

Все стандарты W3C проходят проверку на предмет поддержки доступности веб-контента, которая осуществляется APA (рабочей группой по архитектурам доступных платформ). Они напрямую связаны с обеспечением веб-доступности.

Помимо прочего, комитет IETF (Internet Engineering Task Force), который занимается развитием протоколов и архитектуры интернета, опубликовал документы с описанием архитектуры, элементов протоколов, видов транспорта и механизмов коррекции ошибок WebRTC. Все эти данные получили статус Предложенный стандарт.

О WebRTC


Технология позволяет веб-приложениям и сайтам захватывать и выборочно передавать аудио- и видеопотоки, а также обмениваться данными между браузерами без использования посредников. Именно благодаря ей мы можем созваниваться с коллегами на удаленке без необходимости устанавливать плагины и другое ПО. Приложения, созданный на основе этого стандарта, обрабатывают голосовой и видеотрафик в реальном времени, используя только HTML и JavaScript. По нему, например, работают Google Meet и еще ряд приложений для веб-конференций.

Технология WebRTC развивается компанией Google с 2009 года. В 2011 году компания открыла свои наработки и технологии обработки звука и видео, полученные при поглощении компании GIPS, разрабатывающей системы цифровой обработки сигналов. Тогда же Google предоставил безвозмездный доступ к патентам, связанным с WebRTC.

WebRTC состоит из нескольких взаимосвязанных программных интерфейсов (API) и протоколов. Процесс стандартизации технологии Google начал совместно с такими компаниями, как Mozilla, Microsoft, Cisco и Ericsson.

К слову, WebRTC (как и HTML5) стал одной из причин смерти технологии Flash. С 2017 года ведущие браузеры официально перестали поддерживать Flash и технология исчезла с рынка. О последствиях мы уже писали в блоге.

Сейчас технология WebRTC занимает второе место в топе протоколов видеосвязи после проприетарного Zoom. Стандартным протоколам H.323, SIP, Microsoft Teams и Cisco Webex пока не удается достичь ее успеха.

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

Преимущества стандарта


  • Не требует установки программного обеспечения и плагинов.
  • Использование современных аудио- и видеокодеков; как следствие высокое качество связи.
  • Защищенные и зашифрованные соединения по протоколам DTLS и STRP.
  • Есть встроенный механизм захвата контента (демонстрация рабочего стола)
  • Гибкость реализации интерфейса управления на основе HTML5 и JavaScript.
  • Открытый исходный код.
  • Универсальность: приложение на основе стандарта хорошо работает на любой ОС, если браузер поддерживает WebRTC.

Недостатки стандарта


Для кого-то эти недостатки не будут существенными, но мы их все-таки обозначим.

  • Стандарт не поддерживает удаленное управлением рабочим столом. То есть мы можем показать презентацию или график коллегам, но поработать вместе над составлением годового отчета не получится. Все ради безопасности: код Javascript не может управлять чем-либо за пределами текущего окна браузера. Для расширенных возможностей нужно использовать специально разработанные приложения.
  • Приложения на WebRTC несовместимы между собой, именно поэтому мы не можем позвонить c Google Meets на какой-нибудь BigBlueButton. Но, может, это и не надо?
  • Еще один недостаток состоит в том, что WebRTC определяет IP-адреса пользователей. Прокси и Tor c проблемой не справятся, скрыться помогут только VPN-сервисы.

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

Из чего состоит WebRTC


На структурном уровне это:

  • системы управления пользовательскими сеансами;
  • движок для обработки звука: можно использовать разные кодеки и методы подавления шумов;
  • движок для обработки видео;
  • транспортный уровень: для передачи данных можно использовать протоколы DTLS и SRTP совместно с технологиями организации P2P-каналов связи.

Как мы уже писали ранее, работать с возможностями WebRTC можно через специально подготовленный JavaScript API. API включает в себя такие интерфейсы, как getUserMedia, RTCPeerConnection, RTCDataChannel и getStats.

Интерфейс getUserMedia отвечает за получение аудио и видео из подключенных устройств типа веб-камеры и микрофона или файла. За установку соединения между пользователями, обработку сигналов и защиту канала связи отвечает интерфейс RTCPeerConnection. Обмениваться данными во время конференции помогает RTCDataChannel (с использованием типового API WebSockets). За статистикой к интерфейсу getStats.

Что дальше


На данный момент стандартизированы только базовые части WebRTC. А что можно ожидать в следующей версии стандарта?

  • Расширение, которое позволит использовать протокол QUIC в качестве транспорта и видеокодека AV1.
  • API WebTransport, упрощающего организацию потокового вещания для нескольких получателей.
  • API Scalable Video Coding, адаптирующий видеопоток под пропускную способность клиента.
  • Сквозное шифрование видеоконференций.
  • Live-обработка аудио- и видеопотоков, в том числе с помощью систем машинного обучения.
  • Инструменты для установки постоянного канала связи с умными устройствами.

Подробнее..

DOS Cloud Gaming

17.12.2020 10:10:32 | Автор: admin
Давным давно я занялся разработка JavaScript API для запуска DOS игр в браузере. После стольких потраченных сил с удовлетворением могу сказать, что многие сайты со старыми игрушками перешили и используют именно мое API. Но, прогресс не стоит на месте и я занялся разработкой следующей версии js-dos.

Вас ждет много вкусностей:

  • Универсальные бандлы (zip архивы), которые содержать конфигурацию dosbox. Теперь для развертывании игры на сайте достаточно одной строчки кода
  • Открытый репозитории бандлов
  • Виртуальные кнопки и джойстики для игры на мобильных устройствах
  • Бэкенд эмулятора полностью перемещен в Worker
  • Независимый, заменяемый рендер. Например, можно рендерить игру через Three.js
  • Поддержка нескольких бэкендов. Сейчас это только dosbox, но в планах поддержка dosbox-x и много чего ещё
  • web-rtc бэкенд Janus для игры в облаке


Зачем вообще нужен облачный сервис для DOS игр?


Технологии шагнули далеко вперед, современных мощностей уже зачастую хватает чтобы играть практически во все DOS игры прямиком в браузере. Однако, ситуация на мобильных устройствах выглядит много хуже. Мощностей многих устройств все ещё не достаточно чтобы играть в популярные игры: DOOM, MK, Carmageddon. Некоторые игры (например: C&C, Diablo) не работают даже на PC. Это связано с тем что пока невозможно имплементировать рекомпилирующий эмулятор на WebAssembly.

Именно в таких ситуация и должен помогать облачный сервис. Например, стандартный тест DHRY2 тест запущенный на мобильном устройстве обычно дает результат около 20 очков. Запущенный в облачном сервисе 240 очков (этого достаточно чтобы играть в любую DOS игру).



Тестирование


Сейчас сервис проходит активное тестирование, его можно попробовать в действии. Мне очень нужны тестовые данные из стран СНГ.

Помочь с тестированием очень просто. Все что нужно зарегистрироваться, поиграть по ссылке в DOOM и описать свои ощущения. Для запуска игры в облачном сервисе нужно использовать кнопку >> Turbo.



Могу много интересного рассказать про разработку js-dos v7, не знаю что было бы интересно сообществу. Очень жду обратной связи в комментариях или Discord проекта.

Спасибо.
Подробнее..
Категории: Webrtc , Я пиарюсь , Cloud , Dos , Gaming

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru