Skip to content

Draft: Add WebRTC functionality #409

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 221 additions & 0 deletions src/common/api/webrtc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import AppDispatcher from '../app-dispatcher';

const OFFSET_TYPE = 2;
const OFFSET_VERSION = 4;

class WebRTC {
constructor(sessionurl, identifier, callbacks) {
this.identifier = identifier
this.first = false;
this.polite = false;
this.ignoreOffer = false;
this.makingOffer = false;

this.peerConnection = null;
this.dataChannel = null;
this.signalingClient = null;

this.iceUsername = 'villas';
this.icePassword = 'villas';
this.iceUrls = [
'stun:stun.0l.de:3478',
'turn:turn.0l.de:3478?transport=udp',
'turn:turn.0l.de:3478?transport=tcp'
];

console.log(callbacks)
this.onOpen = callbacks.onOpen.bind(this);
this.onMessage = callbacks.onMessage.bind(this);
this.onClose = callbacks.onClose.bind(this);
this.connectPeers(sessionurl, callbacks);
}

connectPeers(sessionurl, callbacks) {
// Create the local connection and its event listeners
this.peerConnection = new RTCPeerConnection({
iceServers: [{
username: this.iceUsername,
credential: this.icePassword,
urls: this.iceUrls
}]
});

this.peerConnection.onicecandidate = this.handleIceCandidate.bind(this);
this.peerConnection.onnegotiationneeded = this.handleNegotationNeeded.bind(this);
this.peerConnection.ondatachannel = this.handleNewDataChannel.bind(this)

this.peerConnection.onconnectionstatechange = () => console.info('Connection state changed:', this.peerConnection.connectionState);
this.peerConnection.onsignalingstatechange = () => console.info('Signaling state changed:', this.peerConnection.signalingState);
this.peerConnection.oniceconnectionstatechange = () => console.info('ICE connection state changed:', this.peerConnection.iceConnectionState);
this.peerConnection.onicegatheringstatechange = () => console.info('ICE gathering state changed:', this.peerConnection.iceGatheringState);

this.hallo()

this.signalingClient = new WebSocket(sessionurl);
this.signalingClient.onmessage = this.handleSignalingMessage.bind(this);

// Some more logging
this.signalingClient.onopen = (e) => console.info('Connected to signaling channel', e);
this.signalingClient.onerror = (e) => console.error('Failed to establish signaling connection', e);
}

hallo() {
console.info("peer connection (hallo):")
console.info(this.peerConnection)
}

handleIceCandidate(event) {
if (event.candidate == null) {
console.info('Candidate gathering completed');
return;
}

console.info('New local ICE Candidate', event.candidate);

let msg = {
candidate: event.candidate.toJSON()
};
console.info('Sending signaling message', msg);
this.signalingClient.send(JSON.stringify(msg));
}

async handleNegotationNeeded() {
console.info('Negotation needed!');

try {
this.makingOffer = true;
await this.peerConnection.setLocalDescription();
let msg = {
description: this.peerConnection.localDescription.toJSON()
};
console.info('Sending signaling message', msg);
this.signalingClient.send(JSON.stringify(msg));
} catch (err) {
console.error(err);
} finally {
this.makingOffer = false;
}
}

handleNewDataChannel(e) {
console.info('New datachannel', e.channel)

this.handleDataChannel(e.channel);
}

handleDataChannel(ch) {
this.dataChannel = ch;

this.dataChannel.onopen = () => console.info('Datachannel opened');
this.dataChannel.onclose = () => console.info('Datachannel closed');
this.dataChannel.onmessage = this.handleDataChannelMessage.bind(this);
}

async handleSignalingMessage(event) {
let msg = JSON.parse(event.data);

console.info('Received signaling message', msg);

try {
if (msg.control !== undefined) {
this.first = true;
for (var connection of msg.control.connections) {
if (connection.id < msg.control.connection_id)
this.first = false;
}

this.polite = this.first;

console.info('Role', {
polite: this.polite,
first: this.first
})

if (!this.first) {
// Create the data channel and establish its event listeners
let ch = this.peerConnection.createDataChannel('villas');

this.handleDataChannel(ch);
}
} else if (msg.description !== undefined) {
const offerCollision = (msg.description.type == 'offer') &&
(this.makingOffer || this.peerConnection.signalingState != 'stable');

this.ignoreOffer = !this.polite && offerCollision;
if (this.ignoreOffer) {
return;
}

await this.peerConnection.setRemoteDescription(msg.description);
console.info(msg.description);
if (msg.description.type == 'offer') {
await this.peerConnection.setLocalDescription();
let msg = {
description: this.peerConnection.localDescription.toJSON()
}
this.signalingClient.send(JSON.stringify(msg))
}
} else if (msg.candidate !== undefined) {
try {
console.info('New remote ICE candidate', msg.candidate);
await this.peerConnection.addIceCandidate(msg.candidate);
} catch (err) {
if (!this.ignoreOffer) {
throw err;
}
}
}
} catch (err) {
console.error(err);
}
}

// Handle onmessage events for the receiving channel.
// These are the data messages sent by the sending channel.
async handleDataChannelMessage(event) {
let data = await event.data.arrayBuffer()
this.onMessage(data, this.identifier)
}

disconnectPeers() {
console.log("disconnecting peers")

if (this.signalingClient) {
console.info("close signaling client")
this.signalingClient.close()
}

if (this.dataChannel) {
console.info("close data channel")
this.dataChannel.close();
}

if (this.peerConnection) {
console.info("close peer connection")
this.peerConnection.close();
}

this.dataChannel = null;
this.peerConnection = null;
this.signalingClient = null;
}

}

