Передача відео аудіо потоку з Openvidu.

Фронтенд розробка JavaScript. -> Передача відео аудіо потоку з Openvidu.

Передача відео аудіо потоку з Openvidu.

Бібліотека Openvidu надає набір інструментів для забезпечення передачі відео та аудіопотоку між браузерами, шарингу робочого столу, їх запису на стороні сервера, а також обслуговування IP камер.

Процес побудований на технології WebRTC.

Ця технологія досить складна і включає передачу інформації про мережеві та відео налаштування між двома точками по веб-сокетах.

Ця інформація передається у вигляді SDP (опис кодеків, частоти кадрів та ін.) та ICE кандидатів (мережева конфігурація).

Openvidu нам дозволяє не вникати в тонкощі цієї взаємодії яких досить багато, т.к. у різних мережах з’єднати два браузери буває ой як не просто.

Сама бібліотека включає кілька процесів-серверів.

KMS - безкоштовний Karentoo медіа сервер, який обробляє відео та пропускає його через себе кодуючи та записуючи (опціонально), потім він може його передати та браузер буде думати що він працює з іншим браузером але насправді буде віддавати та приймати потік з медіа-сервера. Портти 40000 - 57000 TCP+UDP

Coturn - TURN сервер для визначення IP адрес клієнтів та знаходження дірок мережі через які проводити відео. 3478 порт для визначення IP та 57001 - 65535 TCP+UDP для побудови моста з KMS.

Openvidu - сокет сервер c REST API для обміну повідомленнями між браузером та KMS. Порт 4443.

Redis - база даних

Схема взаємодії.

start page

Маємо 3-х гравців.

openvidu-browser JavaScript бібліотека для встановлення з’єднання.

openvidu-server - Java програма, яка керує KMS медіа сервером.

Kurento Media Server - низькорівневий сервер для операцій з медіа-потоком та його передачею.

Запуск та встановлення включає наступні кроки.

Установка docker та docker-compose.

apt install docker-ce docker-compose

Установка openvidu

cd /opt
curl https://s3-eu-west-1.amazonaws.com/aws.openvidu.io/install_openvidu_latest.sh | bash

Прописуємо налаштування DOMAIN_OR_PUBLIC_IP та OPENVIDU_SECRET в .env.

DOMAIN_OR_PUBLIC_IP=localhost

Стартуємо сервер.

./openvidu start

Старт сервера вручну

docker run -p 4443:4443 --rm -e OPENVIDU_SECRET=MY_SECRET openvidu/openvidu-server-kms:2.18.0

При цьому ми відкриваємо 4443 порт прокидаючи його з контейнера назовні.

Встановлюємо приклад програми

git clone https://github.com/OpenVidu/openvidu-tutorials.git -b v2.18.0

Заходимо у будь-який приклад та запускаємо там статичний вебсервер. Наприклад.

python3 -m http.server 8080

Розробка фронтенд програми.

Приєднуємо клієнта.

<script src="openvidu-browser-2.18.0.js"></script>

Стартуємо сесію.

OV = New OpenVidu();

OV – об’єкт-точка входу до бібліотеки.

Через неї створюємо решту об’єктів.

Сесія

session = OV.initSession();

Є кімнатою користувача, до якої можна додавати з’єднання.

Сесія має низку подій.

streamCreated - спрацьовує коли до сесії додається новий потік. У цій події ми підписуємося на потік і потім ми можемо додавати відеотег довільний HTML елемент за ID.

session.on('streamCreated', event => {
    var subscriber = session.subscribe(event.stream, 'video-container');
})

videoElementCreated - це подія передплатника в якому ми можемо щось зробити після того, як додано відео тег і пішло відео.

streamDestroyed - подія сесії, що спрацьовує при залишанні кімнати та видаленні кожного відеопотоку.

exception - подія сесії при помилках виконання асинхронних операцій.

З’єднання

Представляє конкретний відеопотік користувача, який може бути доданий у сесію.

Сесія має унікальний ідентифікатор, як і з’єднання має унікальний токен.

Створення сесії на сервері.

Для того, щоб на сервері створити сесію, необхідно надіслати POST запит на адресу /openvidu/api/sessions та передати вигадане ім’я сесії.

function createSession(sessionId) { 
// See https://docs.openvidu.io/en/stable/reference-docs/REST-API/#post-openviduapisessions
        return new Promise((resolve, reject) => {
            $.ajax({
                type: "POST",
                url: this.OPENVIDU_SERVER_URL + "/openvidu/api/sessions",
                data: JSON.stringify({ customSessionId: sessionId }),
                headers: {
                    "Authorization": "Basic " + btoa("OPENVIDUAPP:" + this.OPENVIDU_SERVER_SECRET),
                    "Content-Type": "application/json"
                },
                success: (response) => { 
                    return resolve(response.id)
                },
                error: (error) => {

                    if (error.status === 409) {
                        resolve(sessionId);
                    } else {
                        console.warn('Error');
                        }
                    }
                }
            });
        });
    },

