Видео-чат через браузер. WebRTC — это просто, если есть библиотека

Я работаю с MFF и GH. Дружат ли другие браузеры с WebRTC, можно узнать, зайдя на sipjs.com — там без регистрации можно полюбоваться на себя в двух экземплярах (если есть веб-камера), послать себе сообщение или файл. И все это на одной странице. Неинтересно. Интересно, когда я на одной странице, а мой визави на другой. Демо-пример нужно чуть-чуть подправить...



Как это все работает (в моем понимании):


WebRTC — это соединение двух UserAgent (т.е. двух браузеров, далее UA) точка-точка, при котором сигнал с веб-камеры, захваченной одним браузером, стремительным потоком передается другому браузеру.


Чтобы установить соединение и прорваться через частокол NAT'ов нужен SIP и STUN сервер. Оба UA должны быть зарегистрированы на SIP сервере. Что-то типа "1234@myfreefreefreeswitch.ru".


UA с именем "1234@..." и паролем "111" авторизуется на SIP-сервере "myfreefreefreeswitch.ru" и говорит: хочу связаться с UA "5678@..." для передачи голоса. Если оба в сети, сервер их соединяет.


Браузер спрашивает пользователя: "Микрофон просят. Дадим?". Дадим, и получим voice-ip.


Проверить видео-чат с двух разных компьютеров можно на основе демо-примера с sipjs.com, используя в качестве SIP/STUN-сервера sipjs.onsip.com.


Sipjs.onsip.com не требует предварительной регистрации. Он обслуживает пары UA с именами
"alice.случайная строка@sipjs.onsip.com" и
"bob.та же самая случайная строка@sipjs.onsip.com".


На самом деле "bob." или "alice." не обязательны. Можно подключиться с любым уникальным именем. Но это неприлично.


Случайные строки в их демо-примере генеряться JS при открытии страницы и зачем-то передаются на сервер в document.cookie. К SIP-протоколу куки отношения не имеют, видимо, нужны для чего-то другого (например, чтобы зарегистрировать нового любопытного пользователя).


Для проверки видео-чата с 2-х компьютеров http-сервер не обязателен, достаточно 2-х html страниц (на одной: "я Боб, хочу связаться с Алисой", на другой: "я Алиса, хочу связаться с Бобом") и одного скрипта.


Страницы почти одинаковые (сделаны по одному шаблону).!!! Прежде, чем их открыть надо в обеих страницах одинаково исправить 1 строку !!!


ЭТО ВАЖНО!!! var token = '42c3';
42c3 надо исправить на любую длинную строку (англ.буквы и цифры), иначе ваши Боб и Алиса войдут в конфликт с теми, кто не исправил.


Кроме того, возможно, что ваша строка в какой-то момент устареет и все перестанет работать. Замените ее в обеих страницах на новую одинаковую строку.


Помните: это демо-пример от sipjs.com и onsip.com используется не совсем так, как они планировали. Надеюсь, они не обидятся — мы же их популяризируем.


Alice-tv.html
<!DOCTYPE html>    
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <title>Alice-tv</title>
    <script>
        var domain = 'sipjs.onsip.com';

        var token = '42c3';
        var d123 = new Date();
        d123.setTime(d123.getTime() + 1000*60*60); // expires in 1 hour
        document.cookie = ('onsipToken=' + token + ';' + 'expires=' + d123.toUTCString() + ';');

        var fromName = 'Alice';
        var toName   = 'Bob';

        var fromURI  = fromName.toLowerCase() + '.' + token + '@' + domain;
        var toURI    = toName.toLowerCase()   + '.' + token + '@' + domain;
    </script>
    <script src="https://rawgit.com/onsip/SIP.js/0.7.5/dist/sip-0.7.5.js"></script>
    <script src="demo.js"></script>
  </head>

  <body>    
    <div class="content">
        <div class="demo-window">
            <div class="left">
              <h4>Я Алиса</h4>
              <h5>В окне доктор Боб</h5>
            </div>
            <div class="demo-view">
              <video id="video" muted="muted"></video>
            </div>

            <button id="video-button" class="right" type="button">video</button>
            <div class="clearfix"></div>

            <div id="content-message">

                <div id="message-display">
                    <p class="message"><span class="message-from"></span> <span class="message-body placeholder">No messages yet</span></p>
                </div>
                <textarea id="message-input" class="message-input" placeholder="Enter your message here!"></textarea>
                <br>
                <button id="message-button" class="right" type="button">send message</button>
            </div>
        </div>
    </div>
