Афоризм
Я выгляжу неполохо. Но не часто.
Наталья Резник
Последние статьи

 • Активности Android
Многоэкранные Android приложения
 • Fragment dynamic
Динамическая загрузка фрагментов в Android
 • Fragment lifecycle
Жизненный цикл Fragment'ов в Android
 • Fragment example
Пример Fragment'ов в Android
 • Data Binding
Описание и пример Data Binding
 • Пример MVVM
Пример использования MVVM в Android
 • Компонент TreeTable
Описание компонента TreeTable для Swing
 • Пример TreeTable
Пример использования TreeTable
 • Хранилища Android
Внутренние и внешние хранилища данных
 • Пример SQLite
Пример использования SQLite в Android
 • WebSocket
Описание и пример реализации WebSocket
 • Визуальные компоненты
Улучшен компонент выбора даты из календаря
 • Анимация jQuery
Описание и примеры анимации элементов DOM
 • APK-файл Android
Создание apk-файла для android устройств, .dex файлы
 • платформа JaBricks
Платформа OSGi-приложения JaBricks
Поддержка проекта

Если Вам сайт понравился и помог, то будем признательны за Ваш «посильный» вклад в его поддержку и развитие
 • Yandex.Деньги
  410013796724260

 • Webmoney
  R335386147728
  Z369087728698

Пример простого RTCDataChannel

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

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

Видео-блок управления камерой

Исходный код примера включения камеры и захвата изображения здесь немного изменим, чтобы при отключении камеры захваченый видео-кадр стирался и «вывешивался» флаг (в скриптах, не в интерфейсе). Это позволит отправить отдельно только текстовое сообщение. Для этого в методы takepicture и shutdown скриптового файла webcam.js (см. описание примера) внесем небольшие изменения. Так в метод takepicture добавим строку $('#photo').addClass('picture'), которая будет сигнализировать о захваченном кадре. В метод shutdown добавим строку $('#photo').removeClass('picture'), которая будет сбрасывать флаг захваченного кадра, после чего вызовем метод очистки изображения. Вы можете раскрыть листинг и посмотреть на изменения методов.

Раскрыть листинг изменений webcam.js

function takepicture() {
    var context = canvas.getContext('2d');
    if (width && height) {
        canvas.width = width;
        canvas.height = height;
        context.drawImage(video, 0, 0, width, height);

        var data = canvas.toDataURL('image/png');
        photo.setAttribute('src', data);
        // установка флага добавлением класса
        $('#photo').addClass('picture');
    } else {
        clearphoto();
    }
}
function shutdown() {
    if (streaming) {
        ...
        // сброс флага удалением класса
        $('#photo').removeClass('picture');
        // очистка захваченного видео-кадра
        clearphoto();
    }
}

Камера
Фото с камеры

Блок передачи данных

Узел отправки сообщения
В сообщение можно включить фото и текст (вместе или по отдельности)
Узел приема сообщения

Messages received :

Примечание :
1. Чтобы пример функционировал под управлением Apache(2.4) и Tomcat необходимо использовать SSL-протокол, т.е. пример можно демонстрировать только при безопасном подключении.
2. При разработке примера под управлением IDE (Eclipse) SSL-протокол (SSL-сертификат) не требуется.

Описание примера

Для установения канала связи необходимо нажать кнопку «Подключение». При удачном соединении доступ к кнопке будет заблокирован, но откроется доступ к кнопке «Отключение» и будет разблокировано поле ввода текста «Введите сообщение» с кнопкой «Отправить сообщение». После этого можно ввести текст и нажать кнопку «Отправить сообщение». В результате текст сообщения появится в узле приема сообщений. Чтобы отправить изображение (фото) необходимо включить камеру и захватить изображение. После этого кнопкой «Отправить сообщение» фотография будет доставлена в блок приема. Для совместной передачи изображения и текстового сообщения необходимо захватить видео-кадр и написать текстовое сообщение; при отправке сообщения выполняется контроль захваченного кадра и текстовой строки. Изображение и текст отправляются разными сообщениями, т.е. двумя передачами.

Листинг страницы html

Описание html-кода камеры рассмотрено в примере захвата видеокадра с WebRTC. Поэтому здесь ограничимся рассмотрением только интерфейса второй части примера : установления соединения и отправки сообщения.

В отличие от прототипа примера использования RTCDataChannel интерфейсные изменения коснулись только внесения дополнительных (уточняющих) надписей и комментария. В секции заголовка <head> определены css-файлы со стилями и js-файлы со скриптами. Кроме этого, дополнительно загружается файл адаптера adapter.js для обеспечения совместимости при использовании методов технологии WebRTC.