При цьому запит може повернути статус 409 у разі сесії з таким ім’ям. При цьому нічого страшного не відбувається, а результат ігнорується і передається далі ланцюжком асинхронних промісів.

Створення з’єднання.

POST запит на /openvidu/api/sessions/’ + sessionId + ‘/connection’

Поверне токен з’єднання.

function createToken(sessionId) { 
// See https://docs.openvidu.io/en/stable/reference-docs/REST-API/#post-openviduapisessionsltsession_idgtconnection
        return new Promise((resolve, reject) => {
            $.ajax({
                type: 'POST',
                url: this.OPENVIDU_SERVER_URL + '/openvidu/api/sessions/' + sessionId + '/connection',
                data: JSON.stringify({}),
                headers: {
                    'Authorization': 'Basic ' + btoa('OPENVIDUAPP:' + this.OPENVIDU_SERVER_SECRET),
                    'Content-Type': 'application/json',
                },
                success: (response) => {
                    return resolve(response.token)
                },
                error: (error) => reject(error)
            });
        });
    },

Функція, що поєднує 2 запити в ланцюжок.

function getToken(mySessionId) {
    return createSession(mySessionId).then(sessionId =>     createToken(sessionId));
}

Т.к. ці запити мають секрети, їх слід виконувати на стороні сервера і не світити в js.

Розмітка сторінки.

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Openvidu sender</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
        <script src="/static/openvidu.js"></script>

    </head>

    <body>
        <h1>Openvidu Sender page</h1>

        <div id="senderCam"></div>

        <a id="start">Start video translation</a>


        <script src="/static/ov/index.js"></script>
        <script>
        $('#start').on('click', () => {
            window.myapp.initappSender('woman');
        })


        </script>

    </body>
</html>

openvidu.js - браузерна бібліотека, можна скопіювати з будь-якого прикладу.

Створюємо об’єкт програми та функцію ініціалізації для транслюючого відео.

var ovapp = {
    OPENVIDU_SERVER_URL: 'https//localhost:4443',
    OPENVIDU_SERVER_SECRET: 'MY_SECRET',


    initappSender: function(uname) {
        OV = new OpenVidu();
        session = OV.initSession();
    }

}

Додамо функції створення сесії та з’єднання.

var ovapp = {
    OPENVIDU_SERVER_URL: 'https://localhost:4443',
    OPENVIDU_SERVER_SECRET: 'MY_SECRET',


    initappSender: function(uname) {
        OV = new OpenVidu();
        session = OV.initSession();
        this.getToken('woman').then(
            (token) => {
                console.log(`we got token ${token}!!!`)
            }
        );
    },


    createSession: function (sessionId) { 
        return new Promise((resolve, reject) => {
            $.ajax({
                type: "POST",
                url: this.OPENVIDU_SERVER_URL + "/openvidu/api/sessions",
                data: JSON.stringify({ customSessionId: sessionId }),
                headers: {
                    "Authorization": "Basic " + btoa("OPENVIDUAPP:" + this.OPENVIDU_SERVER_SECRET),
                    "Content-Type": "application/json"
                },
                success: response => resolve(response.id),
                error: (error) => {
                    if (error.status === 409) {
                        resolve(sessionId);
                    } else {
                        console.warn('No connection to OpenVidu Server. This may be a certificate error at ' + this.OPENVIDU_SERVER_URL);
                        if (window.confirm('No connection to OpenVidu Server. This may be a certificate error at \"' + this.OPENVIDU_SERVER_URL + '\"\n\nClick OK to navigate and accept it. ' +
                            'If no certificate warning is shown, then check that your OpenVidu Server is up and running at "' + this.OPENVIDU_SERVER_URL + '"')) {
                            location.assign(this.OPENVIDU_SERVER_URL + '/accept-certificate');
                        }
                    }
                }
            });
        });
    },



    createToken: function (sessionId) { 
        return new Promise((resolve, reject) => {
            $.ajax({
                type: 'POST',
                url: this.OPENVIDU_SERVER_URL + '/openvidu/api/sessions/' + sessionId + '/connection',
                data: JSON.stringify({}),
                headers: {
                    'Authorization': 'Basic ' + btoa('OPENVIDUAPP:' + this.OPENVIDU_SERVER_SECRET),
                    'Content-Type': 'application/json',
                },
                success: (response) => resolve(response.token),
                error: (error) => reject(error)
            });
        });
    },

    getToken: function(mySessionId) {
        return this.createSession(mySessionId).then(sessionId =>     this.createToken(sessionId));
    }

}

Результат.

start page

Створимо сокет-з’єднання для отримання повідомлень про запит на передачу відео.

Встановимо клієнта.

npm install socket.io-client --save

Скопіюємо з папки dist та підключимо бібліотеку на сторінці.

<script src="/static/socket.io.min.js"></script>

Створимо з’єднання із сервером по сокетах.