export default WebRTC;
26 changes: 22 additions & 4 deletions src/ic/ic-data-data-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
******************************************************************************/

import WebsocketAPI from '../common/api/websocket-api';
import WebRTC from '../common/api/webrtc';
import AppDispatcher from '../common/app-dispatcher';
import RestAPI from "../common/api/rest-api";

Expand All @@ -25,14 +26,22 @@ const OFFSET_VERSION = 4;
class IcDataDataManager {
constructor() {
this._sockets = {};
this._webrtc_connections = {};
}

open(websocketurl, identifier) {
// pass signals to onOpen callback
if (this._sockets[identifier] != null)
return; // already open?

this._sockets[identifier] = new WebsocketAPI(websocketurl, { onOpen: (event) => this.onOpen(event, identifier, true), onClose: (event) => this.onClose(event, identifier), onMessage: (event) => this.onMessage(event, identifier) });
this._sockets[identifier] = new WebsocketAPI(websocketurl, { onOpen: (event) => this.onOpen(event, identifier, true), onClose: (event) => this.onClose(event, identifier), onMessage: (event) => this.onMessage(event.data, identifier) });
}

openWebRTC(sessionurl, identifier) {
if (this._webrtc_connections[identifier] != null)
return; // already connected

this._webrtc_connections[identifier] = new WebRTC(sessionurl, identifier, { onOpen: (event) => this.onOpen(event, identifier, true), onClose: (event) => this.onClose(event, identifier), onMessage: (event) => this.onMessage(event, identifier) });
}

update(websocketurl, identifier) {
Expand All @@ -52,6 +61,14 @@ class IcDataDataManager {
delete this._sockets[identifier];
}
}

// close all open WebRTC connections
for (var rtc_id in this._webrtc_connections) {
if (this._webrtc_connections.hasOwnProperty(rtc_id)) {
this._webrtc_connections[rtc_id].disconnectPeers();
delete this._webrtc_connections[rtc_id];
}
}
}

send(message, identifier) {
Expand Down Expand Up @@ -85,8 +102,9 @@ class IcDataDataManager {
delete this._sockets[identifier];
}

onMessage(event, identifier) {
var msgs = this.bufferToMessageArray(event.data);
onMessage(dataBuffer, identifier) {
console.log(dataBuffer)
var msgs = this.bufferToMessageArray(dataBuffer);

if (msgs.length > 0) {
AppDispatcher.dispatch({
Expand Down Expand Up @@ -166,7 +184,7 @@ class IcDataDataManager {
return buffer;
}

updateSignalValueInWidgets(signalID, newValues){
updateSignalValueInWidgets(signalID, newValues) {
AppDispatcher.dispatch({
type: 'widgets/signal-value-changed',
signalID: signalID,
Expand Down
6 changes: 6 additions & 0 deletions src/ic/ic-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ class InfrastructureComponentStore extends ArrayStore {
for (let ic of action.data) {
if (ic.websocketurl != null && ic.websocketurl !== '') {
ICDataDataManager.open(ic.websocketurl, ic.id);
} else if (ic.statusupdateraw != null && ic.statusupdateraw.properties != null) {
let rawProps = ic.statusupdateraw.properties
if (rawProps != null && typeof rawProps.server !== 'undefined') {
let url = rawProps.server + '/' + rawProps.session
ICDataDataManager.openWebRTC(url, ic.id);
}
} else {
NotificationsDataManager.addNotification(NotificationsFactory.WEBSOCKET_URL_WARN(ic.name, ic.uuid));
}
Expand Down