Скрыть листинг html
<head>
   <link href="webrtc-camera.css" type="text/css" 
                                  rel="stylesheet"
                                  media="all">
   <link href="webrtc-datachannel.css" type="text/css" 
                                  rel="stylesheet"
                                  media="all">
   <script src="webrtc-camera.js"></script>
   <script src="webrtc-datachannel.js"></script>

   <script src="adapter.js"></script>
</head>
<body>
  <div class="controlbox">
    <button id="connectButton"
            name="connectButton" 
            class="buttonleft">Подключение</button>
    <button id="disconnectButton"
            name="disconnectButton"
            class="buttonright" 
            disabled>Отключение</button>
  </div>
  <b>Узел отправки сообщения</b>
  <div class="messagebox">
    <label for="messageInput">Введите сообщение :
      <input type="text" name="messageInput" 
             id="messageInput" placeholder="Message text"
             inputmode="latin" size=59 maxlength=120 
             disabled>
    </label>
    <button id="sendButton" 
            name="sendButton"
            class="buttonsend"
            disabled>Отправить сообщение</button>
    <span style="font-size:0.75em;font-style:italic;
                 float:left;margin-left:5px;color:#444">
         В сообщение можно включить фото и текст
        (вместе или по отдельности)</span>
  </div>
  <b>Узел приема сообщения</b>
  <div class="receivebox" id="receivebox">
    <p>Messages received : </p>
  </div>
</body>
 

Листинг datachannel.css

Содержимое css-файла незначительно отличается от исходного прототипа. Изменения касаются только размеров messagebox и controlbox, а также положения кнопки buttonsend.


.messagebox {
  border: 1px solid black;
  padding: 5px;
  width: 450px;
  height: 90px;
  margin-bottom:10px;
}

.receivebox {
  border: 1px solid black;
  padding: 5px;
  width: 450px;
  height: 280px;
}

.buttonright {
  float: right;
  width: 120px;
}

.buttonleft {
  float: left;
  width: 120px;
}

.buttonsend {
  float: left;
  margin-top:10px;
  margin-bottom:5px;
  width: 160px;
}

.controlbox {
  padding: 5px;
  width: 450px;
  height: 28px;
  margin-bottom:10px;
}
 

Листинг datachannel.js

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

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


(function() {
    // Определение глобальных переменных

    // кнопки управления  
    var btnConnect       = null;
    var btnDisconn       = null;
    var btnSend          = null;
	
    // блоки ввода и получения сообщений
    var boxInput         = null;
    var boxReceive       = null;

    // RTCPeerConnection соединения : локальное и удаленное
    var connectionLocal  = null;
    var connectionRemote = null;

    // RTCDataChannel канал данных : 
    //   локальный (отправитель) и удаленный (получатель)
    var sendChannel      = null;
    var receiveChannel   = null;

    function init()                            {...}
    function createPeerConnection()            {...}
    function handleCreateDescriptionError()    {...}
    function handleLocalAddCandidateSuccess()  {...}
    function handleRemoteAddCandidateSuccess() {...}
    function handleAddCandidateError()         {...}

    function receiveChannelCallback()          {...}
    function handleReceiveChannelStatusChange(){...}
    function handleSendChannelStatusChange()   {...}
    function sendMessage()                     {...}
    function handleReceiveMessage()            {...}

    function closePeerConnection()             {...}

    // Подключение слушателя для вызова метода init после 
    // загрузки страницы и формирования структуры DOM
    window.addEventListener('load', init, false);
})();
 

Листинг метода init

В методе init выполняется инициализация переменных с использованием метода document.getElementById(), т.е. «связываются» переменные с элементами DOM страницы. Кроме этого, к кнопкам управления соединением подключаются слушатели, которые определяют вызываемые при нажатии на кнопки методы.


function init()
{
   btnConnect = document.getElementById('connectButton'   );
   btnDisconn = document.getElementById('disconnectButton');
   btnSend    = document.getElementById('sendButton'      );
   boxInput   = document.getElementById('message'         );
   boxReceive = document.getElementById('receivebox'      );

   // Подключение слушателей к кнопкам управления
   btnConnect.addEventListener('click', 
                               createPeerConnection, false);
   btnDisconn.addEventListener('click', 
                               closePeerConnection , false);
   btnSend   .addEventListener('click', 
                               sendMessage         , false);
}
 

Листинг метода createPeerConnection

Метод createPeerConnection вызывается при нажатии на кнопку btnConnect. В данном методе создаются локальный и удаленный узлы соединения (connectionLocal, connectionRemote). После этого создается канал данных sendChannel типа RTCDataChannel, к которому подключаются слушатели установления связи (onopen) и закрытии канала onclose. К удаленному соединению подключается слушатель установления канала данных ondatachannel.

