UNPKG

9.59 kBJavaScriptView Raw
1var util = require('util');
2var webrtc = require('webrtcsupport');
3var PeerConnection = require('rtcpeerconnection');
4var WildEmitter = require('wildemitter');
5var FileTransfer = require('filetransfer');
6
7// the inband-v1 protocol is sending metadata inband in a serialized JSON object
8// followed by the actual data. Receiver closes the datachannel upon completion
9var INBAND_FILETRANSFER_V1 = 'https://simplewebrtc.com/protocol/filetransfer#inband-v1';
10
11function Peer(options) {
12 var self = this;
13
14 this.id = options.id;
15 this.parent = options.parent;
16 this.type = options.type || 'video';
17 this.oneway = options.oneway || false;
18 this.sharemyscreen = options.sharemyscreen || false;
19 this.browserPrefix = options.prefix;
20 this.stream = options.stream;
21 this.enableDataChannels = options.enableDataChannels === undefined ? this.parent.config.enableDataChannels : options.enableDataChannels;
22 this.receiveMedia = options.receiveMedia || this.parent.config.receiveMedia;
23 this.channels = {};
24 this.sid = options.sid || Date.now().toString();
25 // Create an RTCPeerConnection via the polyfill
26 this.pc = new PeerConnection(this.parent.config.peerConnectionConfig, this.parent.config.peerConnectionConstraints);
27 this.pc.on('ice', this.onIceCandidate.bind(this));
28 this.pc.on('offer', function (offer) {
29 self.send('offer', offer);
30 });
31 this.pc.on('answer', function (offer) {
32 self.send('answer', offer);
33 });
34 this.pc.on('addStream', this.handleRemoteStreamAdded.bind(this));
35 this.pc.on('addChannel', this.handleDataChannelAdded.bind(this));
36 this.pc.on('removeStream', this.handleStreamRemoved.bind(this));
37 // Just fire negotiation needed events for now
38 // When browser re-negotiation handling seems to work
39 // we can use this as the trigger for starting the offer/answer process
40 // automatically. We'll just leave it be for now while this stabalizes.
41 this.pc.on('negotiationNeeded', this.emit.bind(this, 'negotiationNeeded'));
42 this.pc.on('iceConnectionStateChange', this.emit.bind(this, 'iceConnectionStateChange'));
43 this.pc.on('iceConnectionStateChange', function () {
44 switch (self.pc.iceConnectionState) {
45 case 'failed':
46 // currently, in chrome only the initiator goes to failed
47 // so we need to signal this to the peer
48 if (self.pc.pc.peerconnection.localDescription.type === 'offer') {
49 self.parent.emit('iceFailed', self);
50 self.send('connectivityError');
51 }
52 break;
53 }
54 });
55 this.pc.on('signalingStateChange', this.emit.bind(this, 'signalingStateChange'));
56 this.logger = this.parent.logger;
57
58 // handle screensharing/broadcast mode
59 if (options.type === 'screen') {
60 if (this.parent.localScreen && this.sharemyscreen) {
61 this.logger.log('adding local screen stream to peer connection');
62 this.pc.addStream(this.parent.localScreen);
63 this.broadcaster = options.broadcaster;
64 }
65 } else {
66 this.parent.localStreams.forEach(function (stream) {
67 self.pc.addStream(stream);
68 });
69 }
70
71 // call emitter constructor
72 WildEmitter.call(this);
73
74 this.on('channelOpen', function (channel) {
75 if (channel.protocol === INBAND_FILETRANSFER_V1) {
76 channel.onmessage = function (event) {
77 var metadata = JSON.parse(event.data);
78 var receiver = new FileTransfer.Receiver();
79 receiver.receive(metadata, channel);
80 self.emit('fileTransfer', metadata, receiver);
81 receiver.on('receivedFile', function (file, metadata) {
82 receiver.channel.close();
83 });
84 };
85 }
86 });
87
88 // proxy events to parent
89 this.on('*', function () {
90 self.parent.emit.apply(self.parent, arguments);
91 });
92}
93
94util.inherits(Peer, WildEmitter);
95
96Peer.prototype.handleMessage = function (message) {
97 var self = this;
98
99 this.logger.log('getting', message.type, message);
100
101 if (message.prefix) this.browserPrefix = message.prefix;
102
103 if (message.type === 'offer') {
104 // workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1064247
105 message.payload.sdp = message.payload.sdp.replace('a=fmtp:0 profile-level-id=0x42e00c;packetization-mode=1\r\n', '');
106 this.pc.handleOffer(message.payload, function (err) {
107 if (err) {
108 return;
109 }
110 // auto-accept
111 self.pc.answer(self.receiveMedia, function (err, sessionDescription) {
112 //self.send('answer', sessionDescription);
113 });
114 });
115 } else if (message.type === 'answer') {
116 this.pc.handleAnswer(message.payload);
117 } else if (message.type === 'candidate') {
118 this.pc.processIce(message.payload);
119 } else if (message.type === 'connectivityError') {
120 this.parent.emit('connectivityError', self);
121 } else if (message.type === 'mute') {
122 this.parent.emit('mute', {id: message.from, name: message.payload.name});
123 } else if (message.type === 'unmute') {
124 this.parent.emit('unmute', {id: message.from, name: message.payload.name});
125 }
126};
127
128// send via signalling channel
129Peer.prototype.send = function (messageType, payload) {
130 var message = {
131 to: this.id,
132 sid: this.sid,
133 broadcaster: this.broadcaster,
134 roomType: this.type,
135 type: messageType,
136 payload: payload,
137 prefix: webrtc.prefix
138 };
139 this.logger.log('sending', messageType, message);
140 this.parent.emit('message', message);
141};
142
143// send via data channel
144// returns true when message was sent and false if channel is not open
145Peer.prototype.sendDirectly = function (channel, messageType, payload) {
146 var message = {
147 type: messageType,
148 payload: payload
149 };
150 this.logger.log('sending via datachannel', channel, messageType, message);
151 var dc = this.getDataChannel(channel);
152 if (dc.readyState != 'open') return false;
153 dc.send(JSON.stringify(message));
154 return true;
155};
156
157// Internal method registering handlers for a data channel and emitting events on the peer
158Peer.prototype._observeDataChannel = function (channel) {
159 var self = this;
160 channel.onclose = this.emit.bind(this, 'channelClose', channel);
161 channel.onerror = this.emit.bind(this, 'channelError', channel);
162 channel.onmessage = function (event) {
163 self.emit('channelMessage', self, channel.label, JSON.parse(event.data), channel, event);
164 };
165 channel.onopen = this.emit.bind(this, 'channelOpen', channel);
166};
167
168// Fetch or create a data channel by the given name
169Peer.prototype.getDataChannel = function (name, opts) {
170 if (!webrtc.supportDataChannel) return this.emit('error', new Error('createDataChannel not supported'));
171 var channel = this.channels[name];
172 opts || (opts = {});
173 if (channel) return channel;
174 // if we don't have one by this label, create it
175 channel = this.channels[name] = this.pc.createDataChannel(name, opts);
176 this._observeDataChannel(channel);
177 return channel;
178};
179
180Peer.prototype.onIceCandidate = function (candidate) {
181 if (this.closed) return;
182 if (candidate) {
183 this.send('candidate', candidate);
184 } else {
185 this.logger.log("End of candidates.");
186 }
187};
188
189Peer.prototype.start = function () {
190 var self = this;
191
192 // well, the webrtc api requires that we either
193 // a) create a datachannel a priori
194 // b) do a renegotiation later to add the SCTP m-line
195 // Let's do (a) first...
196 if (this.enableDataChannels) {
197 this.getDataChannel('simplewebrtc');
198 }
199
200 this.pc.offer(this.receiveMedia, function (err, sessionDescription) {
201 //self.send('offer', sessionDescription);
202 });
203};
204
205Peer.prototype.icerestart = function () {
206 var constraints = this.receiveMedia;
207 constraints.mandatory.IceRestart = true;
208 this.pc.offer(constraints, function (err, success) { });
209};
210
211Peer.prototype.end = function () {
212 if (this.closed) return;
213 this.pc.close();
214 this.handleStreamRemoved();
215};
216
217Peer.prototype.handleRemoteStreamAdded = function (event) {
218 var self = this;
219 if (this.stream) {
220 this.logger.warn('Already have a remote stream');
221 } else {
222 this.stream = event.stream;
223 // FIXME: addEventListener('ended', ...) would be nicer
224 // but does not work in firefox
225 this.stream.onended = function () {
226 self.end();
227 };
228 this.parent.emit('peerStreamAdded', this);
229 }
230};
231
232Peer.prototype.handleStreamRemoved = function () {
233 this.parent.peers.splice(this.parent.peers.indexOf(this), 1);
234 this.closed = true;
235 this.parent.emit('peerStreamRemoved', this);
236};
237
238Peer.prototype.handleDataChannelAdded = function (channel) {
239 this.channels[channel.label] = channel;
240 this._observeDataChannel(channel);
241};
242
243Peer.prototype.sendFile = function (file) {
244 var sender = new FileTransfer.Sender();
245 var dc = this.getDataChannel('filetransfer' + (new Date()).getTime(), {
246 protocol: INBAND_FILETRANSFER_V1
247 });
248 // override onopen
249 dc.onopen = function () {
250 dc.send(JSON.stringify({
251 size: file.size,
252 name: file.name
253 }));
254 sender.send(file, dc);
255 };
256 // override onclose
257 dc.onclose = function () {
258 console.log('sender received transfer');
259 sender.emit('complete');
260 };
261 return sender;
262};
263
264module.exports = Peer;