var ovapp = {
    ...
    SOCKET_URL: 'http://localhost:5001',

    socketConnect: function(login) {
        socket = io(`${this.SOCKET_URL}`, {transports:['websocket']});
        socket.on('connect', () => {
            console.log('Connection was established');
            window.sessionStorage.setItem('sid',socket.id);
            socket.emit('login',{login});
        })            
    },

При цьому ми за подією з’єднання додатково повідомляємо сервер про логіну людини, яка приєдналася, щоб сервер це зафіксував у БД і знав хто в онлайні

    initappSender: function(username) {
        ...
        this.socketConnect(username);
    ...

Відреагуємо на повідомлення на прийом відео від сторони, що приймає.

Відобразимо діалогове вікно з кнопками згоди або відмови, а також з відео тегом.

    socket.on('calling', (msg) => {

        const tpl = `<div id="responseBox">
            <h1 id="callerName">${msg.login} is calling you!<h1>
            <input type="text" id="recieverLogin" value="${msg.login}">
            <video autoplay="true" width="200" id="myVideo"></video>  
            <div style="text-align: center">              
            <a class="btn" id="acceptOffer">Accept</a>
            <a class="btn" id="declineOffer">Decline</a>
            <a class="btn" style="display:none" id="stopVideo">Stop video</a>
            </div>
        </div>`;
        $('#senderCam').html(tpl);
        $('#senderCam').show();
    });

Оформимо сторінку на стороні, що приймає.

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Openvidu reciever</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
        <script src="/static/openvidu.js"></script>
        <script src="/static/socket.io.min.js"></script>
        <link rel="stylesheet" href="/static/video.css">
    </head>
    <body>
        <h1>Openvidu Reciever page</h1>
        <div id="recieverCam"></div>
        <button id="VideoCall" data-username="woman">Video call</button>
        <script src="/static/ov/index.js"></script>
        <script>
            window.ovapp.initappReciever('man');
        </script>
    </body>
</html>

window.myapp.initappReciever(‘man’) - тут ми ініціалізуємо додаток для приймаючого та передаємо його логін, для подальшої передачі його на бік передавального, щоб вивести його на діалоговому вікні виклику.

У кнопку виклику закладаємо інформацію про логіну викликаного.

Оформимо функцію initappReciever.

var ovapp = {
   ...
    initappReciever: function(username){
        console.log('Init sender app');
        socket = io(`${this.SOCKET_URL}`, {transports:['websocket']});

        $('#VideoCall').on('click', (e) => {
            this.callUser();
        })
    },

...

Тепер у функції callUser ми повинні надіслати повідомлення до браузера передавального.

callUser: function() {
    const url = `${this.SERVER_URL}/call`;
    const username =  $('#VideoCall').attr( "data-username" );
    $.ajax({
        url: url,
        type: "POST",
        data: JSON.stringify({
            login: username,
            sid: window.sessionStorage.getItem('sid')
        }),
        contentType: "application/json",
        success: (response) => {
            if(response.status === 0) {
                $('#VideoCall').html(response.message);
            } else {
                alert(response.message);
            }

        }
    }); 
},

Для цього пошлемо HTTP запит на сервер і він передасть його через сокет на бік передавального.

Результат.

Start page

Відпрацюємо відмову, надіславши на сервер повідомлення по ресту.

   socket.on('calling', (msg) => {

       ...
        $('#senderCam').show();
        $('#declineOffer').on('click', async (e) => {
            const url = `${this.SERVER_URL}/decline`;
            $.ajax({
                type: "POST",
                url: url,
                contentType: "application/json",
                data: JSON.stringify({
                    'sid': window.sessionStorage.getItem('sid'),
                    'reciever_login': $('#recieverLogin').val()
                }),
                    success: (data) => {
                        console.log(data);
                        $('#senderCam').html('');
                    },
            });
        }) 
    });

Тепер ухвалення запрошення.

        $('#acceptOffer').on('click', async (e) => {
            $('#acceptOffer').hide();
            $('#declineOffer').hide();
            $('#stopVideo').show();
            $('#callerName').html('Click here to move');

            const url = `${this.SERVER_URL}/status`;
            $.ajax({
                type: "POST",
                url: url,
                contentType: "application/json",
                data: JSON.stringify({
                    'sid': window.sessionStorage.getItem('sid'),
                    'status':'beasy'
                }),
                    success: (data) => {

                    },
            });
        });

Де ми приховуємо та відображаємо відповідні кнопки та надсилаємо повідомлення на сервер про те, що користувач зайнятий.

Далі нам необхідно відобразити камеру на передавальній строні.

$('#acceptOffer').on('click', async (e) => {
    $('#acceptOffer').hide();
    $('#declineOffer').hide();
    $('#stopVideo').show();
    $('#callerName').html('Click here to move');

    OV = new OpenVidu();
    session = OV.initSession();
    this.getToken(this.USERNAME).then(function(token) {
        session.connect(token, { })
        .then(() => {
            var publisher = OV.initPublisher('video-container', {
                audioSource: undefined, 
                videoSource: undefined, 
                publishAudio: true, 
                publishVideo: true, 
                resolution: '640x480', 
                frameRate: 30,
                insertMode: 'APPEND',
                mirror: false
            });
            session.publish(publisher);

        })
    });
    ...

Відобразимо її і на приймаючій.