Следующим шагом к локальному и удаленным узлам соединения подключаются слушатели ICE кандидатов. Они сработают когда будут созданы новые экземпляры ICE для обмена с другим узлом. Таким образом, к каждому узлу соединения PeerConnection подключается обработчик события для icecandidate.

Для установления соединения между двумя узлами последним шагом в методе создается предложение createOffer, которое выглядит очень «навороченным»; на самом деле оно представляет цепочку действий, описанных после листинга.

function createPeerConnection()
{
    // Создание локального и удаленного соединений
    connectionLocal  = new RTCPeerConnection();
    connectionRemote = new RTCPeerConnection();

    // Создание канала данных RTCDataChannel и 
    // подключение слушателей
    sendChannel       = connectionLocal.createDataChannel(
                                           "sendChannel");
    sendChannel.onopen  = handleSendChannelStatusChange;
    sendChannel.onclose = handleSendChannelStatusChange;

    // Подключениек к удаленному соединению слушателя 
    // установления канала
    connectionRemote.ondatachannel = receiveChannelCallback;

    // Подключение слушателей ICE кандидатов для соединений
    connectionLocal.onicecandidate = e => !e.candidate
        || connectionRemote.addIceCandidate(e.candidate)
        .catch(handleAddCandidateError);

    connectionRemote.onicecandidate = e => !e.candidate
        || connectionLocal.addIceCandidate(e.candidate)
        .catch(handleAddCandidateError);

    // Создание предложения offer-answer на установление
	// соединения; старт процесса
    connectionLocal
        .createOffer()
        .then(offer => connectionLocal.setLocalDescription(
                                                    offer))
        .then(() => connectionRemote.setRemoteDescription(
                         connectionLocal.localDescription))
        .then(() => connectionRemote.createAnswer())
        .then(answer => connectionRemote.setLocalDescription(
                                                     answer))
        .then(() => connectionLocal.setRemoteDescription(
                          connectionRemote.localDescription))
        .catch(handleCreateDescriptionError);
}
 

Процесс предложение-ответ

Процесс «предложение-ответ» (offer-answer), описанный в RFC3264 и использующий SDP (Session Description Protocol), выполняется как для установления первого соединения, так и в любой момент, когда формат соединения или какие-либо конфигурации нуждаются в изменении. В этом процессе выделяется два агента : инициатор offerer — это тот, кто первым генерирует SDP-описание сессии для создания новой или модификации существующей, и answerer — это тот, кто получает SDP-описание сессии от другого агента и отвечает ему собственным описанием сессии (answer SDP).

Рассмотрим процесс создания «предложения» и получения «ответа», организуемым в примере, по строкам/частям :

  1. Методом createOffer определяется объект SDP (Session Description Protocol) для создания соединения. Опционально метод может включать объект с ограничениями (поддержка аудио, видео), накладываемыми на соединение. В нашем примере ограничений нет.
  2. Если предложение создано успешно, то вызывается метод RTCPeerConnection.setLocalDescription, которому это предложение (offer) передается.
  3. На следующем шаге устанавливается соединение локального узла с удаленным методом RTCPeerConnection.setRemoteDescription, которому передается предложение локального узла connectionLocal.localDescription. В этот момент удаленный узел узнает о начале установления с ним соединения. В реальном приложении это управляется «сигнальным» сервером для обмена объектами описания соединения.
  4. Наступил момент, когда удаленный узел должен ответить : вызывается метод createAnswer, который генерирует SDP, описывающий необходимые для него условия соединения.
  5. Как только ответ будет подготовлен он передается удаленному соединению remoteConnection вызовом метода RTCPeerConnection.setLocalDescription. Таким образом, формируется удаленный узел соединения, который для удаленного узла является локальным. Обычно в реальном приложении это также выполняется «сигнальным».
  6. В заключении, локальный узел localConnection получает информацию об удаленном узле и настраивает одноранговое соединение вызовом метода RTCPeerConnection.setRemoteDescription с передачей ему предложения удаленного узла connectionRemote.localDescription.
  7. В случае возникновения ошибки перехватчик catch вызывает метод handleCreateDescriptionError, отображающий сообщение об ошибке в консоли браузера.

Листинг метода handleCreateDescriptionError

  // Ошибка может возникнуть как при создании 
  // предложения offer, так и при формировании answer
  function handleCreateDescriptionError(error) {
    console.log("Unable to create an offer: " + 
                  error.toString());
  }
 

Обработка установления соединения

На каждой из сторон успешного однорангового peer-to-peer соединения вызываются RTCPeerConnection обработчики icecandidate, которые можно использовать для выполнения необходимых действий. В примере эти обработчики используются для блокировки кнопки создания соединения connectButton и разблокировки кнопки закрытия соединения disconnectButton. В случае возникновения ошибки создания P2P (peer-to-peer) соединения вызывается метод handleAddCandidateError.