</body>
</html>

Bob-tv.html
<!DOCTYPE html>    
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <title>Bob-tv</title>
    <script>
        var domain = 'sipjs.onsip.com';

        var token = '42c3';
        var d123 = new Date();
        d123.setTime(d123.getTime() + 1000*60*60); // expires in 1 hour
        document.cookie = ('onsipToken=' + token + ';' + 'expires=' + d123.toUTCString() + ';');

        var fromName = 'Bob';
        var toName   = 'Alice';

        var fromURI  = fromName.toLowerCase() + '.' + token + '@' + domain;
        var toURI    = toName.toLowerCase()   + '.' + token + '@' + domain;
    </script>
    <script src="https://rawgit.com/onsip/SIP.js/0.7.5/dist/sip-0.7.5.js"></script>
    <script src="demo.js"></script>
  </head>

  <body>    
    <div class="content">
        <div class="demo-window">
            <div class="left">
              <h4>Я доктор Боб</h4>
              <h5>В окне вижу Алису</h5>
            </div>
            <div class="demo-view">
              <video id="video" muted="muted"></video>
            </div>

            <button id="video-button" class="right" type="button">video</button>
            <div class="clearfix"></div>

            <div id="content-message">

                <div id="message-display">
                    <p class="message"><span class="message-from"></span> <span class="message-body placeholder">No messages yet</span></p>
                </div>
                <textarea id="message-input" class="message-input" placeholder="Enter your message here!"></textarea>
                <br>
                <button id="message-button" class="right" type="button">send message</button>
            </div>
        </div>
    </div>
</body>
</html>

Скрипт не передает звук (компьютер не начнет орать, как телевизор). Чтобы разрешить audio, надо исправить false на true в 2-х местах:


