Skip to content

Instantly share code, notes, and snippets.

@sandangel
Forked from steveblue/data-channel.config.ts
Created May 11, 2018 07:43
Show Gist options
  • Save sandangel/54c705f9e4d6db9e1bd33b9dff186687 to your computer and use it in GitHub Desktop.
Save sandangel/54c705f9e4d6db9e1bd33b9dff186687 to your computer and use it in GitHub Desktop.
WebRTC DataChannel Angular Service
let uuid = function() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
let r = Math.random() * 16 | 0, v = c === 'x' ? r : ( r & 0x3 | 0x8 );
return v.toString(16);
});
};
let defaultRoom = function() {
let text = '';
let possible = 'ABCDEFGHJKMNOPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz023456789';
for ( let i = 0; i < 5; i++ ) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text.charAt(0).toUpperCase() + text.slice(1);
};
let config = {
room: defaultRoom(),
username: uuid(),
server: 'https://my-server.firebaseio.com/'
};
export default config;
import { NgModule } from '@angular/core';
import { DataChannel } from './data-channel.service';
@NgModule({
providers: [ DataChannel ]
})
export class DataChannelModule {}
import config from './data-channel.config';
import { Injectable, EventEmitter } from '@angular/core';
import { Observable, Observer } from 'rxjs/Observable';
import { Observer } from 'rxjs/Observer';
import 'rxjs/add/operator/share';
// import FirebaseConfig from './firebase.config';
// declare let firebase: any;
let uuid = function () {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
};
let defaultRoom = function () {
let text = '';
let possible = 'ABCDEFGHJKMNOPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz023456789';
for (let i = 0; i < 5; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text.charAt(0).toUpperCase() + text.slice(1);
};
@Injectable()
export class DataChannel {
public id: string;
public room: string;
public name: string;
public conf: any;
public db: any;
public ws: WebSocket; // signal
public wss: WebSocket; // messaging
public announce: WebSocket; // announce
public url: string;
public stun: any;
public remotePeer: any;
public observer: Observable<any>;
public channelObserver: Observer<any>;
public peerConnection: any;
public hasPulse: boolean;
public isOpen: boolean;
public hasWSConnection: boolean;
public hasRTCConnection: boolean;
public channel: any;
// public channels: any;
public dataChannel: any;
public emitter: EventEmitter<any>;
public messages: EventEmitter<any>;
public connections: any;
public websocketConnections: any;
public debug: boolean;
public isWebSocket: boolean;
public count: number;
public config: any;
public store: {
messages: any
};
constructor() {
let self = this;
// firebase.initializeApp(FirebaseConfig);
this.config = config;
this.id = this.config.username || uuid(); // the username, unique id that makes each peer => make uuid?
this.room = this.config.room || defaultRoom(); // the room name.
this.url = this.config.server || 'https://your-name-here.firebaseio.com/'; // replace with your server name
this.name = 'channel'; // the name of the channel
// this.db = firebase.database().ref(); //new Firebase(this.url); // only supports Firebase for now, support for custom web socket server in the future.
this.ws = new WebSocket(`ws://${location.host.split(':')[0]}:5555`);
this.announce = new WebSocket(`ws://${location.host.split(':')[0]}:5556`);
this.wss = new WebSocket(`ws://${location.host.split(':')[0]}:5557`);
this.count = 0;
this.hasPulse = false;
this.isOpen = false;
this.hasWSConnection = false;
this.hasRTCConnection = false;
this.connections = {};
this.websocketConnections = {};
this.isWebSocket = (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window['MSStream']) ? true : false;
this.debug = false;
this.store = { messages: [] };
this.stun = {
iceServers: [{
url: 'stun:stun.l.google.com:19302'
}]
};
this.conf = {
ordered: false,
maxRetransmitTime: 1000
};
// this.channels = {
// announce: this.db.child('announce'),
// signal: this.db.child('messages').child(this.id)
// };
this.ws.onmessage = (msg) => this.onSignal(msg);
this.announce.onmessage = (msg) => this.onAnnounce(msg);
// this.channels.signal.on('child_added', this.onSignal.bind(self));
// this.channels.announce.on('child_added', this.onAnnounce.bind(self));
this.emitter = new EventEmitter();
this.messages = new EventEmitter();
this.observer = new Observable(observer => this.channelObserver = observer).share();
//this.ws.onopen = (msg) => { this.sendSignal(msg.data) };
this.announce.onopen = this.sendAnnounce.bind(this);
// this.sendAnnounce();
}
sendAnnounce() {
let RTCPeerConnection = (<any>window).RTCPeerConnection || (<any>window).mozRTCPeerConnection ||
(<any>window).webkitRTCPeerConnection;
let msg = {
sharedKey: this.room,
id: this.id,
method: !RTCPeerConnection ? 'socket' : 'webrtc'
};
if (!RTCPeerConnection) {
this.isWebSocket = true;
}
this.announce.send(JSON.stringify(msg));
if (this.debug) {
console.log('Announced our sharedKey is ' + this.room);
}
if (this.debug) {
console.log('Announced our ID is ' + this.id);
}
// this.channels.announce.remove(() => {
// this.channels.announce.push(msg);
// if (this.debug) {
// console.log('Announced our sharedKey is ' + this.room);
// }
// if (this.debug) {
// console.log('Announced our ID is ' + this.id);
// }
// });
}
onAnnounce(snapshot) {
let msg = JSON.parse(snapshot.data);
if (msg.id !== this.id && msg.sharedKey === this.room) {
if (this.debug) {
console.log('Discovered matching ' + msg.method + ' announcement from ' + msg.id);
}
if (msg.method !== 'socket') {
this.connections[msg.id] = {
id: msg.id,
isConnected: false
};
}
if (msg.method === 'socket') {
this.websocketConnections[msg.id] = {
id: msg.id,
isConnected: false
};
}
if (msg.method === 'webrtc') {
this.init();
this.connect();
} else {
this.sendSignal({
id: this.id,
key: this.room,
url: this.url,
type: 'ws-offer'
});
if (!this.isOpen) {
this.addWSPeer(msg);
this.initSocket(msg);
}
}
}
}
sendSignal(msg) {
msg.sender = this.id;
if (msg.type !== 'ws-offer') {
for (let prop in this.connections) {
if (this.connections[prop].isConnected === false) {
// send message to all recipients
this.ws.send(JSON.stringify(msg));
// this.db.child('messages').child(this.connections[prop].id).push(msg);
if (this.debug) {
console.log('Sending offer from ' + this.id + ' to ' + this.connections[prop].id);
}
}
}
} else {
for (let pr in this.websocketConnections) {
if (this.websocketConnections[pr].isConnected === false) {
// send message to all recipients
this.ws.send(JSON.stringify(msg));
// this.db.child('messages').child(this.websocketConnections[pr].id).push(msg);
if (this.debug) {
console.log('Sending ws-offer from ' + this.id + ' to ' + this.websocketConnections[pr].id);
}
if (!this.hasWSConnection) { // TODO: figure out why signaling is broken here
this.addWSPeer({ id: this.websocketConnections[pr].id });
this.initSocket({ id: this.websocketConnections[pr].id });
}
}
}
}
}
onOffer(msg) {
let RTCSessionDescription = (<any>window).RTCSessionDescription || (<any>window).mozRTCSessionDescription;
this.hasPulse = true;
if (this.debug) {
console.log('Client has pulse');
}
this.connections[msg.sender] = {
id: msg.sender,
isConnected: false
};
this.init();
this.sendCandidates();
if (RTCSessionDescription) {
this.peerConnection.setRemoteDescription(new RTCSessionDescription(msg), () => {
// console.warn('Set setRemoteDescription, creating Answer');
this.peerConnection.createAnswer((sessionDescription) => {
if (this.debug) {
console.log('Sending answer to ' + msg.sender);
}
this.peerConnection.setLocalDescription(sessionDescription);
this.sendSignal(sessionDescription.toJSON());
}, (err) => {
if (this.debug) {
console.error('Could not create offer', err);
}
});
});
}
}
onAnswerSignal(msg) {
let RTCSessionDescription = (<any>window).RTCSessionDescription || (<any>window).mozRTCSessionDescription;
if (this.debug) {
console.log('Handling answer from ' + msg.sender);
}
if (RTCSessionDescription) {
this.peerConnection.setRemoteDescription(new RTCSessionDescription(msg));
}
}
onCandidateSignal(msg) {
// console.warn(msg, this.id, msg.sender);
let candidate = new (<any>window).RTCIceCandidate(msg);
if (this.debug) {
console.log('Adding candidate to peerConnection: ' + msg.sender);
}
this.peerConnection.addIceCandidate(candidate);
}
onSignal(snapshot) {
let msg = JSON.parse(snapshot.data);
let sender = msg.sender;
let type = msg.type;
if (this.debug) {
console.log('Received a \'' + type + '\' signal from ' + sender + ' of type ' + type);
}
if (type === 'message') {
this.onWebSocketMessage(msg);
}
if (type === 'ws-offer') {
if (!this.hasWSConnection) {
this.addWSPeer(msg);
this.initSocket(msg);
}
}
if (type === 'offer') {
this.onOffer(msg);
}
if (type === 'answer') {
this.onAnswerSignal(msg);
}
if (type === 'candidate' && this.hasPulse) {
this.onCandidateSignal(msg);
}
}
sendCandidates() {
this.peerConnection.onicecandidate = this.onICECandidate.bind(this);
}
onICEStateChange() {
// console.warn(this.peerConnection.iceConnectionState);
if (this.peerConnection.iceConnectionState === 'disconnected') {
if (this.debug) {
console.log('Client disconnected!');
}
this.sendAnnounce();
}
}
onICECandidate(ev) {
let candidate = ev.candidate;
if (candidate) {
candidate = candidate.toJSON();
candidate.type = 'candidate';
if (this.debug) {
console.log('Sending candidate');
}
this.sendSignal(candidate);
} else {
if (this.debug) {
console.log('All candidates sent');
}
}
}
onDataChannel(ev) {
ev.channel.onmessage = this.onDataChannelMessage.bind(this);
}
onDataChannelOpen() {
if (this.debug) {
console.log('Data channel created! The channel is: ' + this.channel.readyState);
}
if (this.channel.readyState === 'open') {
this.isOpen = true;
this.emitter.emit('open');
}
}
onDataChannelClosed() {
if (this.debug) {
console.log('The data channel has been closed!');
}
}
onDataChannelMessage(ev) {
let msg = JSON.parse(ev.data);
this.store.messages.push({
id: this.count++,
data: JSON.parse(msg.data),
sender: msg.sender,
createdAt: new Date()
});
this.messages.emit(this.store.messages[this.store.messages.length -1]);
this.channelObserver.next(this.store.messages);
if (this.debug) {
console.log('Received Message: ' + ev.data);
}
}
onWebSocketMessage(ev) {
this.store.messages.push({
id: this.count++,
data: JSON.parse(ev.data),
sender: ev.sender,
createdAt: new Date()
});
this.messages.emit(this.store.messages[this.store.messages.length - 1]);
this.channelObserver.next(this.store.messages);
if (this.debug) {
console.log('Received Message: ' + ev.data, {
id: this.count++,
data: JSON.parse(ev.data),
sender: ev.sender,
createdAt: new Date()
});
}
}
onWebSocketSignal(snapshot) {
let msg = JSON.parse(snapshot.data);
if (msg.constructor.name === 'String') {
msg = JSON.parse(msg);
}
// if ( sender === this.remotePeer ) {
if (this.debug) {
console.log('Received a \'' + msg.type + '\' signal from ' + msg.sender + ' of type ' + msg.type);
}
if (msg.type === 'message') {
this.onWebSocketMessage(msg);
}
if (msg.type === 'ws-offer') {
this.addWSPeer(msg);
this.initSocket(msg);
}
// if (msg.type === 'offer') {
// this.onOffer(msg);
// }
// if (msg.type === 'answer') {
// this.onAnswerSignal(msg);
// }
// if (msg.type === 'candidate' && this.hasPulse) {
// this.onCandidateSignal(msg);
// }
// }
}
send(data: any) {
let msg = JSON.stringify({
type: 'message',
sender: this.id,
data: data
});
if (this.debug) {
console.log(JSON.parse(JSON.stringify(msg, null, 4)));
}
if (Object.keys(this.connections).length > 0) {
this.channel.send(msg);
}
if (Object.keys(this.websocketConnections).length > 0) {
this.sendSocketMessage(data);
}
}
sendSocketMessage(data: any) {
let msg = JSON.stringify({
type: 'message',
sender: this.id,
data: data
});
for (let prop in this.websocketConnections) {
if (this.websocketConnections) {
if (this.debug) {
console.log('Sending WebSocket message from: ' + this.id + ' to: ' + this.websocketConnections[prop].id, data);
}
// send a message to all recipients
this.wss.send(JSON.stringify(msg));
// this.db.child('messages').child( this.websocketConnections[prop].id ).push(msg);
}
}
}
createWebSocketChannel() {
return {
send: this.sendSocketMessage.bind(this)
};
}
connect() {
this.sendCandidates();
this.peerConnection.createOffer((sessionDescription) => {
if (this.debug) {
console.log('Sending offer');
}
this.peerConnection.setLocalDescription(sessionDescription);
this.sendSignal(sessionDescription.toJSON());
}, function (err) {
console.error('Could not create offer', err);
});
}
init() {
let RTCPeerConnection = (<any>window).RTCPeerConnection || (<any>window).mozRTCPeerConnection ||
(<any>window).webkitRTCPeerConnection;
if (this.hasRTCConnection === false) {
this.peerConnection = new RTCPeerConnection(this.stun);
this.peerConnection.ondatachannel = this.onDataChannel.bind(this);
this.peerConnection.oniceconnectionstatechange = this.onICEStateChange.bind(this);
this.channel = this.peerConnection.createDataChannel(this.name, this.conf);
this.channel.onopen = this.onDataChannelOpen.bind(this);
this.channel.onmessage = this.onDataChannelMessage.bind(this);
this.hasRTCConnection = true;
if (this.debug) {
console.log('Setting up peer connection');
}
}
}
addWSPeer(conf) {
if (!this.websocketConnections[conf.id]) {
this.websocketConnections[conf.id] = {
id: conf.id,
isConnected: true
};
}
if (this.debug) {
console.log('Setting up websocket connection with ' + this.websocketConnections[conf.id].id);
}
}
initSocket(conf: any) {
if (!this.hasWSConnection) {
this.isOpen = true;
this.hasWSConnection = true;
this.channel = this.createWebSocketChannel();
// create a child
// this.channels.websocket = this.db.child('messages').child(this.id);
// this.channels.websocket.on('child_added', this.onWebSocketSignal.bind(this));
this.wss.onmessage = (msg) => this.onWebSocketSignal(msg);
this.hasWSConnection = true;
this.hasPulse = true;
this.emitter.emit('open');
if (this.debug) {
console.log('Client has pulse');
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment