peerjs-rails 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +34 -0
- data/Rakefile +2 -0
- data/lib/peerjs/rails.rb +8 -0
- data/lib/peerjs/rails/version.rb +5 -0
- data/peerjs-rails.gemspec +23 -0
- data/vendor/assets/javascripts/peer.js +2711 -0
- data/vendor/assets/javascripts/peer.min.js +1 -0
- metadata +82 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: cf6ed23f32eb1c5d1f0638d534f4ce39209d819d
|
|
4
|
+
data.tar.gz: 7224b53cec59794e4b9409f25001d25efc1e383b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5db94f95f675ac76514851692750824c6e3cac15227b8e4cb0283d142b1fc0da55ce1988b471d4b103f6c63cfd3bccb8ddb1b1850b46420e1662a2cd2be37b1c
|
|
7
|
+
data.tar.gz: c6792954f643d314e825e541f03ed5ff63806d989108bb80b5a98fec4a702a0b06f0b2edeb42431b4bd2665161fd9e17e84798d71751da1b7626d17bb89c53f0
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2014 Anton Shemerey
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Peerjs::Rails
|
|
2
|
+
|
|
3
|
+
Asset Pipline wrapper, for awesome [PeerJS](https://github.com/peers/peerjs), version `0.3.9`
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'peerjs-rails'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
$ bundle
|
|
16
|
+
|
|
17
|
+
Now you are able to add this awesome feature into your product by adding
|
|
18
|
+
following into your `application.js` file:
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
//= require peer
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
TODO: Write usage instructions here
|
|
27
|
+
|
|
28
|
+
## Contributing
|
|
29
|
+
|
|
30
|
+
1. Fork it ( https://github.com/shemerey/peerjs-rails/fork )
|
|
31
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
32
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
33
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
34
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/lib/peerjs/rails.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require 'peerjs/rails/version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = "peerjs-rails"
|
|
8
|
+
spec.version = Peerjs::Rails::VERSION
|
|
9
|
+
spec.authors = ["Anton Shemerey"]
|
|
10
|
+
spec.email = ["shemerey@gmail.com"]
|
|
11
|
+
spec.summary = %q{PeerJS Asset Pipline wrapper}
|
|
12
|
+
spec.description = %q{Asset Pipline wrapper around "PeerJS"}
|
|
13
|
+
spec.homepage = "https://github.com/shemerey/peerjs-rails"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
|
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
19
|
+
spec.require_paths = ["lib"]
|
|
20
|
+
|
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
|
23
|
+
end
|
|
@@ -0,0 +1,2711 @@
|
|
|
1
|
+
/*! peerjs.js build:0.3.9, development. Copyright(c) 2013 Michelle Bu <michelle@michellebu.com> */
|
|
2
|
+
(function(exports){
|
|
3
|
+
var binaryFeatures = {};
|
|
4
|
+
binaryFeatures.useBlobBuilder = (function(){
|
|
5
|
+
try {
|
|
6
|
+
new Blob([]);
|
|
7
|
+
return false;
|
|
8
|
+
} catch (e) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
})();
|
|
12
|
+
|
|
13
|
+
binaryFeatures.useArrayBufferView = !binaryFeatures.useBlobBuilder && (function(){
|
|
14
|
+
try {
|
|
15
|
+
return (new Blob([new Uint8Array([])])).size === 0;
|
|
16
|
+
} catch (e) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
})();
|
|
20
|
+
|
|
21
|
+
exports.binaryFeatures = binaryFeatures;
|
|
22
|
+
exports.BlobBuilder = window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder || window.BlobBuilder;
|
|
23
|
+
|
|
24
|
+
function BufferBuilder(){
|
|
25
|
+
this._pieces = [];
|
|
26
|
+
this._parts = [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
BufferBuilder.prototype.append = function(data) {
|
|
30
|
+
if(typeof data === 'number') {
|
|
31
|
+
this._pieces.push(data);
|
|
32
|
+
} else {
|
|
33
|
+
this.flush();
|
|
34
|
+
this._parts.push(data);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
BufferBuilder.prototype.flush = function() {
|
|
39
|
+
if (this._pieces.length > 0) {
|
|
40
|
+
var buf = new Uint8Array(this._pieces);
|
|
41
|
+
if(!binaryFeatures.useArrayBufferView) {
|
|
42
|
+
buf = buf.buffer;
|
|
43
|
+
}
|
|
44
|
+
this._parts.push(buf);
|
|
45
|
+
this._pieces = [];
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
BufferBuilder.prototype.getBuffer = function() {
|
|
50
|
+
this.flush();
|
|
51
|
+
if(binaryFeatures.useBlobBuilder) {
|
|
52
|
+
var builder = new BlobBuilder();
|
|
53
|
+
for(var i = 0, ii = this._parts.length; i < ii; i++) {
|
|
54
|
+
builder.append(this._parts[i]);
|
|
55
|
+
}
|
|
56
|
+
return builder.getBlob();
|
|
57
|
+
} else {
|
|
58
|
+
return new Blob(this._parts);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
exports.BinaryPack = {
|
|
62
|
+
unpack: function(data){
|
|
63
|
+
var unpacker = new Unpacker(data);
|
|
64
|
+
return unpacker.unpack();
|
|
65
|
+
},
|
|
66
|
+
pack: function(data){
|
|
67
|
+
var packer = new Packer();
|
|
68
|
+
packer.pack(data);
|
|
69
|
+
var buffer = packer.getBuffer();
|
|
70
|
+
return buffer;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function Unpacker (data){
|
|
75
|
+
// Data is ArrayBuffer
|
|
76
|
+
this.index = 0;
|
|
77
|
+
this.dataBuffer = data;
|
|
78
|
+
this.dataView = new Uint8Array(this.dataBuffer);
|
|
79
|
+
this.length = this.dataBuffer.byteLength;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
Unpacker.prototype.unpack = function(){
|
|
84
|
+
var type = this.unpack_uint8();
|
|
85
|
+
if (type < 0x80){
|
|
86
|
+
var positive_fixnum = type;
|
|
87
|
+
return positive_fixnum;
|
|
88
|
+
} else if ((type ^ 0xe0) < 0x20){
|
|
89
|
+
var negative_fixnum = (type ^ 0xe0) - 0x20;
|
|
90
|
+
return negative_fixnum;
|
|
91
|
+
}
|
|
92
|
+
var size;
|
|
93
|
+
if ((size = type ^ 0xa0) <= 0x0f){
|
|
94
|
+
return this.unpack_raw(size);
|
|
95
|
+
} else if ((size = type ^ 0xb0) <= 0x0f){
|
|
96
|
+
return this.unpack_string(size);
|
|
97
|
+
} else if ((size = type ^ 0x90) <= 0x0f){
|
|
98
|
+
return this.unpack_array(size);
|
|
99
|
+
} else if ((size = type ^ 0x80) <= 0x0f){
|
|
100
|
+
return this.unpack_map(size);
|
|
101
|
+
}
|
|
102
|
+
switch(type){
|
|
103
|
+
case 0xc0:
|
|
104
|
+
return null;
|
|
105
|
+
case 0xc1:
|
|
106
|
+
return undefined;
|
|
107
|
+
case 0xc2:
|
|
108
|
+
return false;
|
|
109
|
+
case 0xc3:
|
|
110
|
+
return true;
|
|
111
|
+
case 0xca:
|
|
112
|
+
return this.unpack_float();
|
|
113
|
+
case 0xcb:
|
|
114
|
+
return this.unpack_double();
|
|
115
|
+
case 0xcc:
|
|
116
|
+
return this.unpack_uint8();
|
|
117
|
+
case 0xcd:
|
|
118
|
+
return this.unpack_uint16();
|
|
119
|
+
case 0xce:
|
|
120
|
+
return this.unpack_uint32();
|
|
121
|
+
case 0xcf:
|
|
122
|
+
return this.unpack_uint64();
|
|
123
|
+
case 0xd0:
|
|
124
|
+
return this.unpack_int8();
|
|
125
|
+
case 0xd1:
|
|
126
|
+
return this.unpack_int16();
|
|
127
|
+
case 0xd2:
|
|
128
|
+
return this.unpack_int32();
|
|
129
|
+
case 0xd3:
|
|
130
|
+
return this.unpack_int64();
|
|
131
|
+
case 0xd4:
|
|
132
|
+
return undefined;
|
|
133
|
+
case 0xd5:
|
|
134
|
+
return undefined;
|
|
135
|
+
case 0xd6:
|
|
136
|
+
return undefined;
|
|
137
|
+
case 0xd7:
|
|
138
|
+
return undefined;
|
|
139
|
+
case 0xd8:
|
|
140
|
+
size = this.unpack_uint16();
|
|
141
|
+
return this.unpack_string(size);
|
|
142
|
+
case 0xd9:
|
|
143
|
+
size = this.unpack_uint32();
|
|
144
|
+
return this.unpack_string(size);
|
|
145
|
+
case 0xda:
|
|
146
|
+
size = this.unpack_uint16();
|
|
147
|
+
return this.unpack_raw(size);
|
|
148
|
+
case 0xdb:
|
|
149
|
+
size = this.unpack_uint32();
|
|
150
|
+
return this.unpack_raw(size);
|
|
151
|
+
case 0xdc:
|
|
152
|
+
size = this.unpack_uint16();
|
|
153
|
+
return this.unpack_array(size);
|
|
154
|
+
case 0xdd:
|
|
155
|
+
size = this.unpack_uint32();
|
|
156
|
+
return this.unpack_array(size);
|
|
157
|
+
case 0xde:
|
|
158
|
+
size = this.unpack_uint16();
|
|
159
|
+
return this.unpack_map(size);
|
|
160
|
+
case 0xdf:
|
|
161
|
+
size = this.unpack_uint32();
|
|
162
|
+
return this.unpack_map(size);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
Unpacker.prototype.unpack_uint8 = function(){
|
|
167
|
+
var byte = this.dataView[this.index] & 0xff;
|
|
168
|
+
this.index++;
|
|
169
|
+
return byte;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
Unpacker.prototype.unpack_uint16 = function(){
|
|
173
|
+
var bytes = this.read(2);
|
|
174
|
+
var uint16 =
|
|
175
|
+
((bytes[0] & 0xff) * 256) + (bytes[1] & 0xff);
|
|
176
|
+
this.index += 2;
|
|
177
|
+
return uint16;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
Unpacker.prototype.unpack_uint32 = function(){
|
|
181
|
+
var bytes = this.read(4);
|
|
182
|
+
var uint32 =
|
|
183
|
+
((bytes[0] * 256 +
|
|
184
|
+
bytes[1]) * 256 +
|
|
185
|
+
bytes[2]) * 256 +
|
|
186
|
+
bytes[3];
|
|
187
|
+
this.index += 4;
|
|
188
|
+
return uint32;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
Unpacker.prototype.unpack_uint64 = function(){
|
|
192
|
+
var bytes = this.read(8);
|
|
193
|
+
var uint64 =
|
|
194
|
+
((((((bytes[0] * 256 +
|
|
195
|
+
bytes[1]) * 256 +
|
|
196
|
+
bytes[2]) * 256 +
|
|
197
|
+
bytes[3]) * 256 +
|
|
198
|
+
bytes[4]) * 256 +
|
|
199
|
+
bytes[5]) * 256 +
|
|
200
|
+
bytes[6]) * 256 +
|
|
201
|
+
bytes[7];
|
|
202
|
+
this.index += 8;
|
|
203
|
+
return uint64;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
Unpacker.prototype.unpack_int8 = function(){
|
|
208
|
+
var uint8 = this.unpack_uint8();
|
|
209
|
+
return (uint8 < 0x80 ) ? uint8 : uint8 - (1 << 8);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
Unpacker.prototype.unpack_int16 = function(){
|
|
213
|
+
var uint16 = this.unpack_uint16();
|
|
214
|
+
return (uint16 < 0x8000 ) ? uint16 : uint16 - (1 << 16);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
Unpacker.prototype.unpack_int32 = function(){
|
|
218
|
+
var uint32 = this.unpack_uint32();
|
|
219
|
+
return (uint32 < Math.pow(2, 31) ) ? uint32 :
|
|
220
|
+
uint32 - Math.pow(2, 32);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
Unpacker.prototype.unpack_int64 = function(){
|
|
224
|
+
var uint64 = this.unpack_uint64();
|
|
225
|
+
return (uint64 < Math.pow(2, 63) ) ? uint64 :
|
|
226
|
+
uint64 - Math.pow(2, 64);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
Unpacker.prototype.unpack_raw = function(size){
|
|
230
|
+
if ( this.length < this.index + size){
|
|
231
|
+
throw new Error('BinaryPackFailure: index is out of range'
|
|
232
|
+
+ ' ' + this.index + ' ' + size + ' ' + this.length);
|
|
233
|
+
}
|
|
234
|
+
var buf = this.dataBuffer.slice(this.index, this.index + size);
|
|
235
|
+
this.index += size;
|
|
236
|
+
|
|
237
|
+
//buf = util.bufferToString(buf);
|
|
238
|
+
|
|
239
|
+
return buf;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
Unpacker.prototype.unpack_string = function(size){
|
|
243
|
+
var bytes = this.read(size);
|
|
244
|
+
var i = 0, str = '', c, code;
|
|
245
|
+
while(i < size){
|
|
246
|
+
c = bytes[i];
|
|
247
|
+
if ( c < 128){
|
|
248
|
+
str += String.fromCharCode(c);
|
|
249
|
+
i++;
|
|
250
|
+
} else if ((c ^ 0xc0) < 32){
|
|
251
|
+
code = ((c ^ 0xc0) << 6) | (bytes[i+1] & 63);
|
|
252
|
+
str += String.fromCharCode(code);
|
|
253
|
+
i += 2;
|
|
254
|
+
} else {
|
|
255
|
+
code = ((c & 15) << 12) | ((bytes[i+1] & 63) << 6) |
|
|
256
|
+
(bytes[i+2] & 63);
|
|
257
|
+
str += String.fromCharCode(code);
|
|
258
|
+
i += 3;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
this.index += size;
|
|
262
|
+
return str;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
Unpacker.prototype.unpack_array = function(size){
|
|
266
|
+
var objects = new Array(size);
|
|
267
|
+
for(var i = 0; i < size ; i++){
|
|
268
|
+
objects[i] = this.unpack();
|
|
269
|
+
}
|
|
270
|
+
return objects;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
Unpacker.prototype.unpack_map = function(size){
|
|
274
|
+
var map = {};
|
|
275
|
+
for(var i = 0; i < size ; i++){
|
|
276
|
+
var key = this.unpack();
|
|
277
|
+
var value = this.unpack();
|
|
278
|
+
map[key] = value;
|
|
279
|
+
}
|
|
280
|
+
return map;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
Unpacker.prototype.unpack_float = function(){
|
|
284
|
+
var uint32 = this.unpack_uint32();
|
|
285
|
+
var sign = uint32 >> 31;
|
|
286
|
+
var exp = ((uint32 >> 23) & 0xff) - 127;
|
|
287
|
+
var fraction = ( uint32 & 0x7fffff ) | 0x800000;
|
|
288
|
+
return (sign == 0 ? 1 : -1) *
|
|
289
|
+
fraction * Math.pow(2, exp - 23);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
Unpacker.prototype.unpack_double = function(){
|
|
293
|
+
var h32 = this.unpack_uint32();
|
|
294
|
+
var l32 = this.unpack_uint32();
|
|
295
|
+
var sign = h32 >> 31;
|
|
296
|
+
var exp = ((h32 >> 20) & 0x7ff) - 1023;
|
|
297
|
+
var hfrac = ( h32 & 0xfffff ) | 0x100000;
|
|
298
|
+
var frac = hfrac * Math.pow(2, exp - 20) +
|
|
299
|
+
l32 * Math.pow(2, exp - 52);
|
|
300
|
+
return (sign == 0 ? 1 : -1) * frac;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
Unpacker.prototype.read = function(length){
|
|
304
|
+
var j = this.index;
|
|
305
|
+
if (j + length <= this.length) {
|
|
306
|
+
return this.dataView.subarray(j, j + length);
|
|
307
|
+
} else {
|
|
308
|
+
throw new Error('BinaryPackFailure: read index out of range');
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function Packer(){
|
|
313
|
+
this.bufferBuilder = new BufferBuilder();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
Packer.prototype.getBuffer = function(){
|
|
317
|
+
return this.bufferBuilder.getBuffer();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
Packer.prototype.pack = function(value){
|
|
321
|
+
var type = typeof(value);
|
|
322
|
+
if (type == 'string'){
|
|
323
|
+
this.pack_string(value);
|
|
324
|
+
} else if (type == 'number'){
|
|
325
|
+
if (Math.floor(value) === value){
|
|
326
|
+
this.pack_integer(value);
|
|
327
|
+
} else{
|
|
328
|
+
this.pack_double(value);
|
|
329
|
+
}
|
|
330
|
+
} else if (type == 'boolean'){
|
|
331
|
+
if (value === true){
|
|
332
|
+
this.bufferBuilder.append(0xc3);
|
|
333
|
+
} else if (value === false){
|
|
334
|
+
this.bufferBuilder.append(0xc2);
|
|
335
|
+
}
|
|
336
|
+
} else if (type == 'undefined'){
|
|
337
|
+
this.bufferBuilder.append(0xc0);
|
|
338
|
+
} else if (type == 'object'){
|
|
339
|
+
if (value === null){
|
|
340
|
+
this.bufferBuilder.append(0xc0);
|
|
341
|
+
} else {
|
|
342
|
+
var constructor = value.constructor;
|
|
343
|
+
if (constructor == Array){
|
|
344
|
+
this.pack_array(value);
|
|
345
|
+
} else if (constructor == Blob || constructor == File) {
|
|
346
|
+
this.pack_bin(value);
|
|
347
|
+
} else if (constructor == ArrayBuffer) {
|
|
348
|
+
if(binaryFeatures.useArrayBufferView) {
|
|
349
|
+
this.pack_bin(new Uint8Array(value));
|
|
350
|
+
} else {
|
|
351
|
+
this.pack_bin(value);
|
|
352
|
+
}
|
|
353
|
+
} else if ('BYTES_PER_ELEMENT' in value){
|
|
354
|
+
if(binaryFeatures.useArrayBufferView) {
|
|
355
|
+
this.pack_bin(new Uint8Array(value.buffer));
|
|
356
|
+
} else {
|
|
357
|
+
this.pack_bin(value.buffer);
|
|
358
|
+
}
|
|
359
|
+
} else if (constructor == Object){
|
|
360
|
+
this.pack_object(value);
|
|
361
|
+
} else if (constructor == Date){
|
|
362
|
+
this.pack_string(value.toString());
|
|
363
|
+
} else if (typeof value.toBinaryPack == 'function'){
|
|
364
|
+
this.bufferBuilder.append(value.toBinaryPack());
|
|
365
|
+
} else {
|
|
366
|
+
throw new Error('Type "' + constructor.toString() + '" not yet supported');
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
} else {
|
|
370
|
+
throw new Error('Type "' + type + '" not yet supported');
|
|
371
|
+
}
|
|
372
|
+
this.bufferBuilder.flush();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
Packer.prototype.pack_bin = function(blob){
|
|
377
|
+
var length = blob.length || blob.byteLength || blob.size;
|
|
378
|
+
if (length <= 0x0f){
|
|
379
|
+
this.pack_uint8(0xa0 + length);
|
|
380
|
+
} else if (length <= 0xffff){
|
|
381
|
+
this.bufferBuilder.append(0xda) ;
|
|
382
|
+
this.pack_uint16(length);
|
|
383
|
+
} else if (length <= 0xffffffff){
|
|
384
|
+
this.bufferBuilder.append(0xdb);
|
|
385
|
+
this.pack_uint32(length);
|
|
386
|
+
} else{
|
|
387
|
+
throw new Error('Invalid length');
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
this.bufferBuilder.append(blob);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
Packer.prototype.pack_string = function(str){
|
|
394
|
+
var length = utf8Length(str);
|
|
395
|
+
|
|
396
|
+
if (length <= 0x0f){
|
|
397
|
+
this.pack_uint8(0xb0 + length);
|
|
398
|
+
} else if (length <= 0xffff){
|
|
399
|
+
this.bufferBuilder.append(0xd8) ;
|
|
400
|
+
this.pack_uint16(length);
|
|
401
|
+
} else if (length <= 0xffffffff){
|
|
402
|
+
this.bufferBuilder.append(0xd9);
|
|
403
|
+
this.pack_uint32(length);
|
|
404
|
+
} else{
|
|
405
|
+
throw new Error('Invalid length');
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
this.bufferBuilder.append(str);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
Packer.prototype.pack_array = function(ary){
|
|
412
|
+
var length = ary.length;
|
|
413
|
+
if (length <= 0x0f){
|
|
414
|
+
this.pack_uint8(0x90 + length);
|
|
415
|
+
} else if (length <= 0xffff){
|
|
416
|
+
this.bufferBuilder.append(0xdc)
|
|
417
|
+
this.pack_uint16(length);
|
|
418
|
+
} else if (length <= 0xffffffff){
|
|
419
|
+
this.bufferBuilder.append(0xdd);
|
|
420
|
+
this.pack_uint32(length);
|
|
421
|
+
} else{
|
|
422
|
+
throw new Error('Invalid length');
|
|
423
|
+
}
|
|
424
|
+
for(var i = 0; i < length ; i++){
|
|
425
|
+
this.pack(ary[i]);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
Packer.prototype.pack_integer = function(num){
|
|
430
|
+
if ( -0x20 <= num && num <= 0x7f){
|
|
431
|
+
this.bufferBuilder.append(num & 0xff);
|
|
432
|
+
} else if (0x00 <= num && num <= 0xff){
|
|
433
|
+
this.bufferBuilder.append(0xcc);
|
|
434
|
+
this.pack_uint8(num);
|
|
435
|
+
} else if (-0x80 <= num && num <= 0x7f){
|
|
436
|
+
this.bufferBuilder.append(0xd0);
|
|
437
|
+
this.pack_int8(num);
|
|
438
|
+
} else if ( 0x0000 <= num && num <= 0xffff){
|
|
439
|
+
this.bufferBuilder.append(0xcd);
|
|
440
|
+
this.pack_uint16(num);
|
|
441
|
+
} else if (-0x8000 <= num && num <= 0x7fff){
|
|
442
|
+
this.bufferBuilder.append(0xd1);
|
|
443
|
+
this.pack_int16(num);
|
|
444
|
+
} else if ( 0x00000000 <= num && num <= 0xffffffff){
|
|
445
|
+
this.bufferBuilder.append(0xce);
|
|
446
|
+
this.pack_uint32(num);
|
|
447
|
+
} else if (-0x80000000 <= num && num <= 0x7fffffff){
|
|
448
|
+
this.bufferBuilder.append(0xd2);
|
|
449
|
+
this.pack_int32(num);
|
|
450
|
+
} else if (-0x8000000000000000 <= num && num <= 0x7FFFFFFFFFFFFFFF){
|
|
451
|
+
this.bufferBuilder.append(0xd3);
|
|
452
|
+
this.pack_int64(num);
|
|
453
|
+
} else if (0x0000000000000000 <= num && num <= 0xFFFFFFFFFFFFFFFF){
|
|
454
|
+
this.bufferBuilder.append(0xcf);
|
|
455
|
+
this.pack_uint64(num);
|
|
456
|
+
} else{
|
|
457
|
+
throw new Error('Invalid integer');
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
Packer.prototype.pack_double = function(num){
|
|
462
|
+
var sign = 0;
|
|
463
|
+
if (num < 0){
|
|
464
|
+
sign = 1;
|
|
465
|
+
num = -num;
|
|
466
|
+
}
|
|
467
|
+
var exp = Math.floor(Math.log(num) / Math.LN2);
|
|
468
|
+
var frac0 = num / Math.pow(2, exp) - 1;
|
|
469
|
+
var frac1 = Math.floor(frac0 * Math.pow(2, 52));
|
|
470
|
+
var b32 = Math.pow(2, 32);
|
|
471
|
+
var h32 = (sign << 31) | ((exp+1023) << 20) |
|
|
472
|
+
(frac1 / b32) & 0x0fffff;
|
|
473
|
+
var l32 = frac1 % b32;
|
|
474
|
+
this.bufferBuilder.append(0xcb);
|
|
475
|
+
this.pack_int32(h32);
|
|
476
|
+
this.pack_int32(l32);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
Packer.prototype.pack_object = function(obj){
|
|
480
|
+
var keys = Object.keys(obj);
|
|
481
|
+
var length = keys.length;
|
|
482
|
+
if (length <= 0x0f){
|
|
483
|
+
this.pack_uint8(0x80 + length);
|
|
484
|
+
} else if (length <= 0xffff){
|
|
485
|
+
this.bufferBuilder.append(0xde);
|
|
486
|
+
this.pack_uint16(length);
|
|
487
|
+
} else if (length <= 0xffffffff){
|
|
488
|
+
this.bufferBuilder.append(0xdf);
|
|
489
|
+
this.pack_uint32(length);
|
|
490
|
+
} else{
|
|
491
|
+
throw new Error('Invalid length');
|
|
492
|
+
}
|
|
493
|
+
for(var prop in obj){
|
|
494
|
+
if (obj.hasOwnProperty(prop)){
|
|
495
|
+
this.pack(prop);
|
|
496
|
+
this.pack(obj[prop]);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
Packer.prototype.pack_uint8 = function(num){
|
|
502
|
+
this.bufferBuilder.append(num);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
Packer.prototype.pack_uint16 = function(num){
|
|
506
|
+
this.bufferBuilder.append(num >> 8);
|
|
507
|
+
this.bufferBuilder.append(num & 0xff);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
Packer.prototype.pack_uint32 = function(num){
|
|
511
|
+
var n = num & 0xffffffff;
|
|
512
|
+
this.bufferBuilder.append((n & 0xff000000) >>> 24);
|
|
513
|
+
this.bufferBuilder.append((n & 0x00ff0000) >>> 16);
|
|
514
|
+
this.bufferBuilder.append((n & 0x0000ff00) >>> 8);
|
|
515
|
+
this.bufferBuilder.append((n & 0x000000ff));
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
Packer.prototype.pack_uint64 = function(num){
|
|
519
|
+
var high = num / Math.pow(2, 32);
|
|
520
|
+
var low = num % Math.pow(2, 32);
|
|
521
|
+
this.bufferBuilder.append((high & 0xff000000) >>> 24);
|
|
522
|
+
this.bufferBuilder.append((high & 0x00ff0000) >>> 16);
|
|
523
|
+
this.bufferBuilder.append((high & 0x0000ff00) >>> 8);
|
|
524
|
+
this.bufferBuilder.append((high & 0x000000ff));
|
|
525
|
+
this.bufferBuilder.append((low & 0xff000000) >>> 24);
|
|
526
|
+
this.bufferBuilder.append((low & 0x00ff0000) >>> 16);
|
|
527
|
+
this.bufferBuilder.append((low & 0x0000ff00) >>> 8);
|
|
528
|
+
this.bufferBuilder.append((low & 0x000000ff));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
Packer.prototype.pack_int8 = function(num){
|
|
532
|
+
this.bufferBuilder.append(num & 0xff);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
Packer.prototype.pack_int16 = function(num){
|
|
536
|
+
this.bufferBuilder.append((num & 0xff00) >> 8);
|
|
537
|
+
this.bufferBuilder.append(num & 0xff);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
Packer.prototype.pack_int32 = function(num){
|
|
541
|
+
this.bufferBuilder.append((num >>> 24) & 0xff);
|
|
542
|
+
this.bufferBuilder.append((num & 0x00ff0000) >>> 16);
|
|
543
|
+
this.bufferBuilder.append((num & 0x0000ff00) >>> 8);
|
|
544
|
+
this.bufferBuilder.append((num & 0x000000ff));
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
Packer.prototype.pack_int64 = function(num){
|
|
548
|
+
var high = Math.floor(num / Math.pow(2, 32));
|
|
549
|
+
var low = num % Math.pow(2, 32);
|
|
550
|
+
this.bufferBuilder.append((high & 0xff000000) >>> 24);
|
|
551
|
+
this.bufferBuilder.append((high & 0x00ff0000) >>> 16);
|
|
552
|
+
this.bufferBuilder.append((high & 0x0000ff00) >>> 8);
|
|
553
|
+
this.bufferBuilder.append((high & 0x000000ff));
|
|
554
|
+
this.bufferBuilder.append((low & 0xff000000) >>> 24);
|
|
555
|
+
this.bufferBuilder.append((low & 0x00ff0000) >>> 16);
|
|
556
|
+
this.bufferBuilder.append((low & 0x0000ff00) >>> 8);
|
|
557
|
+
this.bufferBuilder.append((low & 0x000000ff));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function _utf8Replace(m){
|
|
561
|
+
var code = m.charCodeAt(0);
|
|
562
|
+
|
|
563
|
+
if(code <= 0x7ff) return '00';
|
|
564
|
+
if(code <= 0xffff) return '000';
|
|
565
|
+
if(code <= 0x1fffff) return '0000';
|
|
566
|
+
if(code <= 0x3ffffff) return '00000';
|
|
567
|
+
return '000000';
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function utf8Length(str){
|
|
571
|
+
if (str.length > 600) {
|
|
572
|
+
// Blob method faster for large strings
|
|
573
|
+
return (new Blob([str])).size;
|
|
574
|
+
} else {
|
|
575
|
+
return str.replace(/[^\u0000-\u007F]/g, _utf8Replace).length;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Light EventEmitter. Ported from Node.js/events.js
|
|
580
|
+
* Eric Zhang
|
|
581
|
+
*/
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* EventEmitter class
|
|
585
|
+
* Creates an object with event registering and firing methods
|
|
586
|
+
*/
|
|
587
|
+
function EventEmitter() {
|
|
588
|
+
// Initialise required storage variables
|
|
589
|
+
this._events = {};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
var isArray = Array.isArray;
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
EventEmitter.prototype.addListener = function(type, listener, scope, once) {
|
|
596
|
+
if ('function' !== typeof listener) {
|
|
597
|
+
throw new Error('addListener only takes instances of Function');
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// To avoid recursion in the case that type == "newListeners"! Before
|
|
601
|
+
// adding it to the listeners, first emit "newListeners".
|
|
602
|
+
this.emit('newListener', type, typeof listener.listener === 'function' ?
|
|
603
|
+
listener.listener : listener);
|
|
604
|
+
|
|
605
|
+
if (!this._events[type]) {
|
|
606
|
+
// Optimize the case of one listener. Don't need the extra array object.
|
|
607
|
+
this._events[type] = listener;
|
|
608
|
+
} else if (isArray(this._events[type])) {
|
|
609
|
+
|
|
610
|
+
// If we've already got an array, just append.
|
|
611
|
+
this._events[type].push(listener);
|
|
612
|
+
|
|
613
|
+
} else {
|
|
614
|
+
// Adding the second element, need to change to array.
|
|
615
|
+
this._events[type] = [this._events[type], listener];
|
|
616
|
+
}
|
|
617
|
+
return this;
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
|
|
621
|
+
|
|
622
|
+
EventEmitter.prototype.once = function(type, listener, scope) {
|
|
623
|
+
if ('function' !== typeof listener) {
|
|
624
|
+
throw new Error('.once only takes instances of Function');
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
var self = this;
|
|
628
|
+
function g() {
|
|
629
|
+
self.removeListener(type, g);
|
|
630
|
+
listener.apply(this, arguments);
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
g.listener = listener;
|
|
634
|
+
self.on(type, g);
|
|
635
|
+
|
|
636
|
+
return this;
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
EventEmitter.prototype.removeListener = function(type, listener, scope) {
|
|
640
|
+
if ('function' !== typeof listener) {
|
|
641
|
+
throw new Error('removeListener only takes instances of Function');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// does not use listeners(), so no side effect of creating _events[type]
|
|
645
|
+
if (!this._events[type]) return this;
|
|
646
|
+
|
|
647
|
+
var list = this._events[type];
|
|
648
|
+
|
|
649
|
+
if (isArray(list)) {
|
|
650
|
+
var position = -1;
|
|
651
|
+
for (var i = 0, length = list.length; i < length; i++) {
|
|
652
|
+
if (list[i] === listener ||
|
|
653
|
+
(list[i].listener && list[i].listener === listener))
|
|
654
|
+
{
|
|
655
|
+
position = i;
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (position < 0) return this;
|
|
661
|
+
list.splice(position, 1);
|
|
662
|
+
if (list.length == 0)
|
|
663
|
+
delete this._events[type];
|
|
664
|
+
} else if (list === listener ||
|
|
665
|
+
(list.listener && list.listener === listener))
|
|
666
|
+
{
|
|
667
|
+
delete this._events[type];
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return this;
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
EventEmitter.prototype.removeAllListeners = function(type) {
|
|
678
|
+
if (arguments.length === 0) {
|
|
679
|
+
this._events = {};
|
|
680
|
+
return this;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// does not use listeners(), so no side effect of creating _events[type]
|
|
684
|
+
if (type && this._events && this._events[type]) this._events[type] = null;
|
|
685
|
+
return this;
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
EventEmitter.prototype.listeners = function(type) {
|
|
689
|
+
if (!this._events[type]) this._events[type] = [];
|
|
690
|
+
if (!isArray(this._events[type])) {
|
|
691
|
+
this._events[type] = [this._events[type]];
|
|
692
|
+
}
|
|
693
|
+
return this._events[type];
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
EventEmitter.prototype.emit = function(type) {
|
|
697
|
+
var type = arguments[0];
|
|
698
|
+
var handler = this._events[type];
|
|
699
|
+
if (!handler) return false;
|
|
700
|
+
|
|
701
|
+
if (typeof handler == 'function') {
|
|
702
|
+
switch (arguments.length) {
|
|
703
|
+
// fast cases
|
|
704
|
+
case 1:
|
|
705
|
+
handler.call(this);
|
|
706
|
+
break;
|
|
707
|
+
case 2:
|
|
708
|
+
handler.call(this, arguments[1]);
|
|
709
|
+
break;
|
|
710
|
+
case 3:
|
|
711
|
+
handler.call(this, arguments[1], arguments[2]);
|
|
712
|
+
break;
|
|
713
|
+
// slower
|
|
714
|
+
default:
|
|
715
|
+
var l = arguments.length;
|
|
716
|
+
var args = new Array(l - 1);
|
|
717
|
+
for (var i = 1; i < l; i++) args[i - 1] = arguments[i];
|
|
718
|
+
handler.apply(this, args);
|
|
719
|
+
}
|
|
720
|
+
return true;
|
|
721
|
+
|
|
722
|
+
} else if (isArray(handler)) {
|
|
723
|
+
var l = arguments.length;
|
|
724
|
+
var args = new Array(l - 1);
|
|
725
|
+
for (var i = 1; i < l; i++) args[i - 1] = arguments[i];
|
|
726
|
+
|
|
727
|
+
var listeners = handler.slice();
|
|
728
|
+
for (var i = 0, l = listeners.length; i < l; i++) {
|
|
729
|
+
listeners[i].apply(this, args);
|
|
730
|
+
}
|
|
731
|
+
return true;
|
|
732
|
+
} else {
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Reliable transfer for Chrome Canary DataChannel impl.
|
|
741
|
+
* Author: @michellebu
|
|
742
|
+
*/
|
|
743
|
+
function Reliable(dc, debug) {
|
|
744
|
+
if (!(this instanceof Reliable)) return new Reliable(dc);
|
|
745
|
+
this._dc = dc;
|
|
746
|
+
|
|
747
|
+
util.debug = debug;
|
|
748
|
+
|
|
749
|
+
// Messages sent/received so far.
|
|
750
|
+
// id: { ack: n, chunks: [...] }
|
|
751
|
+
this._outgoing = {};
|
|
752
|
+
// id: { ack: ['ack', id, n], chunks: [...] }
|
|
753
|
+
this._incoming = {};
|
|
754
|
+
this._received = {};
|
|
755
|
+
|
|
756
|
+
// Window size.
|
|
757
|
+
this._window = 1000;
|
|
758
|
+
// MTU.
|
|
759
|
+
this._mtu = 500;
|
|
760
|
+
// Interval for setInterval. In ms.
|
|
761
|
+
this._interval = 0;
|
|
762
|
+
|
|
763
|
+
// Messages sent.
|
|
764
|
+
this._count = 0;
|
|
765
|
+
|
|
766
|
+
// Outgoing message queue.
|
|
767
|
+
this._queue = [];
|
|
768
|
+
|
|
769
|
+
this._setupDC();
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
// Send a message reliably.
|
|
773
|
+
Reliable.prototype.send = function(msg) {
|
|
774
|
+
// Determine if chunking is necessary.
|
|
775
|
+
var bl = util.pack(msg);
|
|
776
|
+
if (bl.size < this._mtu) {
|
|
777
|
+
this._handleSend(['no', bl]);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
this._outgoing[this._count] = {
|
|
782
|
+
ack: 0,
|
|
783
|
+
chunks: this._chunk(bl)
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
if (util.debug) {
|
|
787
|
+
this._outgoing[this._count].timer = new Date();
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Send prelim window.
|
|
791
|
+
this._sendWindowedChunks(this._count);
|
|
792
|
+
this._count += 1;
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
// Set up interval for processing queue.
|
|
796
|
+
Reliable.prototype._setupInterval = function() {
|
|
797
|
+
// TODO: fail gracefully.
|
|
798
|
+
|
|
799
|
+
var self = this;
|
|
800
|
+
this._timeout = setInterval(function() {
|
|
801
|
+
// FIXME: String stuff makes things terribly async.
|
|
802
|
+
var msg = self._queue.shift();
|
|
803
|
+
if (msg._multiple) {
|
|
804
|
+
for (var i = 0, ii = msg.length; i < ii; i += 1) {
|
|
805
|
+
self._intervalSend(msg[i]);
|
|
806
|
+
}
|
|
807
|
+
} else {
|
|
808
|
+
self._intervalSend(msg);
|
|
809
|
+
}
|
|
810
|
+
}, this._interval);
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
Reliable.prototype._intervalSend = function(msg) {
|
|
814
|
+
var self = this;
|
|
815
|
+
msg = util.pack(msg);
|
|
816
|
+
util.blobToBinaryString(msg, function(str) {
|
|
817
|
+
self._dc.send(str);
|
|
818
|
+
});
|
|
819
|
+
if (self._queue.length === 0) {
|
|
820
|
+
clearTimeout(self._timeout);
|
|
821
|
+
self._timeout = null;
|
|
822
|
+
//self._processAcks();
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
// Go through ACKs to send missing pieces.
|
|
827
|
+
Reliable.prototype._processAcks = function() {
|
|
828
|
+
for (var id in this._outgoing) {
|
|
829
|
+
if (this._outgoing.hasOwnProperty(id)) {
|
|
830
|
+
this._sendWindowedChunks(id);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
// Handle sending a message.
|
|
836
|
+
// FIXME: Don't wait for interval time for all messages...
|
|
837
|
+
Reliable.prototype._handleSend = function(msg) {
|
|
838
|
+
var push = true;
|
|
839
|
+
for (var i = 0, ii = this._queue.length; i < ii; i += 1) {
|
|
840
|
+
var item = this._queue[i];
|
|
841
|
+
if (item === msg) {
|
|
842
|
+
push = false;
|
|
843
|
+
} else if (item._multiple && item.indexOf(msg) !== -1) {
|
|
844
|
+
push = false;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (push) {
|
|
848
|
+
this._queue.push(msg);
|
|
849
|
+
if (!this._timeout) {
|
|
850
|
+
this._setupInterval();
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
// Set up DataChannel handlers.
|
|
856
|
+
Reliable.prototype._setupDC = function() {
|
|
857
|
+
// Handle various message types.
|
|
858
|
+
var self = this;
|
|
859
|
+
this._dc.onmessage = function(e) {
|
|
860
|
+
var msg = e.data;
|
|
861
|
+
var datatype = msg.constructor;
|
|
862
|
+
// FIXME: msg is String until binary is supported.
|
|
863
|
+
// Once that happens, this will have to be smarter.
|
|
864
|
+
if (datatype === String) {
|
|
865
|
+
var ab = util.binaryStringToArrayBuffer(msg);
|
|
866
|
+
msg = util.unpack(ab);
|
|
867
|
+
self._handleMessage(msg);
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
// Handles an incoming message.
|
|
873
|
+
Reliable.prototype._handleMessage = function(msg) {
|
|
874
|
+
var id = msg[1];
|
|
875
|
+
var idata = this._incoming[id];
|
|
876
|
+
var odata = this._outgoing[id];
|
|
877
|
+
var data;
|
|
878
|
+
switch (msg[0]) {
|
|
879
|
+
// No chunking was done.
|
|
880
|
+
case 'no':
|
|
881
|
+
var message = id;
|
|
882
|
+
if (!!message) {
|
|
883
|
+
this.onmessage(util.unpack(message));
|
|
884
|
+
}
|
|
885
|
+
break;
|
|
886
|
+
// Reached the end of the message.
|
|
887
|
+
case 'end':
|
|
888
|
+
data = idata;
|
|
889
|
+
|
|
890
|
+
// In case end comes first.
|
|
891
|
+
this._received[id] = msg[2];
|
|
892
|
+
|
|
893
|
+
if (!data) {
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
this._ack(id);
|
|
898
|
+
break;
|
|
899
|
+
case 'ack':
|
|
900
|
+
data = odata;
|
|
901
|
+
if (!!data) {
|
|
902
|
+
var ack = msg[2];
|
|
903
|
+
// Take the larger ACK, for out of order messages.
|
|
904
|
+
data.ack = Math.max(ack, data.ack);
|
|
905
|
+
|
|
906
|
+
// Clean up when all chunks are ACKed.
|
|
907
|
+
if (data.ack >= data.chunks.length) {
|
|
908
|
+
util.log('Time: ', new Date() - data.timer);
|
|
909
|
+
delete this._outgoing[id];
|
|
910
|
+
} else {
|
|
911
|
+
this._processAcks();
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
// If !data, just ignore.
|
|
915
|
+
break;
|
|
916
|
+
// Received a chunk of data.
|
|
917
|
+
case 'chunk':
|
|
918
|
+
// Create a new entry if none exists.
|
|
919
|
+
data = idata;
|
|
920
|
+
if (!data) {
|
|
921
|
+
var end = this._received[id];
|
|
922
|
+
if (end === true) {
|
|
923
|
+
break;
|
|
924
|
+
}
|
|
925
|
+
data = {
|
|
926
|
+
ack: ['ack', id, 0],
|
|
927
|
+
chunks: []
|
|
928
|
+
};
|
|
929
|
+
this._incoming[id] = data;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
var n = msg[2];
|
|
933
|
+
var chunk = msg[3];
|
|
934
|
+
data.chunks[n] = new Uint8Array(chunk);
|
|
935
|
+
|
|
936
|
+
// If we get the chunk we're looking for, ACK for next missing.
|
|
937
|
+
// Otherwise, ACK the same N again.
|
|
938
|
+
if (n === data.ack[2]) {
|
|
939
|
+
this._calculateNextAck(id);
|
|
940
|
+
}
|
|
941
|
+
this._ack(id);
|
|
942
|
+
break;
|
|
943
|
+
default:
|
|
944
|
+
// Shouldn't happen, but would make sense for message to just go
|
|
945
|
+
// through as is.
|
|
946
|
+
this._handleSend(msg);
|
|
947
|
+
break;
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
// Chunks BL into smaller messages.
|
|
952
|
+
Reliable.prototype._chunk = function(bl) {
|
|
953
|
+
var chunks = [];
|
|
954
|
+
var size = bl.size;
|
|
955
|
+
var start = 0;
|
|
956
|
+
while (start < size) {
|
|
957
|
+
var end = Math.min(size, start + this._mtu);
|
|
958
|
+
var b = bl.slice(start, end);
|
|
959
|
+
var chunk = {
|
|
960
|
+
payload: b
|
|
961
|
+
}
|
|
962
|
+
chunks.push(chunk);
|
|
963
|
+
start = end;
|
|
964
|
+
}
|
|
965
|
+
util.log('Created', chunks.length, 'chunks.');
|
|
966
|
+
return chunks;
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
// Sends ACK N, expecting Nth blob chunk for message ID.
|
|
970
|
+
Reliable.prototype._ack = function(id) {
|
|
971
|
+
var ack = this._incoming[id].ack;
|
|
972
|
+
|
|
973
|
+
// if ack is the end value, then call _complete.
|
|
974
|
+
if (this._received[id] === ack[2]) {
|
|
975
|
+
this._complete(id);
|
|
976
|
+
this._received[id] = true;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
this._handleSend(ack);
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
// Calculates the next ACK number, given chunks.
|
|
983
|
+
Reliable.prototype._calculateNextAck = function(id) {
|
|
984
|
+
var data = this._incoming[id];
|
|
985
|
+
var chunks = data.chunks;
|
|
986
|
+
for (var i = 0, ii = chunks.length; i < ii; i += 1) {
|
|
987
|
+
// This chunk is missing!!! Better ACK for it.
|
|
988
|
+
if (chunks[i] === undefined) {
|
|
989
|
+
data.ack[2] = i;
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
data.ack[2] = chunks.length;
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
// Sends the next window of chunks.
|
|
997
|
+
Reliable.prototype._sendWindowedChunks = function(id) {
|
|
998
|
+
util.log('sendWindowedChunks for: ', id);
|
|
999
|
+
var data = this._outgoing[id];
|
|
1000
|
+
var ch = data.chunks;
|
|
1001
|
+
var chunks = [];
|
|
1002
|
+
var limit = Math.min(data.ack + this._window, ch.length);
|
|
1003
|
+
for (var i = data.ack; i < limit; i += 1) {
|
|
1004
|
+
if (!ch[i].sent || i === data.ack) {
|
|
1005
|
+
ch[i].sent = true;
|
|
1006
|
+
chunks.push(['chunk', id, i, ch[i].payload]);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if (data.ack + this._window >= ch.length) {
|
|
1010
|
+
chunks.push(['end', id, ch.length])
|
|
1011
|
+
}
|
|
1012
|
+
chunks._multiple = true;
|
|
1013
|
+
this._handleSend(chunks);
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
// Puts together a message from chunks.
|
|
1017
|
+
Reliable.prototype._complete = function(id) {
|
|
1018
|
+
util.log('Completed called for', id);
|
|
1019
|
+
var self = this;
|
|
1020
|
+
var chunks = this._incoming[id].chunks;
|
|
1021
|
+
var bl = new Blob(chunks);
|
|
1022
|
+
util.blobToArrayBuffer(bl, function(ab) {
|
|
1023
|
+
self.onmessage(util.unpack(ab));
|
|
1024
|
+
});
|
|
1025
|
+
delete this._incoming[id];
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
// Ups bandwidth limit on SDP. Meant to be called during offer/answer.
|
|
1029
|
+
Reliable.higherBandwidthSDP = function(sdp) {
|
|
1030
|
+
// AS stands for Application-Specific Maximum.
|
|
1031
|
+
// Bandwidth number is in kilobits / sec.
|
|
1032
|
+
// See RFC for more info: http://www.ietf.org/rfc/rfc2327.txt
|
|
1033
|
+
|
|
1034
|
+
// Chrome 31+ doesn't want us munging the SDP, so we'll let them have their
|
|
1035
|
+
// way.
|
|
1036
|
+
var version = navigator.appVersion.match(/Chrome\/(.*?) /);
|
|
1037
|
+
if (version) {
|
|
1038
|
+
version = parseInt(version[1].split('.').shift());
|
|
1039
|
+
if (version < 31) {
|
|
1040
|
+
var parts = sdp.split('b=AS:30');
|
|
1041
|
+
var replace = 'b=AS:102400'; // 100 Mbps
|
|
1042
|
+
if (parts.length > 1) {
|
|
1043
|
+
return parts[0] + replace + parts[1];
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return sdp;
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
// Overwritten, typically.
|
|
1052
|
+
Reliable.prototype.onmessage = function(msg) {};
|
|
1053
|
+
|
|
1054
|
+
exports.Reliable = Reliable;
|
|
1055
|
+
exports.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription;
|
|
1056
|
+
exports.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
|
|
1057
|
+
exports.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate;
|
|
1058
|
+
var defaultConfig = {'iceServers': [{ 'url': 'stun:stun.l.google.com:19302' }]};
|
|
1059
|
+
var dataCount = 1;
|
|
1060
|
+
|
|
1061
|
+
var util = {
|
|
1062
|
+
noop: function() {},
|
|
1063
|
+
|
|
1064
|
+
CLOUD_HOST: '0.peerjs.com',
|
|
1065
|
+
CLOUD_PORT: 9000,
|
|
1066
|
+
|
|
1067
|
+
// Browsers that need chunking:
|
|
1068
|
+
chunkedBrowsers: {'Chrome': 1},
|
|
1069
|
+
chunkedMTU: 16300, // The original 60000 bytes setting does not work when sending data from Firefox to Chrome, which is "cut off" after 16384 bytes and delivered individually.
|
|
1070
|
+
|
|
1071
|
+
// Logging logic
|
|
1072
|
+
logLevel: 0,
|
|
1073
|
+
setLogLevel: function(level) {
|
|
1074
|
+
var debugLevel = parseInt(level, 10);
|
|
1075
|
+
if (!isNaN(parseInt(level, 10))) {
|
|
1076
|
+
util.logLevel = debugLevel;
|
|
1077
|
+
} else {
|
|
1078
|
+
// If they are using truthy/falsy values for debug
|
|
1079
|
+
util.logLevel = level ? 3 : 0;
|
|
1080
|
+
}
|
|
1081
|
+
util.log = util.warn = util.error = util.noop;
|
|
1082
|
+
if (util.logLevel > 0) {
|
|
1083
|
+
util.error = util._printWith('ERROR');
|
|
1084
|
+
}
|
|
1085
|
+
if (util.logLevel > 1) {
|
|
1086
|
+
util.warn = util._printWith('WARNING');
|
|
1087
|
+
}
|
|
1088
|
+
if (util.logLevel > 2) {
|
|
1089
|
+
util.log = util._print;
|
|
1090
|
+
}
|
|
1091
|
+
},
|
|
1092
|
+
setLogFunction: function(fn) {
|
|
1093
|
+
if (fn.constructor !== Function) {
|
|
1094
|
+
util.warn('The log function you passed in is not a function. Defaulting to regular logs.');
|
|
1095
|
+
} else {
|
|
1096
|
+
util._print = fn;
|
|
1097
|
+
}
|
|
1098
|
+
},
|
|
1099
|
+
|
|
1100
|
+
_printWith: function(prefix) {
|
|
1101
|
+
return function() {
|
|
1102
|
+
var copy = Array.prototype.slice.call(arguments);
|
|
1103
|
+
copy.unshift(prefix);
|
|
1104
|
+
util._print.apply(util, copy);
|
|
1105
|
+
};
|
|
1106
|
+
},
|
|
1107
|
+
_print: function () {
|
|
1108
|
+
var err = false;
|
|
1109
|
+
var copy = Array.prototype.slice.call(arguments);
|
|
1110
|
+
copy.unshift('PeerJS: ');
|
|
1111
|
+
for (var i = 0, l = copy.length; i < l; i++){
|
|
1112
|
+
if (copy[i] instanceof Error) {
|
|
1113
|
+
copy[i] = '(' + copy[i].name + ') ' + copy[i].message;
|
|
1114
|
+
err = true;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
err ? console.error.apply(console, copy) : console.log.apply(console, copy);
|
|
1118
|
+
},
|
|
1119
|
+
//
|
|
1120
|
+
|
|
1121
|
+
// Returns browser-agnostic default config
|
|
1122
|
+
defaultConfig: defaultConfig,
|
|
1123
|
+
//
|
|
1124
|
+
|
|
1125
|
+
// Returns the current browser.
|
|
1126
|
+
browser: (function() {
|
|
1127
|
+
if (window.mozRTCPeerConnection) {
|
|
1128
|
+
return 'Firefox';
|
|
1129
|
+
} else if (window.webkitRTCPeerConnection) {
|
|
1130
|
+
return 'Chrome';
|
|
1131
|
+
} else if (window.RTCPeerConnection) {
|
|
1132
|
+
return 'Supported';
|
|
1133
|
+
} else {
|
|
1134
|
+
return 'Unsupported';
|
|
1135
|
+
}
|
|
1136
|
+
})(),
|
|
1137
|
+
//
|
|
1138
|
+
|
|
1139
|
+
// Lists which features are supported
|
|
1140
|
+
supports: (function() {
|
|
1141
|
+
if (typeof RTCPeerConnection === 'undefined') {
|
|
1142
|
+
return {};
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
var data = true;
|
|
1146
|
+
var audioVideo = true;
|
|
1147
|
+
|
|
1148
|
+
var binaryBlob = false;
|
|
1149
|
+
var sctp = false;
|
|
1150
|
+
var onnegotiationneeded = !!window.webkitRTCPeerConnection;
|
|
1151
|
+
|
|
1152
|
+
var pc, dc;
|
|
1153
|
+
try {
|
|
1154
|
+
pc = new RTCPeerConnection(defaultConfig, {optional: [{RtpDataChannels: true}]});
|
|
1155
|
+
} catch (e) {
|
|
1156
|
+
data = false;
|
|
1157
|
+
audioVideo = false;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
if (data) {
|
|
1161
|
+
try {
|
|
1162
|
+
dc = pc.createDataChannel('_PEERJSTEST');
|
|
1163
|
+
} catch (e) {
|
|
1164
|
+
data = false;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
if (data) {
|
|
1169
|
+
// Binary test
|
|
1170
|
+
try {
|
|
1171
|
+
dc.binaryType = 'blob';
|
|
1172
|
+
binaryBlob = true;
|
|
1173
|
+
} catch (e) {
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Reliable test.
|
|
1177
|
+
// Unfortunately Chrome is a bit unreliable about whether or not they
|
|
1178
|
+
// support reliable.
|
|
1179
|
+
var reliablePC = new RTCPeerConnection(defaultConfig, {});
|
|
1180
|
+
try {
|
|
1181
|
+
var reliableDC = reliablePC.createDataChannel('_PEERJSRELIABLETEST', {});
|
|
1182
|
+
sctp = reliableDC.reliable;
|
|
1183
|
+
} catch (e) {
|
|
1184
|
+
}
|
|
1185
|
+
reliablePC.close();
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// FIXME: not really the best check...
|
|
1189
|
+
if (audioVideo) {
|
|
1190
|
+
audioVideo = !!pc.addStream;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// FIXME: this is not great because in theory it doesn't work for
|
|
1194
|
+
// av-only browsers (?).
|
|
1195
|
+
if (!onnegotiationneeded && data) {
|
|
1196
|
+
// sync default check.
|
|
1197
|
+
var negotiationPC = new RTCPeerConnection(defaultConfig, {optional: [{RtpDataChannels: true}]});
|
|
1198
|
+
negotiationPC.onnegotiationneeded = function() {
|
|
1199
|
+
onnegotiationneeded = true;
|
|
1200
|
+
// async check.
|
|
1201
|
+
if (util && util.supports) {
|
|
1202
|
+
util.supports.onnegotiationneeded = true;
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
var negotiationDC = negotiationPC.createDataChannel('_PEERJSNEGOTIATIONTEST');
|
|
1206
|
+
|
|
1207
|
+
setTimeout(function() {
|
|
1208
|
+
negotiationPC.close();
|
|
1209
|
+
}, 1000);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
if (pc) {
|
|
1213
|
+
pc.close();
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
return {
|
|
1217
|
+
audioVideo: audioVideo,
|
|
1218
|
+
data: data,
|
|
1219
|
+
binaryBlob: binaryBlob,
|
|
1220
|
+
binary: sctp, // deprecated; sctp implies binary support.
|
|
1221
|
+
reliable: sctp, // deprecated; sctp implies reliable data.
|
|
1222
|
+
sctp: sctp,
|
|
1223
|
+
onnegotiationneeded: onnegotiationneeded
|
|
1224
|
+
};
|
|
1225
|
+
}()),
|
|
1226
|
+
//
|
|
1227
|
+
|
|
1228
|
+
// Ensure alphanumeric ids
|
|
1229
|
+
validateId: function(id) {
|
|
1230
|
+
// Allow empty ids
|
|
1231
|
+
return !id || /^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.exec(id);
|
|
1232
|
+
},
|
|
1233
|
+
|
|
1234
|
+
validateKey: function(key) {
|
|
1235
|
+
// Allow empty keys
|
|
1236
|
+
return !key || /^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.exec(key);
|
|
1237
|
+
},
|
|
1238
|
+
|
|
1239
|
+
|
|
1240
|
+
debug: false,
|
|
1241
|
+
|
|
1242
|
+
inherits: function(ctor, superCtor) {
|
|
1243
|
+
ctor.super_ = superCtor;
|
|
1244
|
+
ctor.prototype = Object.create(superCtor.prototype, {
|
|
1245
|
+
constructor: {
|
|
1246
|
+
value: ctor,
|
|
1247
|
+
enumerable: false,
|
|
1248
|
+
writable: true,
|
|
1249
|
+
configurable: true
|
|
1250
|
+
}
|
|
1251
|
+
});
|
|
1252
|
+
},
|
|
1253
|
+
extend: function(dest, source) {
|
|
1254
|
+
for(var key in source) {
|
|
1255
|
+
if(source.hasOwnProperty(key)) {
|
|
1256
|
+
dest[key] = source[key];
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return dest;
|
|
1260
|
+
},
|
|
1261
|
+
pack: BinaryPack.pack,
|
|
1262
|
+
unpack: BinaryPack.unpack,
|
|
1263
|
+
|
|
1264
|
+
log: function () {
|
|
1265
|
+
if (util.debug) {
|
|
1266
|
+
var err = false;
|
|
1267
|
+
var copy = Array.prototype.slice.call(arguments);
|
|
1268
|
+
copy.unshift('PeerJS: ');
|
|
1269
|
+
for (var i = 0, l = copy.length; i < l; i++){
|
|
1270
|
+
if (copy[i] instanceof Error) {
|
|
1271
|
+
copy[i] = '(' + copy[i].name + ') ' + copy[i].message;
|
|
1272
|
+
err = true;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
err ? console.error.apply(console, copy) : console.log.apply(console, copy);
|
|
1276
|
+
}
|
|
1277
|
+
},
|
|
1278
|
+
|
|
1279
|
+
setZeroTimeout: (function(global) {
|
|
1280
|
+
var timeouts = [];
|
|
1281
|
+
var messageName = 'zero-timeout-message';
|
|
1282
|
+
|
|
1283
|
+
// Like setTimeout, but only takes a function argument. There's
|
|
1284
|
+
// no time argument (always zero) and no arguments (you have to
|
|
1285
|
+
// use a closure).
|
|
1286
|
+
function setZeroTimeoutPostMessage(fn) {
|
|
1287
|
+
timeouts.push(fn);
|
|
1288
|
+
global.postMessage(messageName, '*');
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function handleMessage(event) {
|
|
1292
|
+
if (event.source == global && event.data == messageName) {
|
|
1293
|
+
if (event.stopPropagation) {
|
|
1294
|
+
event.stopPropagation();
|
|
1295
|
+
}
|
|
1296
|
+
if (timeouts.length) {
|
|
1297
|
+
timeouts.shift()();
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
if (global.addEventListener) {
|
|
1302
|
+
global.addEventListener('message', handleMessage, true);
|
|
1303
|
+
} else if (global.attachEvent) {
|
|
1304
|
+
global.attachEvent('onmessage', handleMessage);
|
|
1305
|
+
}
|
|
1306
|
+
return setZeroTimeoutPostMessage;
|
|
1307
|
+
}(this)),
|
|
1308
|
+
|
|
1309
|
+
// Binary stuff
|
|
1310
|
+
|
|
1311
|
+
// chunks a blob.
|
|
1312
|
+
chunk: function(bl) {
|
|
1313
|
+
var chunks = [];
|
|
1314
|
+
var size = bl.size;
|
|
1315
|
+
var start = index = 0;
|
|
1316
|
+
var total = Math.ceil(size / util.chunkedMTU);
|
|
1317
|
+
while (start < size) {
|
|
1318
|
+
var end = Math.min(size, start + util.chunkedMTU);
|
|
1319
|
+
var b = bl.slice(start, end);
|
|
1320
|
+
|
|
1321
|
+
var chunk = {
|
|
1322
|
+
__peerData: dataCount,
|
|
1323
|
+
n: index,
|
|
1324
|
+
data: b,
|
|
1325
|
+
total: total
|
|
1326
|
+
};
|
|
1327
|
+
|
|
1328
|
+
chunks.push(chunk);
|
|
1329
|
+
|
|
1330
|
+
start = end;
|
|
1331
|
+
index += 1;
|
|
1332
|
+
}
|
|
1333
|
+
dataCount += 1;
|
|
1334
|
+
return chunks;
|
|
1335
|
+
},
|
|
1336
|
+
|
|
1337
|
+
blobToArrayBuffer: function(blob, cb){
|
|
1338
|
+
var fr = new FileReader();
|
|
1339
|
+
fr.onload = function(evt) {
|
|
1340
|
+
cb(evt.target.result);
|
|
1341
|
+
};
|
|
1342
|
+
fr.readAsArrayBuffer(blob);
|
|
1343
|
+
},
|
|
1344
|
+
blobToBinaryString: function(blob, cb){
|
|
1345
|
+
var fr = new FileReader();
|
|
1346
|
+
fr.onload = function(evt) {
|
|
1347
|
+
cb(evt.target.result);
|
|
1348
|
+
};
|
|
1349
|
+
fr.readAsBinaryString(blob);
|
|
1350
|
+
},
|
|
1351
|
+
binaryStringToArrayBuffer: function(binary) {
|
|
1352
|
+
var byteArray = new Uint8Array(binary.length);
|
|
1353
|
+
for (var i = 0; i < binary.length; i++) {
|
|
1354
|
+
byteArray[i] = binary.charCodeAt(i) & 0xff;
|
|
1355
|
+
}
|
|
1356
|
+
return byteArray.buffer;
|
|
1357
|
+
},
|
|
1358
|
+
randomToken: function () {
|
|
1359
|
+
return Math.random().toString(36).substr(2);
|
|
1360
|
+
},
|
|
1361
|
+
//
|
|
1362
|
+
|
|
1363
|
+
isSecure: function() {
|
|
1364
|
+
return location.protocol === 'https:';
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
|
|
1368
|
+
exports.util = util;
|
|
1369
|
+
/**
|
|
1370
|
+
* A peer who can initiate connections with other peers.
|
|
1371
|
+
*/
|
|
1372
|
+
function Peer(id, options) {
|
|
1373
|
+
if (!(this instanceof Peer)) return new Peer(id, options);
|
|
1374
|
+
EventEmitter.call(this);
|
|
1375
|
+
|
|
1376
|
+
// Deal with overloading
|
|
1377
|
+
if (id && id.constructor == Object) {
|
|
1378
|
+
options = id;
|
|
1379
|
+
id = undefined;
|
|
1380
|
+
} else if (id) {
|
|
1381
|
+
// Ensure id is a string
|
|
1382
|
+
id = id.toString();
|
|
1383
|
+
}
|
|
1384
|
+
//
|
|
1385
|
+
|
|
1386
|
+
// Configurize options
|
|
1387
|
+
options = util.extend({
|
|
1388
|
+
debug: 0, // 1: Errors, 2: Warnings, 3: All logs
|
|
1389
|
+
host: util.CLOUD_HOST,
|
|
1390
|
+
port: util.CLOUD_PORT,
|
|
1391
|
+
key: 'peerjs',
|
|
1392
|
+
path: '/',
|
|
1393
|
+
token: util.randomToken(),
|
|
1394
|
+
config: util.defaultConfig
|
|
1395
|
+
}, options);
|
|
1396
|
+
this.options = options;
|
|
1397
|
+
// Detect relative URL host.
|
|
1398
|
+
if (options.host === '/') {
|
|
1399
|
+
options.host = window.location.hostname;
|
|
1400
|
+
}
|
|
1401
|
+
// Set path correctly.
|
|
1402
|
+
if (options.path[0] !== '/') {
|
|
1403
|
+
options.path = '/' + options.path;
|
|
1404
|
+
}
|
|
1405
|
+
if (options.path[options.path.length - 1] !== '/') {
|
|
1406
|
+
options.path += '/';
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// Set whether we use SSL to same as current host
|
|
1410
|
+
if (options.secure === undefined && options.host !== util.CLOUD_HOST) {
|
|
1411
|
+
options.secure = util.isSecure();
|
|
1412
|
+
}
|
|
1413
|
+
// Set a custom log function if present
|
|
1414
|
+
if (options.logFunction) {
|
|
1415
|
+
util.setLogFunction(options.logFunction);
|
|
1416
|
+
}
|
|
1417
|
+
util.setLogLevel(options.debug);
|
|
1418
|
+
//
|
|
1419
|
+
|
|
1420
|
+
// Sanity checks
|
|
1421
|
+
// Ensure WebRTC supported
|
|
1422
|
+
if (!util.supports.audioVideo && !util.supports.data ) {
|
|
1423
|
+
this._delayedAbort('browser-incompatible', 'The current browser does not support WebRTC');
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
// Ensure alphanumeric id
|
|
1427
|
+
if (!util.validateId(id)) {
|
|
1428
|
+
this._delayedAbort('invalid-id', 'ID "' + id + '" is invalid');
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
// Ensure valid key
|
|
1432
|
+
if (!util.validateKey(options.key)) {
|
|
1433
|
+
this._delayedAbort('invalid-key', 'API KEY "' + options.key + '" is invalid');
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
// Ensure not using unsecure cloud server on SSL page
|
|
1437
|
+
if (options.secure && options.host === '0.peerjs.com') {
|
|
1438
|
+
this._delayedAbort('ssl-unavailable',
|
|
1439
|
+
'The cloud server currently does not support HTTPS. Please run your own PeerServer to use HTTPS.');
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
//
|
|
1443
|
+
|
|
1444
|
+
// States.
|
|
1445
|
+
this.destroyed = false; // Connections have been killed
|
|
1446
|
+
this.disconnected = false; // Connection to PeerServer killed but P2P connections still active
|
|
1447
|
+
this.open = false; // Sockets and such are not yet open.
|
|
1448
|
+
//
|
|
1449
|
+
|
|
1450
|
+
// References
|
|
1451
|
+
this.connections = {}; // DataConnections for this peer.
|
|
1452
|
+
this._lostMessages = {}; // src => [list of messages]
|
|
1453
|
+
//
|
|
1454
|
+
|
|
1455
|
+
// Start the server connection
|
|
1456
|
+
this._initializeServerConnection();
|
|
1457
|
+
if (id) {
|
|
1458
|
+
this._initialize(id);
|
|
1459
|
+
} else {
|
|
1460
|
+
this._retrieveId();
|
|
1461
|
+
}
|
|
1462
|
+
//
|
|
1463
|
+
};
|
|
1464
|
+
|
|
1465
|
+
util.inherits(Peer, EventEmitter);
|
|
1466
|
+
|
|
1467
|
+
// Initialize the 'socket' (which is actually a mix of XHR streaming and
|
|
1468
|
+
// websockets.)
|
|
1469
|
+
Peer.prototype._initializeServerConnection = function() {
|
|
1470
|
+
var self = this;
|
|
1471
|
+
this.socket = new Socket(this.options.secure, this.options.host, this.options.port, this.options.path, this.options.key);
|
|
1472
|
+
this.socket.on('message', function(data) {
|
|
1473
|
+
self._handleMessage(data);
|
|
1474
|
+
});
|
|
1475
|
+
this.socket.on('error', function(error) {
|
|
1476
|
+
self._abort('socket-error', error);
|
|
1477
|
+
});
|
|
1478
|
+
this.socket.on('disconnected', function() {
|
|
1479
|
+
// If we haven't explicitly disconnected, emit error and disconnect.
|
|
1480
|
+
if (!self.disconnected) {
|
|
1481
|
+
self.emitError('network', 'Lost connection to server.')
|
|
1482
|
+
self.disconnect();
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
this.socket.on('close', function() {
|
|
1486
|
+
// If we haven't explicitly disconnected, emit error.
|
|
1487
|
+
if (!self.disconnected) {
|
|
1488
|
+
self._abort('socket-closed', 'Underlying socket is already closed.');
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
};
|
|
1492
|
+
|
|
1493
|
+
/** Get a unique ID from the server via XHR. */
|
|
1494
|
+
Peer.prototype._retrieveId = function(cb) {
|
|
1495
|
+
var self = this;
|
|
1496
|
+
var http = new XMLHttpRequest();
|
|
1497
|
+
var protocol = this.options.secure ? 'https://' : 'http://';
|
|
1498
|
+
var url = protocol + this.options.host + ':' + this.options.port
|
|
1499
|
+
+ this.options.path + this.options.key + '/id';
|
|
1500
|
+
var queryString = '?ts=' + new Date().getTime() + '' + Math.random();
|
|
1501
|
+
url += queryString;
|
|
1502
|
+
|
|
1503
|
+
// If there's no ID we need to wait for one before trying to init socket.
|
|
1504
|
+
http.open('get', url, true);
|
|
1505
|
+
http.onerror = function(e) {
|
|
1506
|
+
util.error('Error retrieving ID', e);
|
|
1507
|
+
var pathError = '';
|
|
1508
|
+
if (self.options.path === '/' && self.options.host !== util.CLOUD_HOST) {
|
|
1509
|
+
pathError = ' If you passed in a `path` to your self-hosted PeerServer, '
|
|
1510
|
+
+ 'you\'ll also need to pass in that same path when creating a new'
|
|
1511
|
+
+ ' Peer.';
|
|
1512
|
+
}
|
|
1513
|
+
self._abort('server-error', 'Could not get an ID from the server.' + pathError);
|
|
1514
|
+
}
|
|
1515
|
+
http.onreadystatechange = function() {
|
|
1516
|
+
if (http.readyState !== 4) {
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
if (http.status !== 200) {
|
|
1520
|
+
http.onerror();
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
self._initialize(http.responseText);
|
|
1524
|
+
};
|
|
1525
|
+
http.send(null);
|
|
1526
|
+
};
|
|
1527
|
+
|
|
1528
|
+
/** Initialize a connection with the server. */
|
|
1529
|
+
Peer.prototype._initialize = function(id) {
|
|
1530
|
+
this.id = id;
|
|
1531
|
+
this.socket.start(this.id, this.options.token);
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
/** Handles messages from the server. */
|
|
1535
|
+
Peer.prototype._handleMessage = function(message) {
|
|
1536
|
+
var type = message.type;
|
|
1537
|
+
var payload = message.payload;
|
|
1538
|
+
var peer = message.src;
|
|
1539
|
+
|
|
1540
|
+
switch (type) {
|
|
1541
|
+
case 'OPEN': // The connection to the server is open.
|
|
1542
|
+
this.emit('open', this.id);
|
|
1543
|
+
this.open = true;
|
|
1544
|
+
break;
|
|
1545
|
+
case 'ERROR': // Server error.
|
|
1546
|
+
this._abort('server-error', payload.msg);
|
|
1547
|
+
break;
|
|
1548
|
+
case 'ID-TAKEN': // The selected ID is taken.
|
|
1549
|
+
this._abort('unavailable-id', 'ID `' + this.id + '` is taken');
|
|
1550
|
+
break;
|
|
1551
|
+
case 'INVALID-KEY': // The given API key cannot be found.
|
|
1552
|
+
this._abort('invalid-key', 'API KEY "' + this.options.key + '" is invalid');
|
|
1553
|
+
break;
|
|
1554
|
+
|
|
1555
|
+
//
|
|
1556
|
+
case 'LEAVE': // Another peer has closed its connection to this peer.
|
|
1557
|
+
util.log('Received leave message from', peer);
|
|
1558
|
+
this._cleanupPeer(peer);
|
|
1559
|
+
break;
|
|
1560
|
+
|
|
1561
|
+
case 'EXPIRE': // The offer sent to a peer has expired without response.
|
|
1562
|
+
this.emitError('peer-unavailable', 'Could not connect to peer ' + peer);
|
|
1563
|
+
break;
|
|
1564
|
+
case 'OFFER': // we should consider switching this to CALL/CONNECT, but this is the least breaking option.
|
|
1565
|
+
var connectionId = payload.connectionId;
|
|
1566
|
+
var connection = this.getConnection(peer, connectionId);
|
|
1567
|
+
|
|
1568
|
+
if (connection) {
|
|
1569
|
+
util.warn('Offer received for existing Connection ID:', connectionId);
|
|
1570
|
+
//connection.handleMessage(message);
|
|
1571
|
+
} else {
|
|
1572
|
+
// Create a new connection.
|
|
1573
|
+
if (payload.type === 'media') {
|
|
1574
|
+
var connection = new MediaConnection(peer, this, {
|
|
1575
|
+
connectionId: connectionId,
|
|
1576
|
+
_payload: payload,
|
|
1577
|
+
metadata: payload.metadata
|
|
1578
|
+
});
|
|
1579
|
+
this._addConnection(peer, connection);
|
|
1580
|
+
this.emit('call', connection);
|
|
1581
|
+
} else if (payload.type === 'data') {
|
|
1582
|
+
connection = new DataConnection(peer, this, {
|
|
1583
|
+
connectionId: connectionId,
|
|
1584
|
+
_payload: payload,
|
|
1585
|
+
metadata: payload.metadata,
|
|
1586
|
+
label: payload.label,
|
|
1587
|
+
serialization: payload.serialization,
|
|
1588
|
+
reliable: payload.reliable
|
|
1589
|
+
});
|
|
1590
|
+
this._addConnection(peer, connection);
|
|
1591
|
+
this.emit('connection', connection);
|
|
1592
|
+
} else {
|
|
1593
|
+
util.warn('Received malformed connection type:', payload.type);
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
// Find messages.
|
|
1597
|
+
var messages = this._getMessages(connectionId);
|
|
1598
|
+
for (var i = 0, ii = messages.length; i < ii; i += 1) {
|
|
1599
|
+
connection.handleMessage(messages[i]);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
break;
|
|
1603
|
+
default:
|
|
1604
|
+
if (!payload) {
|
|
1605
|
+
util.warn('You received a malformed message from ' + peer + ' of type ' + type);
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
var id = payload.connectionId;
|
|
1610
|
+
var connection = this.getConnection(peer, id);
|
|
1611
|
+
|
|
1612
|
+
if (connection && connection.pc) {
|
|
1613
|
+
// Pass it on.
|
|
1614
|
+
connection.handleMessage(message);
|
|
1615
|
+
} else if (id) {
|
|
1616
|
+
// Store for possible later use
|
|
1617
|
+
this._storeMessage(id, message);
|
|
1618
|
+
} else {
|
|
1619
|
+
util.warn('You received an unrecognized message:', message);
|
|
1620
|
+
}
|
|
1621
|
+
break;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
/** Stores messages without a set up connection, to be claimed later. */
|
|
1626
|
+
Peer.prototype._storeMessage = function(connectionId, message) {
|
|
1627
|
+
if (!this._lostMessages[connectionId]) {
|
|
1628
|
+
this._lostMessages[connectionId] = [];
|
|
1629
|
+
}
|
|
1630
|
+
this._lostMessages[connectionId].push(message);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
/** Retrieve messages from lost message store */
|
|
1634
|
+
Peer.prototype._getMessages = function(connectionId) {
|
|
1635
|
+
var messages = this._lostMessages[connectionId];
|
|
1636
|
+
if (messages) {
|
|
1637
|
+
delete this._lostMessages[connectionId];
|
|
1638
|
+
return messages;
|
|
1639
|
+
} else {
|
|
1640
|
+
return [];
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
/**
|
|
1645
|
+
* Returns a DataConnection to the specified peer. See documentation for a
|
|
1646
|
+
* complete list of options.
|
|
1647
|
+
*/
|
|
1648
|
+
Peer.prototype.connect = function(peer, options) {
|
|
1649
|
+
if (this.disconnected) {
|
|
1650
|
+
util.warn('You cannot connect to a new Peer because you called '
|
|
1651
|
+
+ '.disconnect() on this Peer and ended your connection with the'
|
|
1652
|
+
+ ' server. You can create a new Peer to reconnect, or call reconnect'
|
|
1653
|
+
+ ' on this peer if you believe its ID to still be available.');
|
|
1654
|
+
this.emitError('disconnected', 'Cannot connect to new Peer after disconnecting from server.');
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
var connection = new DataConnection(peer, this, options);
|
|
1658
|
+
this._addConnection(peer, connection);
|
|
1659
|
+
return connection;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
/**
|
|
1663
|
+
* Returns a MediaConnection to the specified peer. See documentation for a
|
|
1664
|
+
* complete list of options.
|
|
1665
|
+
*/
|
|
1666
|
+
Peer.prototype.call = function(peer, stream, options) {
|
|
1667
|
+
if (this.disconnected) {
|
|
1668
|
+
util.warn('You cannot connect to a new Peer because you called '
|
|
1669
|
+
+ '.disconnect() on this Peer and ended your connection with the'
|
|
1670
|
+
+ ' server. You can create a new Peer to reconnect.');
|
|
1671
|
+
this.emitError('disconnected', 'Cannot connect to new Peer after disconnecting from server.');
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
if (!stream) {
|
|
1675
|
+
util.error('To call a peer, you must provide a stream from your browser\'s `getUserMedia`.');
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
options = options || {};
|
|
1679
|
+
options._stream = stream;
|
|
1680
|
+
var call = new MediaConnection(peer, this, options);
|
|
1681
|
+
this._addConnection(peer, call);
|
|
1682
|
+
return call;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
/** Add a data/media connection to this peer. */
|
|
1686
|
+
Peer.prototype._addConnection = function(peer, connection) {
|
|
1687
|
+
if (!this.connections[peer]) {
|
|
1688
|
+
this.connections[peer] = [];
|
|
1689
|
+
}
|
|
1690
|
+
this.connections[peer].push(connection);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
/** Retrieve a data/media connection for this peer. */
|
|
1694
|
+
Peer.prototype.getConnection = function(peer, id) {
|
|
1695
|
+
var connections = this.connections[peer];
|
|
1696
|
+
if (!connections) {
|
|
1697
|
+
return null;
|
|
1698
|
+
}
|
|
1699
|
+
for (var i = 0, ii = connections.length; i < ii; i++) {
|
|
1700
|
+
if (connections[i].id === id) {
|
|
1701
|
+
return connections[i];
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
return null;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
Peer.prototype._delayedAbort = function(type, message) {
|
|
1708
|
+
var self = this;
|
|
1709
|
+
util.setZeroTimeout(function(){
|
|
1710
|
+
self._abort(type, message);
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
/**
|
|
1715
|
+
* Destroys the Peer and emits an error message.
|
|
1716
|
+
* The Peer is not destroyed if it's in a disconnected state, in which case
|
|
1717
|
+
* it retains its disconnected state and its existing connections.
|
|
1718
|
+
*/
|
|
1719
|
+
Peer.prototype._abort = function(type, message) {
|
|
1720
|
+
util.error('Aborting!');
|
|
1721
|
+
if (!this._lastServerId) {
|
|
1722
|
+
this.destroy();
|
|
1723
|
+
} else {
|
|
1724
|
+
this.disconnect();
|
|
1725
|
+
}
|
|
1726
|
+
this.emitError(type, message);
|
|
1727
|
+
};
|
|
1728
|
+
|
|
1729
|
+
/** Emits a typed error message. */
|
|
1730
|
+
Peer.prototype.emitError = function(type, err) {
|
|
1731
|
+
util.error('Error:', err);
|
|
1732
|
+
if (typeof err === 'string') {
|
|
1733
|
+
err = new Error(err);
|
|
1734
|
+
}
|
|
1735
|
+
err.type = type;
|
|
1736
|
+
this.emit('error', err);
|
|
1737
|
+
};
|
|
1738
|
+
|
|
1739
|
+
/**
|
|
1740
|
+
* Destroys the Peer: closes all active connections as well as the connection
|
|
1741
|
+
* to the server.
|
|
1742
|
+
* Warning: The peer can no longer create or accept connections after being
|
|
1743
|
+
* destroyed.
|
|
1744
|
+
*/
|
|
1745
|
+
Peer.prototype.destroy = function() {
|
|
1746
|
+
if (!this.destroyed) {
|
|
1747
|
+
this._cleanup();
|
|
1748
|
+
this.disconnect();
|
|
1749
|
+
this.destroyed = true;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
|
|
1754
|
+
/** Disconnects every connection on this peer. */
|
|
1755
|
+
Peer.prototype._cleanup = function() {
|
|
1756
|
+
if (this.connections) {
|
|
1757
|
+
var peers = Object.keys(this.connections);
|
|
1758
|
+
for (var i = 0, ii = peers.length; i < ii; i++) {
|
|
1759
|
+
this._cleanupPeer(peers[i]);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
this.emit('close');
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
/** Closes all connections to this peer. */
|
|
1766
|
+
Peer.prototype._cleanupPeer = function(peer) {
|
|
1767
|
+
var connections = this.connections[peer];
|
|
1768
|
+
for (var j = 0, jj = connections.length; j < jj; j += 1) {
|
|
1769
|
+
connections[j].close();
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
/**
|
|
1774
|
+
* Disconnects the Peer's connection to the PeerServer. Does not close any
|
|
1775
|
+
* active connections.
|
|
1776
|
+
* Warning: The peer can no longer create or accept connections after being
|
|
1777
|
+
* disconnected. It also cannot reconnect to the server.
|
|
1778
|
+
*/
|
|
1779
|
+
Peer.prototype.disconnect = function() {
|
|
1780
|
+
var self = this;
|
|
1781
|
+
util.setZeroTimeout(function(){
|
|
1782
|
+
if (!self.disconnected) {
|
|
1783
|
+
self.disconnected = true;
|
|
1784
|
+
self.open = false;
|
|
1785
|
+
if (self.socket) {
|
|
1786
|
+
self.socket.close();
|
|
1787
|
+
}
|
|
1788
|
+
self.emit('disconnected', self.id);
|
|
1789
|
+
self._lastServerId = self.id;
|
|
1790
|
+
self.id = null;
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
/** Attempts to reconnect with the same ID. */
|
|
1796
|
+
Peer.prototype.reconnect = function() {
|
|
1797
|
+
if (this.disconnected && !this.destroyed) {
|
|
1798
|
+
util.log('Attempting reconnection to server with ID ' + this._lastServerId);
|
|
1799
|
+
this.disconnected = false;
|
|
1800
|
+
this._initializeServerConnection();
|
|
1801
|
+
this._initialize(this._lastServerId);
|
|
1802
|
+
} else if (this.destroyed) {
|
|
1803
|
+
throw new Error('This peer cannot reconnect to the server. It has already been destroyed.');
|
|
1804
|
+
} else if (!this.disconnected && !this.open) {
|
|
1805
|
+
// Do nothing. We're still connecting the first time.
|
|
1806
|
+
util.error('In a hurry? We\'re still trying to make the initial connection!');
|
|
1807
|
+
} else {
|
|
1808
|
+
throw new Error('Peer ' + this.id + ' cannot reconnect because it is not disconnected from the server!');
|
|
1809
|
+
}
|
|
1810
|
+
};
|
|
1811
|
+
|
|
1812
|
+
/**
|
|
1813
|
+
* Get a list of available peer IDs. If you're running your own server, you'll
|
|
1814
|
+
* want to set allow_discovery: true in the PeerServer options. If you're using
|
|
1815
|
+
* the cloud server, email team@peerjs.com to get the functionality enabled for
|
|
1816
|
+
* your key.
|
|
1817
|
+
*/
|
|
1818
|
+
Peer.prototype.listAllPeers = function(cb) {
|
|
1819
|
+
cb = cb || function() {};
|
|
1820
|
+
var self = this;
|
|
1821
|
+
var http = new XMLHttpRequest();
|
|
1822
|
+
var protocol = this.options.secure ? 'https://' : 'http://';
|
|
1823
|
+
var url = protocol + this.options.host + ':' + this.options.port
|
|
1824
|
+
+ this.options.path + this.options.key + '/peers';
|
|
1825
|
+
var queryString = '?ts=' + new Date().getTime() + '' + Math.random();
|
|
1826
|
+
url += queryString;
|
|
1827
|
+
|
|
1828
|
+
// If there's no ID we need to wait for one before trying to init socket.
|
|
1829
|
+
http.open('get', url, true);
|
|
1830
|
+
http.onerror = function(e) {
|
|
1831
|
+
self._abort('server-error', 'Could not get peers from the server.');
|
|
1832
|
+
cb([]);
|
|
1833
|
+
}
|
|
1834
|
+
http.onreadystatechange = function() {
|
|
1835
|
+
if (http.readyState !== 4) {
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
if (http.status === 401) {
|
|
1839
|
+
var helpfulError = '';
|
|
1840
|
+
if (self.options.host !== util.CLOUD_HOST) {
|
|
1841
|
+
helpfulError = 'It looks like you\'re using the cloud server. You can email '
|
|
1842
|
+
+ 'team@peerjs.com to enable peer listing for your API key.';
|
|
1843
|
+
} else {
|
|
1844
|
+
helpfulError = 'You need to enable `allow_discovery` on your self-hosted'
|
|
1845
|
+
+ ' PeerServer to use this feature.';
|
|
1846
|
+
}
|
|
1847
|
+
throw new Error('It doesn\'t look like you have permission to list peers IDs. ' + helpfulError);
|
|
1848
|
+
cb([]);
|
|
1849
|
+
} else if (http.status !== 200) {
|
|
1850
|
+
cb([]);
|
|
1851
|
+
} else {
|
|
1852
|
+
cb(JSON.parse(http.responseText));
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
http.send(null);
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
exports.Peer = Peer;
|
|
1859
|
+
/**
|
|
1860
|
+
* Wraps a DataChannel between two Peers.
|
|
1861
|
+
*/
|
|
1862
|
+
function DataConnection(peer, provider, options) {
|
|
1863
|
+
if (!(this instanceof DataConnection)) return new DataConnection(peer, provider, options);
|
|
1864
|
+
EventEmitter.call(this);
|
|
1865
|
+
|
|
1866
|
+
this.options = util.extend({
|
|
1867
|
+
serialization: 'binary',
|
|
1868
|
+
reliable: false
|
|
1869
|
+
}, options);
|
|
1870
|
+
|
|
1871
|
+
// Connection is not open yet.
|
|
1872
|
+
this.open = false;
|
|
1873
|
+
this.type = 'data';
|
|
1874
|
+
this.peer = peer;
|
|
1875
|
+
this.provider = provider;
|
|
1876
|
+
|
|
1877
|
+
this.id = this.options.connectionId || DataConnection._idPrefix + util.randomToken();
|
|
1878
|
+
|
|
1879
|
+
this.label = this.options.label || this.id;
|
|
1880
|
+
this.metadata = this.options.metadata;
|
|
1881
|
+
this.serialization = this.options.serialization;
|
|
1882
|
+
this.reliable = this.options.reliable;
|
|
1883
|
+
|
|
1884
|
+
// Data channel buffering.
|
|
1885
|
+
this._buffer = [];
|
|
1886
|
+
this._buffering = false;
|
|
1887
|
+
this.bufferSize = 0;
|
|
1888
|
+
|
|
1889
|
+
// For storing large data.
|
|
1890
|
+
this._chunkedData = {};
|
|
1891
|
+
|
|
1892
|
+
if (this.options._payload) {
|
|
1893
|
+
this._peerBrowser = this.options._payload.browser;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
Negotiator.startConnection(
|
|
1897
|
+
this,
|
|
1898
|
+
this.options._payload || {
|
|
1899
|
+
originator: true
|
|
1900
|
+
}
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
util.inherits(DataConnection, EventEmitter);
|
|
1905
|
+
|
|
1906
|
+
DataConnection._idPrefix = 'dc_';
|
|
1907
|
+
|
|
1908
|
+
/** Called by the Negotiator when the DataChannel is ready. */
|
|
1909
|
+
DataConnection.prototype.initialize = function(dc) {
|
|
1910
|
+
this._dc = this.dataChannel = dc;
|
|
1911
|
+
this._configureDataChannel();
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
DataConnection.prototype._configureDataChannel = function() {
|
|
1915
|
+
var self = this;
|
|
1916
|
+
if (util.supports.sctp) {
|
|
1917
|
+
this._dc.binaryType = 'arraybuffer';
|
|
1918
|
+
}
|
|
1919
|
+
this._dc.onopen = function() {
|
|
1920
|
+
util.log('Data channel connection success');
|
|
1921
|
+
self.open = true;
|
|
1922
|
+
self.emit('open');
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
// Use the Reliable shim for non Firefox browsers
|
|
1926
|
+
if (!util.supports.sctp && this.reliable) {
|
|
1927
|
+
this._reliable = new Reliable(this._dc, util.debug);
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
if (this._reliable) {
|
|
1931
|
+
this._reliable.onmessage = function(msg) {
|
|
1932
|
+
self.emit('data', msg);
|
|
1933
|
+
};
|
|
1934
|
+
} else {
|
|
1935
|
+
this._dc.onmessage = function(e) {
|
|
1936
|
+
self._handleDataMessage(e);
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
this._dc.onclose = function(e) {
|
|
1940
|
+
util.log('DataChannel closed for:', self.peer);
|
|
1941
|
+
self.close();
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
// Handles a DataChannel message.
|
|
1946
|
+
DataConnection.prototype._handleDataMessage = function(e) {
|
|
1947
|
+
var self = this;
|
|
1948
|
+
var data = e.data;
|
|
1949
|
+
var datatype = data.constructor;
|
|
1950
|
+
if (this.serialization === 'binary' || this.serialization === 'binary-utf8') {
|
|
1951
|
+
if (datatype === Blob) {
|
|
1952
|
+
// Datatype should never be blob
|
|
1953
|
+
util.blobToArrayBuffer(data, function(ab) {
|
|
1954
|
+
data = util.unpack(ab);
|
|
1955
|
+
self.emit('data', data);
|
|
1956
|
+
});
|
|
1957
|
+
return;
|
|
1958
|
+
} else if (datatype === ArrayBuffer) {
|
|
1959
|
+
data = util.unpack(data);
|
|
1960
|
+
} else if (datatype === String) {
|
|
1961
|
+
// String fallback for binary data for browsers that don't support binary yet
|
|
1962
|
+
var ab = util.binaryStringToArrayBuffer(data);
|
|
1963
|
+
data = util.unpack(ab);
|
|
1964
|
+
}
|
|
1965
|
+
} else if (this.serialization === 'json') {
|
|
1966
|
+
data = JSON.parse(data);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
// Check if we've chunked--if so, piece things back together.
|
|
1970
|
+
// We're guaranteed that this isn't 0.
|
|
1971
|
+
if (data.__peerData) {
|
|
1972
|
+
var id = data.__peerData;
|
|
1973
|
+
var chunkInfo = this._chunkedData[id] || {data: [], count: 0, total: data.total};
|
|
1974
|
+
|
|
1975
|
+
chunkInfo.data[data.n] = data.data;
|
|
1976
|
+
chunkInfo.count += 1;
|
|
1977
|
+
|
|
1978
|
+
if (chunkInfo.total === chunkInfo.count) {
|
|
1979
|
+
// Clean up before making the recursive call to `_handleDataMessage`.
|
|
1980
|
+
delete this._chunkedData[id];
|
|
1981
|
+
|
|
1982
|
+
// We've received all the chunks--time to construct the complete data.
|
|
1983
|
+
data = new Blob(chunkInfo.data);
|
|
1984
|
+
this._handleDataMessage({data: data});
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
this._chunkedData[id] = chunkInfo;
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
this.emit('data', data);
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
/**
|
|
1995
|
+
* Exposed functionality for users.
|
|
1996
|
+
*/
|
|
1997
|
+
|
|
1998
|
+
/** Allows user to close connection. */
|
|
1999
|
+
DataConnection.prototype.close = function() {
|
|
2000
|
+
if (!this.open) {
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
this.open = false;
|
|
2004
|
+
Negotiator.cleanup(this);
|
|
2005
|
+
this.emit('close');
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
/** Allows user to send data. */
|
|
2009
|
+
DataConnection.prototype.send = function(data, chunked) {
|
|
2010
|
+
if (!this.open) {
|
|
2011
|
+
this.emit('error', new Error('Connection is not open. You should listen for the `open` event before sending messages.'));
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
if (this._reliable) {
|
|
2015
|
+
// Note: reliable shim sending will make it so that you cannot customize
|
|
2016
|
+
// serialization.
|
|
2017
|
+
this._reliable.send(data);
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
var self = this;
|
|
2021
|
+
if (this.serialization === 'json') {
|
|
2022
|
+
this._bufferedSend(JSON.stringify(data));
|
|
2023
|
+
} else if (this.serialization === 'binary' || this.serialization === 'binary-utf8') {
|
|
2024
|
+
var blob = util.pack(data);
|
|
2025
|
+
|
|
2026
|
+
// For Chrome-Firefox interoperability, we need to make Firefox "chunk"
|
|
2027
|
+
// the data it sends out.
|
|
2028
|
+
var needsChunking = util.chunkedBrowsers[this._peerBrowser] || util.chunkedBrowsers[util.browser];
|
|
2029
|
+
if (needsChunking && !chunked && blob.size > util.chunkedMTU) {
|
|
2030
|
+
this._sendChunks(blob);
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
// DataChannel currently only supports strings.
|
|
2035
|
+
if (!util.supports.sctp) {
|
|
2036
|
+
util.blobToBinaryString(blob, function(str) {
|
|
2037
|
+
self._bufferedSend(str);
|
|
2038
|
+
});
|
|
2039
|
+
} else if (!util.supports.binaryBlob) {
|
|
2040
|
+
// We only do this if we really need to (e.g. blobs are not supported),
|
|
2041
|
+
// because this conversion is costly.
|
|
2042
|
+
util.blobToArrayBuffer(blob, function(ab) {
|
|
2043
|
+
self._bufferedSend(ab);
|
|
2044
|
+
});
|
|
2045
|
+
} else {
|
|
2046
|
+
this._bufferedSend(blob);
|
|
2047
|
+
}
|
|
2048
|
+
} else {
|
|
2049
|
+
this._bufferedSend(data);
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
DataConnection.prototype._bufferedSend = function(msg) {
|
|
2054
|
+
if (this._buffering || !this._trySend(msg)) {
|
|
2055
|
+
this._buffer.push(msg);
|
|
2056
|
+
this.bufferSize = this._buffer.length;
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// Returns true if the send succeeds.
|
|
2061
|
+
DataConnection.prototype._trySend = function(msg) {
|
|
2062
|
+
try {
|
|
2063
|
+
this._dc.send(msg);
|
|
2064
|
+
} catch (e) {
|
|
2065
|
+
this._buffering = true;
|
|
2066
|
+
|
|
2067
|
+
var self = this;
|
|
2068
|
+
setTimeout(function() {
|
|
2069
|
+
// Try again.
|
|
2070
|
+
self._buffering = false;
|
|
2071
|
+
self._tryBuffer();
|
|
2072
|
+
}, 100);
|
|
2073
|
+
return false;
|
|
2074
|
+
}
|
|
2075
|
+
return true;
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
// Try to send the first message in the buffer.
|
|
2079
|
+
DataConnection.prototype._tryBuffer = function() {
|
|
2080
|
+
if (this._buffer.length === 0) {
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
var msg = this._buffer[0];
|
|
2085
|
+
|
|
2086
|
+
if (this._trySend(msg)) {
|
|
2087
|
+
this._buffer.shift();
|
|
2088
|
+
this.bufferSize = this._buffer.length;
|
|
2089
|
+
this._tryBuffer();
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
DataConnection.prototype._sendChunks = function(blob) {
|
|
2094
|
+
var blobs = util.chunk(blob);
|
|
2095
|
+
for (var i = 0, ii = blobs.length; i < ii; i += 1) {
|
|
2096
|
+
var blob = blobs[i];
|
|
2097
|
+
this.send(blob, true);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
DataConnection.prototype.handleMessage = function(message) {
|
|
2102
|
+
var payload = message.payload;
|
|
2103
|
+
|
|
2104
|
+
switch (message.type) {
|
|
2105
|
+
case 'ANSWER':
|
|
2106
|
+
this._peerBrowser = payload.browser;
|
|
2107
|
+
|
|
2108
|
+
// Forward to negotiator
|
|
2109
|
+
Negotiator.handleSDP(message.type, this, payload.sdp);
|
|
2110
|
+
break;
|
|
2111
|
+
case 'CANDIDATE':
|
|
2112
|
+
Negotiator.handleCandidate(this, payload.candidate);
|
|
2113
|
+
break;
|
|
2114
|
+
default:
|
|
2115
|
+
util.warn('Unrecognized message type:', message.type, 'from peer:', this.peer);
|
|
2116
|
+
break;
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
2120
|
+
* Wraps the streaming interface between two Peers.
|
|
2121
|
+
*/
|
|
2122
|
+
function MediaConnection(peer, provider, options) {
|
|
2123
|
+
if (!(this instanceof MediaConnection)) return new MediaConnection(peer, provider, options);
|
|
2124
|
+
EventEmitter.call(this);
|
|
2125
|
+
|
|
2126
|
+
this.options = util.extend({}, options);
|
|
2127
|
+
|
|
2128
|
+
this.open = false;
|
|
2129
|
+
this.type = 'media';
|
|
2130
|
+
this.peer = peer;
|
|
2131
|
+
this.provider = provider;
|
|
2132
|
+
this.metadata = this.options.metadata;
|
|
2133
|
+
this.localStream = this.options._stream;
|
|
2134
|
+
|
|
2135
|
+
this.id = this.options.connectionId || MediaConnection._idPrefix + util.randomToken();
|
|
2136
|
+
if (this.localStream) {
|
|
2137
|
+
Negotiator.startConnection(
|
|
2138
|
+
this,
|
|
2139
|
+
{_stream: this.localStream, originator: true}
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2142
|
+
};
|
|
2143
|
+
|
|
2144
|
+
util.inherits(MediaConnection, EventEmitter);
|
|
2145
|
+
|
|
2146
|
+
MediaConnection._idPrefix = 'mc_';
|
|
2147
|
+
|
|
2148
|
+
MediaConnection.prototype.addStream = function(remoteStream) {
|
|
2149
|
+
util.log('Receiving stream', remoteStream);
|
|
2150
|
+
|
|
2151
|
+
this.remoteStream = remoteStream;
|
|
2152
|
+
this.emit('stream', remoteStream); // Should we call this `open`?
|
|
2153
|
+
|
|
2154
|
+
};
|
|
2155
|
+
|
|
2156
|
+
MediaConnection.prototype.handleMessage = function(message) {
|
|
2157
|
+
var payload = message.payload;
|
|
2158
|
+
|
|
2159
|
+
switch (message.type) {
|
|
2160
|
+
case 'ANSWER':
|
|
2161
|
+
// Forward to negotiator
|
|
2162
|
+
Negotiator.handleSDP(message.type, this, payload.sdp);
|
|
2163
|
+
this.open = true;
|
|
2164
|
+
break;
|
|
2165
|
+
case 'CANDIDATE':
|
|
2166
|
+
Negotiator.handleCandidate(this, payload.candidate);
|
|
2167
|
+
break;
|
|
2168
|
+
default:
|
|
2169
|
+
util.warn('Unrecognized message type:', message.type, 'from peer:', this.peer);
|
|
2170
|
+
break;
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
MediaConnection.prototype.answer = function(stream) {
|
|
2175
|
+
if (this.localStream) {
|
|
2176
|
+
util.warn('Local stream already exists on this MediaConnection. Are you answering a call twice?');
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
this.options._payload._stream = stream;
|
|
2181
|
+
|
|
2182
|
+
this.localStream = stream;
|
|
2183
|
+
Negotiator.startConnection(
|
|
2184
|
+
this,
|
|
2185
|
+
this.options._payload
|
|
2186
|
+
)
|
|
2187
|
+
// Retrieve lost messages stored because PeerConnection not set up.
|
|
2188
|
+
var messages = this.provider._getMessages(this.id);
|
|
2189
|
+
for (var i = 0, ii = messages.length; i < ii; i += 1) {
|
|
2190
|
+
this.handleMessage(messages[i]);
|
|
2191
|
+
}
|
|
2192
|
+
this.open = true;
|
|
2193
|
+
};
|
|
2194
|
+
|
|
2195
|
+
/**
|
|
2196
|
+
* Exposed functionality for users.
|
|
2197
|
+
*/
|
|
2198
|
+
|
|
2199
|
+
/** Allows user to close connection. */
|
|
2200
|
+
MediaConnection.prototype.close = function() {
|
|
2201
|
+
if (!this.open) {
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
this.open = false;
|
|
2205
|
+
Negotiator.cleanup(this);
|
|
2206
|
+
this.emit('close')
|
|
2207
|
+
};
|
|
2208
|
+
/**
|
|
2209
|
+
* Manages all negotiations between Peers.
|
|
2210
|
+
*/
|
|
2211
|
+
var Negotiator = {
|
|
2212
|
+
pcs: {
|
|
2213
|
+
data: {},
|
|
2214
|
+
media: {}
|
|
2215
|
+
}, // type => {peerId: {pc_id: pc}}.
|
|
2216
|
+
//providers: {}, // provider's id => providers (there may be multiple providers/client.
|
|
2217
|
+
queue: [] // connections that are delayed due to a PC being in use.
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
Negotiator._idPrefix = 'pc_';
|
|
2221
|
+
|
|
2222
|
+
/** Returns a PeerConnection object set up correctly (for data, media). */
|
|
2223
|
+
Negotiator.startConnection = function(connection, options) {
|
|
2224
|
+
var pc = Negotiator._getPeerConnection(connection, options);
|
|
2225
|
+
|
|
2226
|
+
if (connection.type === 'media' && options._stream) {
|
|
2227
|
+
// Add the stream.
|
|
2228
|
+
pc.addStream(options._stream);
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
// Set the connection's PC.
|
|
2232
|
+
connection.pc = connection.peerConnection = pc;
|
|
2233
|
+
// What do we need to do now?
|
|
2234
|
+
if (options.originator) {
|
|
2235
|
+
if (connection.type === 'data') {
|
|
2236
|
+
// Create the datachannel.
|
|
2237
|
+
var config = {};
|
|
2238
|
+
// Dropping reliable:false support, since it seems to be crashing
|
|
2239
|
+
// Chrome.
|
|
2240
|
+
/*if (util.supports.sctp && !options.reliable) {
|
|
2241
|
+
// If we have canonical reliable support...
|
|
2242
|
+
config = {maxRetransmits: 0};
|
|
2243
|
+
}*/
|
|
2244
|
+
// Fallback to ensure older browsers don't crash.
|
|
2245
|
+
if (!util.supports.sctp) {
|
|
2246
|
+
config = {reliable: options.reliable};
|
|
2247
|
+
}
|
|
2248
|
+
var dc = pc.createDataChannel(connection.label, config);
|
|
2249
|
+
connection.initialize(dc);
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
if (!util.supports.onnegotiationneeded) {
|
|
2253
|
+
Negotiator._makeOffer(connection);
|
|
2254
|
+
}
|
|
2255
|
+
} else {
|
|
2256
|
+
Negotiator.handleSDP('OFFER', connection, options.sdp);
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
Negotiator._getPeerConnection = function(connection, options) {
|
|
2261
|
+
if (!Negotiator.pcs[connection.type]) {
|
|
2262
|
+
util.error(connection.type + ' is not a valid connection type. Maybe you overrode the `type` property somewhere.');
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
if (!Negotiator.pcs[connection.type][connection.peer]) {
|
|
2266
|
+
Negotiator.pcs[connection.type][connection.peer] = {};
|
|
2267
|
+
}
|
|
2268
|
+
var peerConnections = Negotiator.pcs[connection.type][connection.peer];
|
|
2269
|
+
|
|
2270
|
+
var pc;
|
|
2271
|
+
// Not multiplexing while FF and Chrome have not-great support for it.
|
|
2272
|
+
/*if (options.multiplex) {
|
|
2273
|
+
ids = Object.keys(peerConnections);
|
|
2274
|
+
for (var i = 0, ii = ids.length; i < ii; i += 1) {
|
|
2275
|
+
pc = peerConnections[ids[i]];
|
|
2276
|
+
if (pc.signalingState === 'stable') {
|
|
2277
|
+
break; // We can go ahead and use this PC.
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
} else */
|
|
2281
|
+
if (options.pc) { // Simplest case: PC id already provided for us.
|
|
2282
|
+
pc = Negotiator.pcs[connection.type][connection.peer][options.pc];
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
if (!pc || pc.signalingState !== 'stable') {
|
|
2286
|
+
pc = Negotiator._startPeerConnection(connection);
|
|
2287
|
+
}
|
|
2288
|
+
return pc;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
/*
|
|
2292
|
+
Negotiator._addProvider = function(provider) {
|
|
2293
|
+
if ((!provider.id && !provider.disconnected) || !provider.socket.open) {
|
|
2294
|
+
// Wait for provider to obtain an ID.
|
|
2295
|
+
provider.on('open', function(id) {
|
|
2296
|
+
Negotiator._addProvider(provider);
|
|
2297
|
+
});
|
|
2298
|
+
} else {
|
|
2299
|
+
Negotiator.providers[provider.id] = provider;
|
|
2300
|
+
}
|
|
2301
|
+
}*/
|
|
2302
|
+
|
|
2303
|
+
|
|
2304
|
+
/** Start a PC. */
|
|
2305
|
+
Negotiator._startPeerConnection = function(connection) {
|
|
2306
|
+
util.log('Creating RTCPeerConnection.');
|
|
2307
|
+
|
|
2308
|
+
var id = Negotiator._idPrefix + util.randomToken();
|
|
2309
|
+
var optional = {};
|
|
2310
|
+
|
|
2311
|
+
if (connection.type === 'data' && !util.supports.sctp) {
|
|
2312
|
+
optional = {optional: [{RtpDataChannels: true}]};
|
|
2313
|
+
} else if (connection.type === 'media') {
|
|
2314
|
+
// Interop req for chrome.
|
|
2315
|
+
optional = {optional: [{DtlsSrtpKeyAgreement: true}]};
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
var pc = new RTCPeerConnection(connection.provider.options.config, optional);
|
|
2319
|
+
Negotiator.pcs[connection.type][connection.peer][id] = pc;
|
|
2320
|
+
|
|
2321
|
+
Negotiator._setupListeners(connection, pc, id);
|
|
2322
|
+
|
|
2323
|
+
return pc;
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
/** Set up various WebRTC listeners. */
|
|
2327
|
+
Negotiator._setupListeners = function(connection, pc, pc_id) {
|
|
2328
|
+
var peerId = connection.peer;
|
|
2329
|
+
var connectionId = connection.id;
|
|
2330
|
+
var provider = connection.provider;
|
|
2331
|
+
|
|
2332
|
+
// ICE CANDIDATES.
|
|
2333
|
+
util.log('Listening for ICE candidates.');
|
|
2334
|
+
pc.onicecandidate = function(evt) {
|
|
2335
|
+
if (evt.candidate) {
|
|
2336
|
+
util.log('Received ICE candidates for:', connection.peer);
|
|
2337
|
+
provider.socket.send({
|
|
2338
|
+
type: 'CANDIDATE',
|
|
2339
|
+
payload: {
|
|
2340
|
+
candidate: evt.candidate,
|
|
2341
|
+
type: connection.type,
|
|
2342
|
+
connectionId: connection.id
|
|
2343
|
+
},
|
|
2344
|
+
dst: peerId
|
|
2345
|
+
});
|
|
2346
|
+
}
|
|
2347
|
+
};
|
|
2348
|
+
|
|
2349
|
+
pc.oniceconnectionstatechange = function() {
|
|
2350
|
+
switch (pc.iceConnectionState) {
|
|
2351
|
+
case 'disconnected':
|
|
2352
|
+
case 'failed':
|
|
2353
|
+
util.log('iceConnectionState is disconnected, closing connections to ' + peerId);
|
|
2354
|
+
connection.close();
|
|
2355
|
+
break;
|
|
2356
|
+
case 'completed':
|
|
2357
|
+
pc.onicecandidate = util.noop;
|
|
2358
|
+
break;
|
|
2359
|
+
}
|
|
2360
|
+
};
|
|
2361
|
+
|
|
2362
|
+
// Fallback for older Chrome impls.
|
|
2363
|
+
pc.onicechange = pc.oniceconnectionstatechange;
|
|
2364
|
+
|
|
2365
|
+
// ONNEGOTIATIONNEEDED (Chrome)
|
|
2366
|
+
util.log('Listening for `negotiationneeded`');
|
|
2367
|
+
pc.onnegotiationneeded = function() {
|
|
2368
|
+
util.log('`negotiationneeded` triggered');
|
|
2369
|
+
if (pc.signalingState == 'stable') {
|
|
2370
|
+
Negotiator._makeOffer(connection);
|
|
2371
|
+
} else {
|
|
2372
|
+
util.log('onnegotiationneeded triggered when not stable. Is another connection being established?');
|
|
2373
|
+
}
|
|
2374
|
+
};
|
|
2375
|
+
|
|
2376
|
+
// DATACONNECTION.
|
|
2377
|
+
util.log('Listening for data channel');
|
|
2378
|
+
// Fired between offer and answer, so options should already be saved
|
|
2379
|
+
// in the options hash.
|
|
2380
|
+
pc.ondatachannel = function(evt) {
|
|
2381
|
+
util.log('Received data channel');
|
|
2382
|
+
var dc = evt.channel;
|
|
2383
|
+
var connection = provider.getConnection(peerId, connectionId);
|
|
2384
|
+
connection.initialize(dc);
|
|
2385
|
+
};
|
|
2386
|
+
|
|
2387
|
+
// MEDIACONNECTION.
|
|
2388
|
+
util.log('Listening for remote stream');
|
|
2389
|
+
pc.onaddstream = function(evt) {
|
|
2390
|
+
util.log('Received remote stream');
|
|
2391
|
+
var stream = evt.stream;
|
|
2392
|
+
provider.getConnection(peerId, connectionId).addStream(stream);
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
Negotiator.cleanup = function(connection) {
|
|
2397
|
+
util.log('Cleaning up PeerConnection to ' + connection.peer);
|
|
2398
|
+
|
|
2399
|
+
var pc = connection.pc;
|
|
2400
|
+
|
|
2401
|
+
if (!!pc && (pc.readyState !== 'closed' || pc.signalingState !== 'closed')) {
|
|
2402
|
+
pc.close();
|
|
2403
|
+
connection.pc = null;
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
Negotiator._makeOffer = function(connection) {
|
|
2408
|
+
var pc = connection.pc;
|
|
2409
|
+
pc.createOffer(function(offer) {
|
|
2410
|
+
util.log('Created offer.');
|
|
2411
|
+
|
|
2412
|
+
if (!util.supports.sctp && connection.type === 'data' && connection.reliable) {
|
|
2413
|
+
offer.sdp = Reliable.higherBandwidthSDP(offer.sdp);
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
pc.setLocalDescription(offer, function() {
|
|
2417
|
+
util.log('Set localDescription: offer', 'for:', connection.peer);
|
|
2418
|
+
connection.provider.socket.send({
|
|
2419
|
+
type: 'OFFER',
|
|
2420
|
+
payload: {
|
|
2421
|
+
sdp: offer,
|
|
2422
|
+
type: connection.type,
|
|
2423
|
+
label: connection.label,
|
|
2424
|
+
connectionId: connection.id,
|
|
2425
|
+
reliable: connection.reliable,
|
|
2426
|
+
serialization: connection.serialization,
|
|
2427
|
+
metadata: connection.metadata,
|
|
2428
|
+
browser: util.browser
|
|
2429
|
+
},
|
|
2430
|
+
dst: connection.peer
|
|
2431
|
+
});
|
|
2432
|
+
}, function(err) {
|
|
2433
|
+
connection.provider.emitError('webrtc', err);
|
|
2434
|
+
util.log('Failed to setLocalDescription, ', err);
|
|
2435
|
+
});
|
|
2436
|
+
}, function(err) {
|
|
2437
|
+
connection.provider.emitError('webrtc', err);
|
|
2438
|
+
util.log('Failed to createOffer, ', err);
|
|
2439
|
+
}, connection.options.constraints);
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
Negotiator._makeAnswer = function(connection) {
|
|
2443
|
+
var pc = connection.pc;
|
|
2444
|
+
|
|
2445
|
+
pc.createAnswer(function(answer) {
|
|
2446
|
+
util.log('Created answer.');
|
|
2447
|
+
|
|
2448
|
+
if (!util.supports.sctp && connection.type === 'data' && connection.reliable) {
|
|
2449
|
+
answer.sdp = Reliable.higherBandwidthSDP(answer.sdp);
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
pc.setLocalDescription(answer, function() {
|
|
2453
|
+
util.log('Set localDescription: answer', 'for:', connection.peer);
|
|
2454
|
+
connection.provider.socket.send({
|
|
2455
|
+
type: 'ANSWER',
|
|
2456
|
+
payload: {
|
|
2457
|
+
sdp: answer,
|
|
2458
|
+
type: connection.type,
|
|
2459
|
+
connectionId: connection.id,
|
|
2460
|
+
browser: util.browser
|
|
2461
|
+
},
|
|
2462
|
+
dst: connection.peer
|
|
2463
|
+
});
|
|
2464
|
+
}, function(err) {
|
|
2465
|
+
connection.provider.emitError('webrtc', err);
|
|
2466
|
+
util.log('Failed to setLocalDescription, ', err);
|
|
2467
|
+
});
|
|
2468
|
+
}, function(err) {
|
|
2469
|
+
connection.provider.emitError('webrtc', err);
|
|
2470
|
+
util.log('Failed to create answer, ', err);
|
|
2471
|
+
});
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
/** Handle an SDP. */
|
|
2475
|
+
Negotiator.handleSDP = function(type, connection, sdp) {
|
|
2476
|
+
sdp = new RTCSessionDescription(sdp);
|
|
2477
|
+
var pc = connection.pc;
|
|
2478
|
+
|
|
2479
|
+
util.log('Setting remote description', sdp);
|
|
2480
|
+
pc.setRemoteDescription(sdp, function() {
|
|
2481
|
+
util.log('Set remoteDescription:', type, 'for:', connection.peer);
|
|
2482
|
+
|
|
2483
|
+
if (type === 'OFFER') {
|
|
2484
|
+
Negotiator._makeAnswer(connection);
|
|
2485
|
+
}
|
|
2486
|
+
}, function(err) {
|
|
2487
|
+
connection.provider.emitError('webrtc', err);
|
|
2488
|
+
util.log('Failed to setRemoteDescription, ', err);
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
/** Handle a candidate. */
|
|
2493
|
+
Negotiator.handleCandidate = function(connection, ice) {
|
|
2494
|
+
var candidate = ice.candidate;
|
|
2495
|
+
var sdpMLineIndex = ice.sdpMLineIndex;
|
|
2496
|
+
connection.pc.addIceCandidate(new RTCIceCandidate({
|
|
2497
|
+
sdpMLineIndex: sdpMLineIndex,
|
|
2498
|
+
candidate: candidate
|
|
2499
|
+
}));
|
|
2500
|
+
util.log('Added ICE candidate for:', connection.peer);
|
|
2501
|
+
}
|
|
2502
|
+
/**
|
|
2503
|
+
* An abstraction on top of WebSockets and XHR streaming to provide fastest
|
|
2504
|
+
* possible connection for peers.
|
|
2505
|
+
*/
|
|
2506
|
+
function Socket(secure, host, port, path, key) {
|
|
2507
|
+
if (!(this instanceof Socket)) return new Socket(secure, host, port, path, key);
|
|
2508
|
+
|
|
2509
|
+
EventEmitter.call(this);
|
|
2510
|
+
|
|
2511
|
+
// Disconnected manually.
|
|
2512
|
+
this.disconnected = false;
|
|
2513
|
+
this._queue = [];
|
|
2514
|
+
|
|
2515
|
+
var httpProtocol = secure ? 'https://' : 'http://';
|
|
2516
|
+
var wsProtocol = secure ? 'wss://' : 'ws://';
|
|
2517
|
+
this._httpUrl = httpProtocol + host + ':' + port + path + key;
|
|
2518
|
+
this._wsUrl = wsProtocol + host + ':' + port + path + 'peerjs?key=' + key;
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
util.inherits(Socket, EventEmitter);
|
|
2522
|
+
|
|
2523
|
+
|
|
2524
|
+
/** Check in with ID or get one from server. */
|
|
2525
|
+
Socket.prototype.start = function(id, token) {
|
|
2526
|
+
this.id = id;
|
|
2527
|
+
|
|
2528
|
+
this._httpUrl += '/' + id + '/' + token;
|
|
2529
|
+
this._wsUrl += '&id=' + id + '&token=' + token;
|
|
2530
|
+
|
|
2531
|
+
this._startXhrStream();
|
|
2532
|
+
this._startWebSocket();
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
|
|
2536
|
+
/** Start up websocket communications. */
|
|
2537
|
+
Socket.prototype._startWebSocket = function(id) {
|
|
2538
|
+
var self = this;
|
|
2539
|
+
|
|
2540
|
+
if (this._socket) {
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
this._socket = new WebSocket(this._wsUrl);
|
|
2545
|
+
|
|
2546
|
+
this._socket.onmessage = function(event) {
|
|
2547
|
+
try {
|
|
2548
|
+
var data = JSON.parse(event.data);
|
|
2549
|
+
self.emit('message', data);
|
|
2550
|
+
} catch(e) {
|
|
2551
|
+
util.log('Invalid server message', event.data);
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
};
|
|
2555
|
+
|
|
2556
|
+
this._socket.onclose = function(event) {
|
|
2557
|
+
util.log('Socket closed.');
|
|
2558
|
+
self.disconnected = true;
|
|
2559
|
+
self.emit('disconnected');
|
|
2560
|
+
};
|
|
2561
|
+
|
|
2562
|
+
// Take care of the queue of connections if necessary and make sure Peer knows
|
|
2563
|
+
// socket is open.
|
|
2564
|
+
this._socket.onopen = function() {
|
|
2565
|
+
if (self._timeout) {
|
|
2566
|
+
clearTimeout(self._timeout);
|
|
2567
|
+
setTimeout(function(){
|
|
2568
|
+
self._http.abort();
|
|
2569
|
+
self._http = null;
|
|
2570
|
+
}, 5000);
|
|
2571
|
+
}
|
|
2572
|
+
self._sendQueuedMessages();
|
|
2573
|
+
util.log('Socket open');
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
/** Start XHR streaming. */
|
|
2578
|
+
Socket.prototype._startXhrStream = function(n) {
|
|
2579
|
+
try {
|
|
2580
|
+
var self = this;
|
|
2581
|
+
this._http = new XMLHttpRequest();
|
|
2582
|
+
this._http._index = 1;
|
|
2583
|
+
this._http._streamIndex = n || 0;
|
|
2584
|
+
this._http.open('post', this._httpUrl + '/id?i=' + this._http._streamIndex, true);
|
|
2585
|
+
this._http.onreadystatechange = function() {
|
|
2586
|
+
if (this.readyState == 2 && this.old) {
|
|
2587
|
+
this.old.abort();
|
|
2588
|
+
delete this.old;
|
|
2589
|
+
} else if (this.readyState > 2 && this.status === 200 && this.responseText) {
|
|
2590
|
+
self._handleStream(this);
|
|
2591
|
+
} else if (this.status !== 200) {
|
|
2592
|
+
// If we get a different status code, likely something went wrong.
|
|
2593
|
+
// Stop streaming.
|
|
2594
|
+
clearTimeout(self._timeout);
|
|
2595
|
+
self.emit('disconnected');
|
|
2596
|
+
}
|
|
2597
|
+
};
|
|
2598
|
+
this._http.send(null);
|
|
2599
|
+
this._setHTTPTimeout();
|
|
2600
|
+
} catch(e) {
|
|
2601
|
+
util.log('XMLHttpRequest not available; defaulting to WebSockets');
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
|
|
2606
|
+
/** Handles onreadystatechange response as a stream. */
|
|
2607
|
+
Socket.prototype._handleStream = function(http) {
|
|
2608
|
+
// 3 and 4 are loading/done state. All others are not relevant.
|
|
2609
|
+
var messages = http.responseText.split('\n');
|
|
2610
|
+
|
|
2611
|
+
// Check to see if anything needs to be processed on buffer.
|
|
2612
|
+
if (http._buffer) {
|
|
2613
|
+
while (http._buffer.length > 0) {
|
|
2614
|
+
var index = http._buffer.shift();
|
|
2615
|
+
var bufferedMessage = messages[index];
|
|
2616
|
+
try {
|
|
2617
|
+
bufferedMessage = JSON.parse(bufferedMessage);
|
|
2618
|
+
} catch(e) {
|
|
2619
|
+
http._buffer.shift(index);
|
|
2620
|
+
break;
|
|
2621
|
+
}
|
|
2622
|
+
this.emit('message', bufferedMessage);
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
var message = messages[http._index];
|
|
2627
|
+
if (message) {
|
|
2628
|
+
http._index += 1;
|
|
2629
|
+
// Buffering--this message is incomplete and we'll get to it next time.
|
|
2630
|
+
// This checks if the httpResponse ended in a `\n`, in which case the last
|
|
2631
|
+
// element of messages should be the empty string.
|
|
2632
|
+
if (http._index === messages.length) {
|
|
2633
|
+
if (!http._buffer) {
|
|
2634
|
+
http._buffer = [];
|
|
2635
|
+
}
|
|
2636
|
+
http._buffer.push(http._index - 1);
|
|
2637
|
+
} else {
|
|
2638
|
+
try {
|
|
2639
|
+
message = JSON.parse(message);
|
|
2640
|
+
} catch(e) {
|
|
2641
|
+
util.log('Invalid server message', message);
|
|
2642
|
+
return;
|
|
2643
|
+
}
|
|
2644
|
+
this.emit('message', message);
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
Socket.prototype._setHTTPTimeout = function() {
|
|
2650
|
+
var self = this;
|
|
2651
|
+
this._timeout = setTimeout(function() {
|
|
2652
|
+
var old = self._http;
|
|
2653
|
+
if (!self._wsOpen()) {
|
|
2654
|
+
self._startXhrStream(old._streamIndex + 1);
|
|
2655
|
+
self._http.old = old;
|
|
2656
|
+
} else {
|
|
2657
|
+
old.abort();
|
|
2658
|
+
}
|
|
2659
|
+
}, 25000);
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
/** Is the websocket currently open? */
|
|
2663
|
+
Socket.prototype._wsOpen = function() {
|
|
2664
|
+
return this._socket && this._socket.readyState == 1;
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
/** Send queued messages. */
|
|
2668
|
+
Socket.prototype._sendQueuedMessages = function() {
|
|
2669
|
+
for (var i = 0, ii = this._queue.length; i < ii; i += 1) {
|
|
2670
|
+
this.send(this._queue[i]);
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
/** Exposed send for DC & Peer. */
|
|
2675
|
+
Socket.prototype.send = function(data) {
|
|
2676
|
+
if (this.disconnected) {
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
// If we didn't get an ID yet, we can't yet send anything so we should queue
|
|
2681
|
+
// up these messages.
|
|
2682
|
+
if (!this.id) {
|
|
2683
|
+
this._queue.push(data);
|
|
2684
|
+
return;
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
if (!data.type) {
|
|
2688
|
+
this.emit('error', 'Invalid message');
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
var message = JSON.stringify(data);
|
|
2693
|
+
if (this._wsOpen()) {
|
|
2694
|
+
this._socket.send(message);
|
|
2695
|
+
} else {
|
|
2696
|
+
var http = new XMLHttpRequest();
|
|
2697
|
+
var url = this._httpUrl + '/' + data.type.toLowerCase();
|
|
2698
|
+
http.open('post', url, true);
|
|
2699
|
+
http.setRequestHeader('Content-Type', 'application/json');
|
|
2700
|
+
http.send(message);
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
Socket.prototype.close = function() {
|
|
2705
|
+
if (!this.disconnected && this._wsOpen()) {
|
|
2706
|
+
this._socket.close();
|
|
2707
|
+
this.disconnected = true;
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
})(this);
|