// Функция успешного добавления ICE кандидата
// на локальном узле соединения 
function handleLocalAddCandidateSuccess() {
    btnConnect.disabled = true;
}

// Функция успешного добавления ICE кандидата
// на «удаленном» узле соединения  
function handleRemoteAddCandidateSuccess() {
    btnDisconn.disabled = false;
}

// Метод обработки ошибки при добавлении ICE кандидата
function handleAddCandidateError() {
    console.log("add ICE candidate failed");
}
 

Открытие канала передачи данных удаленного узла

Как только успешно установлено одноранговое P2P соединение RTCPeerConnection для завершения процесса открытия канала данных удаленному узлу связи отправляется событие datachannel, которое вызывает функцию обратной связи receiveChannelCallback, описанную после листинга :


// Функция вызывается после установления соединения P2P и 
// готовности установления канала данных с удаленным узлом

function receiveChannelCallback(event) {
  receiveChannel          =event.channel;
  receiveChannel.onmessage=handleReceiveMessage;
  receiveChannel.onopen   =handleReceiveChannelStatusChange;
  receiveChannel.onclose  =handleReceiveChannelStatusChange;
}

// Обработчик изменения состояния канала данных удаленного
// узла : event.currentTarget имеет тип RTCDataChannel 
function handleReceiveChannelStatusChange(event) {
  if (receiveChannel) {
    // при наличии канала данных 'open'  
    console.log("Receive channel's status has changed to '"+
                 receiveChannel.readyState + "'");
  } else {
    // при отсутствии канала данных 'closed'
    console.log("Receive channel's status has changed to '"+
         event.currentTarget.readyState + "'");
  }
}
 

Свойства события datachannel включают ссылку на RTCDataChannel, представляющий удаленный узел связи и включающий помимо прочего :

  • id – идентификатор;
  • binaryType – тип передаваемых данных, по умолчанию "arraybuffer";
  • label – метку канала данных, определенную в примере как "sendChannel" в методе createPeerConnection;
  • readyState: состояние канала данных ("open" при открытом, "closed" при закрытом).

В методе receiveChannelCallback создается канал, у которого к слушателю события onmessage подключается метод получения данных handleReceiveMessage.

Обработчик handleReceiveChannelStatusChange будет вызываться каждый раз, как только будет меняться статус канала связи, чтобы можно было бы вовремя реагировать. Параметр метода event включает свойство type, имеющее значение либо "open", либо "close". Свойства target и currentTarget параметра event имеют тип RTCDataChannel.

Открытие канала передачи данных локального узла

При открытии канала данных, равно как и при закрытии канала на локальном узле вызывается метод handleSendChannelStatusChange. Если состояние канала изменяется на "open", то это свидетельствует о том, что соединение между двумя P2P узлами установлено успешно. В этом случае блокировки с кнопок «Отправки сообщения» и «Отключения» соединения, а также компонента ввода сообщения разблокируются; кнопка «Подключения» блокируется. При закрытии канала связи все действия выполняются в обратном порядке.


function handleSendChannelStatusChange(event) {
    if (sendChannel) {
        if (sendChannel.readyState === "open") {
            boxInput  .disabled = false;
            btnSend   .disabled = false;
            btnDisconn.disabled = false;
            btnConnect.disabled = true;
            boxInput  .focus();
        } else {
            boxInput  .disabled = true;
            btnSend   .disabled = true;
            btnConnect.disabled = false;
            btnDisconn.disabled = true;
        }
    }
}
 

Отправка сообщения

При нажатии на кнопку «Отправить сообщение» вызывается метод sendMessage, который сначала проверяет наличие захваченного изображения с помощью библиотеки jQuery. Если у элемента photo установлен класс 'picture', значит захват изображения произошел. Отправка сообщения/изображения выполняется вызовом метода sendChannel.send(data), где sendChannel имеет тип RTCDataChannel. При отправке сообщения до окончательного установления соединения, либо при закрытии канала данные буферизируются, если это возможно, в противном случае может выскочить ошибка. Тип отправляемых данных data может быть USVString (Unicode scalar values), Blob, ArrayBuffer или ArrayBufferView (Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array или DataView).

После отправки изображения выполняется проверка текстового сообщения. Если сообщение имеет не нулевой размер, то на втором шаге отправляется текстовое сообщение.


// Отправка сообщения удаленному узлу
function sendMessage() {
    // отправка изображения
    var photo = document.getElementById('photo');
    if ((photo!=null) && ($('#photo').hasClass('picture')))
        sendChannel.send(photo.src);

    // отправка текстового сообщения
    var message = boxInput.value;
    if (message.length > 0)
        sendChannel.send(message);
    
    // очистка поля ввода сообщения    
    boxInput.value = "";
    boxInput.focus();
}
 
  Рейтинг@Mail.ru