строка 96: var options = mediaOptions(false, true, remoteRender, null);
строка 116-117: session = makeCall(userAgent, target,
false, true,


demo.js
function createUA(callerURI, displayName) {

    var configuration = {
        traceSip: true,
        uri: callerURI,
        displayName: displayName
    };
    var userAgent = new SIP.UA(configuration);
    return userAgent;
}    

function setUpMessageInterface(userAgent, target, messageRenderId, messageInputId, buttonId) {
    var messageRender = document.getElementById(messageRenderId);
    var messageInput = document.getElementById(messageInputId);
    var button = document.getElementById(buttonId);

    function sendMessage() {
        var msg = messageInput.value;
        if (msg !== '') {
            messageInput.value = '';
            userAgent.message(target, msg);
        }
    }

    var noMessages = true;

    userAgent.on('message', function (msg) {
        if (noMessages) {
            noMessages = false;
            if (messageRender.childElementCount > 0)
                messageRender.removeChild(messageRender.children[0]);
        }
        var msgTag = createMsgTag(msg.remoteIdentity.displayName, msg.body);
        messageRender.appendChild(msgTag);
    });

    button.addEventListener('click', function () {
        sendMessage();
    });
    messageInput.onkeydown = (function(e) {
        if(e.keyCode == 13 && !e.shiftKey) {
            e.preventDefault();
            sendMessage();
        }
    });
}

function createMsgTag(from, msgBody) {
    var msgTag = document.createElement('p');
    msgTag.className = 'message';

    var fromTag = document.createElement('span');
    fromTag.appendChild(document.createTextNode(from + ':'));

    var msgBodyTag = document.createElement('span');
    msgBodyTag.appendChild(document.createTextNode(' ' + msgBody));
    msgTag.appendChild(fromTag);
    msgTag.appendChild(msgBodyTag);
    return msgTag;
}

function mediaOptions(audio, video, remoteRender, localRender) {
    return {
        media: {
            constraints: {
                audio: audio,
                video: video
            },
            render: {
                remote: remoteRender,
                local: localRender
            }
        }
    };
}

function makeCall(userAgent, target, audio, video, remoteRender, localRender) {
    var options = mediaOptions(audio, video, remoteRender, localRender);
    var session = userAgent.invite('sip:' + target, options);
    return session;
}

function setUpVideoInterface(userAgent, target, remoteRenderId, buttonId) {
    var onCall = false;
    var session;
    var remoteRender = document.getElementById(remoteRenderId);
    var button = document.getElementById(buttonId);

    userAgent.on('invite', function (incomingSession) {
        onCall = true;
        session = incomingSession;
        var options = mediaOptions(false, true, remoteRender, null);
        button.firstChild.nodeValue = 'hang up';
        session.accept(options);
        session.on('bye', function () {
            onCall = false;
            button.firstChild.nodeValue = 'video';
            session = null;
        });
    });

    button.addEventListener('click', function () {
        if (onCall) {
            onCall = false;
            button.firstChild.nodeValue = 'video';
            session.bye();
            session = null;
        }
        else {
            onCall = true;
            button.firstChild.nodeValue = 'hang up';
            session = makeCall(userAgent, target,
                               false, true,
                               remoteRender, null);
            session.on('bye', function () {
                onCall = false;
                button.firstChild.nodeValue = 'video';
                session = null;
            });
        }
    });
}

//****************************

(function () {
if (SIP.WebRTC.isSupported()) {
    window.fromUA = createUA(fromURI, fromName);

    var registrationFailed = false;
    var failRegistration = function () {
        registrationFailed = true;
        failInterfaceSetup();
    };

    fromUA.on('registered', setupInterfaces);
    fromUA.on('registrationFailed', failRegistration);
    window.onunload = function () {
        fromUA.stop();
    };

    function setupInterfaces() {
        setUpVideoInterface(fromUA, toURI, 'video', 'video-button');
        setUpMessageInterface(fromUA, toURI, 'message-display', 'message-input', 'message-button');
    }
    function failInterfaceSetup() {
        alert('Max registration limit hit. The app is disabled.');
    }
}
})();

Для тех, кто будет устанавливать это на сервер и знаком с jinja2


Шаблон
<!DOCTYPE html>    
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <title>{{fromName}}-tv</title>
    <script>
        var domain = 'sipjs.onsip.com';

        var token = '{{token}}';
        var d123 = new Date();
        d123.setTime(d123.getTime() + 1000*60*60); // expires in 1 hour
        document.cookie = ('onsipToken=' + token + ';' + 'expires=' + d123.toUTCString() + ';');

        var fromName = '{{fromName}}';
        var toName   = '{{toName}}';

        var fromURI  = fromName.toLowerCase() +  '.' + token + '@' + domain;
        var toURI    = toName.toLowerCase()   +  '.' + token + '@' + domain;
    </script>
    <script src="https://rawgit.com/onsip/SIP.js/0.7.5/dist/sip-0.7.5.js"></script>
    <script src="{{request.static_url('mydoctor:static/demo.js')}}"></script>
  </head>

  <body>    
    <div class="content">
        <div class="demo-window">
            <div class="left">
              <h4>{{title}}</h4>
              <h5>{{comment}}</h5>
            </div>
            <div class="demo-view">
              <video id="video" muted="muted"></video>
            </div>

            <button id="video-button" class="right" type="button">video</button>
            <div class="clearfix"></div>

            <div id="content-message">

                <div id="message-display">
                    <p class="message"><span class="message-from"></span> <span class="message-body placeholder">No messages yet</span></p>
                </div>
                <textarea id="message-input" class="message-input" placeholder="Enter your message here!"></textarea>
                <br>
                <button id="message-button" class="right" type="button">send message</button>
            </div>
        </div>
    </div>
</body>
</html>

и 2 питоновских словаря
_token = uuid.uuid4().hex
...
    {
        'title': 'Я Алиса',
        'comment': 'В окне доктор Боб',
        'fromName':'Alice',
        'toName':'Bob',
        'token': _token
    }
...
    {
        'title': 'Я доктор Боб',
        'comment': 'В окне вижу Алису',
        'fromName':'Bob',
        'toName':'Alice',
        'token': _token
    }

Уникальные расширения имен для Алисы и Боба сделает сервер при загрузке.


Я попытался установить свой SIP-сервер. Выбрал FreeSWITCH, день разбирался с настройками, но так и не смог настроить: голос поступал с задержкой в 5 (пять) секунд, видео зависало со странной записью в логе FS о том, что видео-формат не поддерживается.


Текст и файлы пересылались нормально. Похоже FS все пытался пропустить через себя (видео-конференцию учинить или для других целей).


Надеюсь, что какой-нибудь спец по SIP/STUN улыбнется, вколотит мне минус за неумейство и объяснит, какой SIP/STUN выбрать и как его настроить.

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 10

    +2
    Или вы не владеете пониманием самых основ WebRTC уровня википедии, либо написали самую непонятную статью по WebRTC в мире — о чём это, какую задачу вы решаете? Никакой SIP не нужен для WebRTC клиент-клиент (stun/turn — нужны). Или ваша цель именно в SIP из браузера?
      0
      По идее, «Chrome», а не «Hrome», то есть, правильно будет «GC» (ну или же «ГХ» =))
        0
        Тоже не смог завести эти библиотеку. Использовали Астериск. Всё делали по мануалам и стаковерфлоувам, но стабильных звонков так и не добились. Много ошибок решилось, но звонок рвется сразу при соединении, как будто «кладется трубка». А еще ошибка ice servers… Понятия не имею что это такое. Не видать нам звонков из браузера в 2016 году
          0
          Как-то пришлось мне разрабатывать проект на angularjs в котором был чат через jabber и видео, аудио звонки. В целом вышло довольно стабильно. Я использовал StropheJS + Jingle для этого. Качество нормальное, были только проблемы между звонками Chrome => FF у них там используются разные видео кодеки, в целом работало но иногда были баги, что звонок рвется при соединении. В целом возможно сделать достаточно стабильное приложение, но для этого необходимо очень хорошо изучить принципы работы webRTC, а STUN сервер это своего рода костыль, для того чтоб пробиться за NAT. Jabber мы использовали исключительно для обмена сообщениями между клиентами, что человеку звонят, отправить ответ, так же через него передавали ссылку на видео-стрим.
            +1
            Самая непонятная и запутывающая статья для тех кто хочет познакомится с WebRTC. Как выше написали, для того чтобы пробиться через NAT необходимы STUN/TURN сервера. Для peer-to-peer можно обойтись только ими. Но если вы захотите сделать live трансляцию или конференцию то понадобится медиа сервер. Сейчас есть хороший открытый проект Kurento. Уже больше пол года занимаемся переездом от Flash-а в видеочате, скажу там больше подводных камней чем кажется. Браузеры до сих пор не договорились нормально о видео/аудио кодеках и самой реализацией (приемлемо FF и Chrome только работает). Стабильно работает только сочетания VP8/Opus кодеков в FF и Chrome.

            P.S: как закончим проект, хочу написать статью со всеми проблемами которые столкнулись и как происходил переезд. Автору советую почитать что такое WebRTC, SDP, IceCandidate, STUN/TURN.
              0
              Всё, я уже жду!
              0
              За совет спасибо, статью почитаю.
              Проясните до выхода статьи пару вопросов: у меня задача сделать видео чат на сайте для зарегистрированных пользователей. Я беру библиотеку SipJS, в которой реализован WebRTC на основе SIP (...full SIP signaling stack to their WebRTC applications).
              Мой сайт передает браузеру страницу с сип-логинами тех, кто в онлайне.
              Я с любым могу войти в видео чат. Все просто и понятно (точнее, до конца непонятно, потому что все внутри библиотеки, но работает).
              А каков алгоритм без СИП? Что мне должен прислать сайт, чтобы браузер снюхался с нужным абонентом через STUN?
              Какие библиотеки Вы используете?
              Какие STUN?
                +1
                Для начала поднимите TURN сервер.
                STUN серверов полно открытых, я использую:
                • stun:stun.l.google.com:19302
                • stun:stun1.l.google.com:19302
                • stun:stun2.l.google.com:19302
                • stun:stun3.l.google.com:19302
                • stun:stun4.l.google.com:19302
                • stun:stun.services.mozilla.com

                Далее необходим механизм обмена IceCandidate и SDP. Можете поднять на nodeJS быстро вебсокет сервер, который будет соединять и обменивать данными между общающимися.

                Процесс создание связи:
                (1) Звонящий
                (2) Принимающий
                1 — добавляет стрим с камеры.
                1 — создает SDP (offer)
                1 — устанавливает локальный SDP
                1 — шлёт (2) принимающему свой локальный SDP (по вебсокетам?).
                2 — устанавливает SDP звонящего
                2 — создает свой SDP(answer) и устанавливает его как локальный
                2 — слушает событие, при успешном соединение в нём придёт сам медиастрим.

                А так, документации полно по этому поводу.
                Посмотрите прекрасный пример от Googl-a
                https://apprtc.appspot.com/
                  0
                  Спасибо за информацию.
                  А зачем столько STUN серверов?

                  Вы используете MDN. На G.Chrome Ваш софт работает?

                    0
                    Можете использовать один, это ради примера написал список.
                    С MDN есть проблемы с тем, что она не умеет IceCandidate по протоколу TCP. Решилось с помощью TURN сервера.

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

              Самое читаемое