@yz-social/webrtc 0.0.5 → 0.1.0
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.
- package/README.md +10 -0
- package/index.js +268 -2
- package/jasmine-standalone-5.7.1/LICENSE +21 -0
- package/jasmine-standalone-5.7.1/SpecRunner.html +28 -0
- package/jasmine-standalone-5.7.1/lib/jasmine-5.7.1/boot0.js +66 -0
- package/jasmine-standalone-5.7.1/lib/jasmine-5.7.1/boot1.js +134 -0
- package/jasmine-standalone-5.7.1/lib/jasmine-5.7.1/jasmine-html.js +971 -0
- package/jasmine-standalone-5.7.1/lib/jasmine-5.7.1/jasmine.css +301 -0
- package/jasmine-standalone-5.7.1/lib/jasmine-5.7.1/jasmine.js +11399 -0
- package/jasmine-standalone-5.7.1/lib/jasmine-5.7.1/jasmine_favicon.png +0 -0
- package/jasmine-standalone-5.7.1/spec/PlayerSpec.js +58 -0
- package/jasmine-standalone-5.7.1/spec/SpecHelper.js +15 -0
- package/jasmine-standalone-5.7.1/src/Player.js +22 -0
- package/jasmine-standalone-5.7.1/src/Song.js +6 -0
- package/package.json +2 -5
- package/spec/test.html +14 -0
- package/spec/webrtcSpec.js +198 -143
- package/lib/datachannels.js +0 -144
- package/lib/peerevents.js +0 -56
- package/lib/utilities.js +0 -92
- package/lib/webrtc.js +0 -57
- package/lib/webrtcbase.js +0 -42
|
Binary file
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
describe('Player', function() {
|
|
2
|
+
let player;
|
|
3
|
+
let song;
|
|
4
|
+
|
|
5
|
+
beforeEach(function() {
|
|
6
|
+
player = new Player();
|
|
7
|
+
song = new Song();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should be able to play a Song', function() {
|
|
11
|
+
player.play(song);
|
|
12
|
+
expect(player.currentlyPlayingSong).toEqual(song);
|
|
13
|
+
|
|
14
|
+
// demonstrates use of custom matcher
|
|
15
|
+
expect(player).toBePlaying(song);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('when song has been paused', function() {
|
|
19
|
+
beforeEach(function() {
|
|
20
|
+
player.play(song);
|
|
21
|
+
player.pause();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should indicate that the song is currently paused', function() {
|
|
25
|
+
expect(player.isPlaying).toBeFalsy();
|
|
26
|
+
|
|
27
|
+
// demonstrates use of 'not' with a custom matcher
|
|
28
|
+
expect(player).not.toBePlaying(song);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should be possible to resume', function() {
|
|
32
|
+
player.resume();
|
|
33
|
+
expect(player.isPlaying).toBeTruthy();
|
|
34
|
+
expect(player.currentlyPlayingSong).toEqual(song);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// demonstrates use of spies to intercept and test method calls
|
|
39
|
+
it('tells the current song if the user has made it a favorite', function() {
|
|
40
|
+
spyOn(song, 'persistFavoriteStatus');
|
|
41
|
+
|
|
42
|
+
player.play(song);
|
|
43
|
+
player.makeFavorite();
|
|
44
|
+
|
|
45
|
+
expect(song.persistFavoriteStatus).toHaveBeenCalledWith(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
//demonstrates use of expected exceptions
|
|
49
|
+
describe('#resume', function() {
|
|
50
|
+
it('should throw an exception if song is already playing', function() {
|
|
51
|
+
player.play(song);
|
|
52
|
+
|
|
53
|
+
expect(function() {
|
|
54
|
+
player.resume();
|
|
55
|
+
}).toThrowError('song is already playing');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
beforeEach(function () {
|
|
2
|
+
jasmine.addMatchers({
|
|
3
|
+
toBePlaying: function () {
|
|
4
|
+
return {
|
|
5
|
+
compare: function (actual, expected) {
|
|
6
|
+
const player = actual;
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
pass: player.currentlyPlayingSong === expected && player.isPlaying
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class Player {
|
|
2
|
+
play(song) {
|
|
3
|
+
this.currentlyPlayingSong = song;
|
|
4
|
+
this.isPlaying = true;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
pause() {
|
|
8
|
+
this.isPlaying = false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
resume() {
|
|
12
|
+
if (this.isPlaying) {
|
|
13
|
+
throw new Error('song is already playing');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
this.isPlaying = true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
makeFavorite() {
|
|
20
|
+
this.currentlyPlayingSong.persistFavoriteStatus(true);
|
|
21
|
+
}
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yz-social/webrtc",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Streamlined portable webrtc management of p2p and client2server.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"webrtc",
|
|
@@ -20,10 +20,7 @@
|
|
|
20
20
|
"./router": "./routes/index.js"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
|
-
"test": "
|
|
24
|
-
"test-only": "npx jasmine",
|
|
25
|
-
"test-server": "node testServer.js",
|
|
26
|
-
"stop-server": "pkill webrtcTestServer"
|
|
23
|
+
"test": "npx jasmine"
|
|
27
24
|
},
|
|
28
25
|
"dependencies": {
|
|
29
26
|
"@roamhq/wrtc": "^0.9.1"
|
package/spec/test.html
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<link rel="stylesheet" href="../jasmine-standalone-5.7.1/lib/jasmine-5.7.1/jasmine.css">
|
|
5
|
+
<script src="../jasmine-standalone-5.7.1/lib/jasmine-5.7.1/jasmine.js"></script>
|
|
6
|
+
<script src="../jasmine-standalone-5.7.1/lib/jasmine-5.7.1/jasmine-html.js"></script>
|
|
7
|
+
<script src="../jasmine-standalone-5.7.1/lib/jasmine-5.7.1/boot0.js"></script>
|
|
8
|
+
<!-- optional: include a file here that configures the Jasmine env -->
|
|
9
|
+
<script src="../jasmine-standalone-5.7.1/lib/jasmine-5.7.1/boot1.js"></script>
|
|
10
|
+
<script type="module">
|
|
11
|
+
import './webrtcSpec.js';
|
|
12
|
+
</script>
|
|
13
|
+
</head>
|
|
14
|
+
</html>
|
package/spec/webrtcSpec.js
CHANGED
|
@@ -1,158 +1,213 @@
|
|
|
1
1
|
const { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach} = globalThis; // For linters.
|
|
2
|
-
import { WebRTC
|
|
3
|
-
|
|
4
|
-
class DirectSignaling extends WebRTCBase {
|
|
5
|
-
signal(type, message) { // Just invoke the method directly on the otherSide.
|
|
6
|
-
this.otherSide[type](message);
|
|
7
|
-
}
|
|
8
|
-
signals = [];
|
|
9
|
-
}
|
|
10
|
-
function delay(ms) {
|
|
11
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
12
|
-
}
|
|
13
|
-
async function fetchSignals(url, signalsToSend) {
|
|
14
|
-
const response = await fetch(url, {
|
|
15
|
-
method: 'POST',
|
|
16
|
-
headers: { 'Content-Type': 'application/json' },
|
|
17
|
-
body: JSON.stringify(signalsToSend)
|
|
18
|
-
});
|
|
19
|
-
return await response.json();
|
|
20
|
-
}
|
|
2
|
+
import { WebRTC } from '../index.js';
|
|
21
3
|
|
|
22
4
|
describe("WebRTC", function () {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
5
|
+
const isBrowser = typeof(process) === 'undefined';
|
|
6
|
+
// maximums
|
|
7
|
+
// 1 channel pair, without negotiated on first signal: nodejs:83, firefox:150+, safari:85+ but gets confused with closing, chrome/edge:50(?)
|
|
8
|
+
// 50 works across the board with one channel pair
|
|
9
|
+
// On Safari (only), anything more than 32 pair starts to loose messages on the SECOND channel.
|
|
10
|
+
const nPairs = 32; //32;
|
|
11
|
+
let connections = [];
|
|
12
|
+
describe("direct in-process signaling", function () {
|
|
13
|
+
function makePair({debug = false, delay = 0, index = 0} = {}) {
|
|
14
|
+
//const configuration = { iceServers: [] };
|
|
15
|
+
const configuration = { iceServers: WebRTC.iceServers };
|
|
16
|
+
const A = new WebRTC({name: `A (impolite) ${index}`, polite: false, debug, configuration});
|
|
17
|
+
const B = new WebRTC({name: `B (polite) ${index}`, polite: true, debug, configuration});
|
|
18
|
+
async function sendingSetup(dc) { // Given an open channel, set up to receive a message and then send a test message.
|
|
19
|
+
const webrtc = dc.webrtc;
|
|
20
|
+
webrtc.receivedMessageCount = webrtc.sentMessageCount = 0;
|
|
21
|
+
webrtc.gotData = new Promise(resolve => {
|
|
22
|
+
dc.onmessage = e => {
|
|
23
|
+
webrtc.receivedMessageCount++;
|
|
24
|
+
webrtc.log('got message on', dc.label, dc.id, e.data);
|
|
25
|
+
resolve(e.data);
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
webrtc.sentMessageCount++;
|
|
35
29
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
// Subtle:
|
|
31
|
+
//
|
|
32
|
+
// An explicit createChannel() arranges for the promise created by getDataChannelPromise() of the same name to
|
|
33
|
+
// resolve on that side of the connection when the channel is open. The other side will internally receive a
|
|
34
|
+
// datachannel event and we arrange for that side to then also resolve a getDataChannelPromise for that name.
|
|
35
|
+
//
|
|
36
|
+
// If only wide side creates the channel, things work out smoothly. Each side ends up resolving the promise for a
|
|
37
|
+
// channel of the given name. The id is as defaulted or specified if negotiated:true, else an internally determined
|
|
38
|
+
// unique id is used.
|
|
39
|
+
//
|
|
40
|
+
// Things are also smooth if negotiated:true and both sides call createChannel simultaneously, such that there
|
|
41
|
+
// are overlappaing offers. One side backs down and there ends up being one channel with the specified id. (The default
|
|
42
|
+
// id will be the same on both sides IFF any multiple data channels are created in the same order on both sides.)
|
|
43
|
+
//
|
|
44
|
+
// However, when negotiated:false, and both sides call createChannel simultaneously, things get complex. Counterintuitively,
|
|
45
|
+
// RTCPeerConnection arranges for the internall determined id to be unique between the two sides, so both get
|
|
46
|
+
// two channels with the same name and different id. There will be two internal open events on each side: first for the
|
|
47
|
+
// channel that it created, and second for one that the other side created. There isn't any reason to do this (as
|
|
48
|
+
// opposed to specifying negotiated:true, unless the application needs each side to listen on one channel and send
|
|
49
|
+
// on the other. However, getDataChannelPromise return a promise based on the specified name alone, and it only
|
|
50
|
+
// resolves once. For compatibility with the single-sided case, we resolve the first channel (on each side, which
|
|
51
|
+
// are different ids). Thus we must attach onmessage to one channel and send on the other. On both sides, the
|
|
52
|
+
// first channel is what the promise resolves to, but after a delay, the second channel is available on webrtc[channelName].
|
|
40
53
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
54
|
+
if (!delay) return dc.send(`Hello from ${webrtc.name}`);
|
|
55
|
+
await WebRTC.delay(delay);
|
|
56
|
+
webrtc.data.send(`Hello from ${webrtc.name}`);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const aOpen = A.getDataChannelPromise('data').then(sendingSetup);
|
|
60
|
+
const bOpen = B.getDataChannelPromise('data').then(sendingSetup);
|
|
61
|
+
const direct = false; // Does signal work direct/one-sided to the other? False makes a request that waits for a response.
|
|
62
|
+
if (direct) {
|
|
63
|
+
A.signal = message => B.onSignal(message);
|
|
64
|
+
B.signal = message => A.onSignal(message);
|
|
65
|
+
} else {
|
|
66
|
+
A.transferSignals = messages => B.respond(messages);
|
|
67
|
+
B.transferSignals = messages => A.respond(messages);
|
|
68
|
+
}
|
|
69
|
+
return connections[index] = {A, B, bothOpen: Promise.all([aOpen, bOpen])};
|
|
70
|
+
}
|
|
71
|
+
function standardBehavior(setup, {includeConflictCheck = isBrowser, includeSecondChannel = true} = {}) {
|
|
72
|
+
beforeAll(async function () {
|
|
73
|
+
const start = Date.now();
|
|
74
|
+
console.log('start setup');
|
|
75
|
+
for (let index = 0; index < nPairs; index++) {
|
|
76
|
+
await setup({index});
|
|
77
|
+
}
|
|
78
|
+
//await Promise.all(connections.map(connection => connection.bothOpen));
|
|
79
|
+
console.log('end setup', Date.now() - start);
|
|
80
|
+
}, nPairs * 1e3);
|
|
81
|
+
for (let index = 0; index < nPairs; index++) {
|
|
82
|
+
it(`connects ${index}.`, function () {
|
|
83
|
+
const {A, B} = connections[index];
|
|
84
|
+
expect(A.data.readyState).toBe('open');
|
|
85
|
+
expect(B.data.readyState).toBe('open');
|
|
86
|
+
});
|
|
87
|
+
it(`receives ${index}.`, async function () {
|
|
88
|
+
const {A, B} = connections[index];
|
|
89
|
+
await B.gotData;
|
|
90
|
+
expect(B.receivedMessageCount).toBe(A.sentMessageCount);
|
|
91
|
+
await A.gotData;
|
|
92
|
+
expect(A.receivedMessageCount).toBe(B.sentMessageCount);
|
|
93
|
+
});
|
|
94
|
+
it(`learns of one open ${index}.`, function () {
|
|
95
|
+
const {A, B} = connections[index];
|
|
96
|
+
expect(A.sentMessageCount).toBe(1);
|
|
97
|
+
expect(B.sentMessageCount).toBe(1);
|
|
98
|
+
});
|
|
99
|
+
if (includeSecondChannel) {
|
|
100
|
+
it(`handles second channel ${index}.`, async function () {
|
|
101
|
+
const {A, B} = connections[index];
|
|
102
|
+
const aOpen = A.getDataChannelPromise('second');
|
|
103
|
+
const bOpen = B.getDataChannelPromise('second');
|
|
104
|
+
const a = A.createChannel('second', {negotiated: true});
|
|
105
|
+
const b = B.createChannel('second', {negotiated: true});
|
|
106
|
+
const dca = await aOpen;
|
|
107
|
+
let gotit = new Promise(resolve => {
|
|
108
|
+
dca.onmessage = event => resolve(event.data);
|
|
109
|
+
});
|
|
110
|
+
const dcb = await bOpen;
|
|
111
|
+
const start = Date.now();
|
|
112
|
+
dcb.send('message');
|
|
113
|
+
expect(await Promise.race([gotit, WebRTC.delay(2e3, 'timeout')])).toBe('message');
|
|
79
114
|
});
|
|
80
|
-
|
|
81
|
-
|
|
115
|
+
}
|
|
116
|
+
if (includeConflictCheck) {
|
|
117
|
+
it(`politely ignores a conflict ${index}.`, function () {
|
|
118
|
+
const {A, B} = connections[index];
|
|
119
|
+
expect(A.rolledBack).toBeFalsy();
|
|
120
|
+
expect(B.rolledBack).toBeTruthy(); // timing dependent, but useful for debugging
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
afterAll(async function () {
|
|
125
|
+
const start = Date.now();
|
|
126
|
+
console.log('start teardown');
|
|
127
|
+
const promises = [];
|
|
128
|
+
for (let index = 0; index < nPairs; index++) {
|
|
129
|
+
const {A, B} = connections[index];
|
|
130
|
+
let promise = A.close().then(async apc => {
|
|
131
|
+
expect(apc.connectionState).toBe('closed'); // Only on the side that explicitly closed.
|
|
132
|
+
expect(apc.signalingState).toBe('closed');
|
|
133
|
+
const bpc = await B.closed; // Waiting for B to notice.
|
|
134
|
+
await B.close(); // Resources are not necessarilly freed when the other side closes. An explicit close() is needed.
|
|
135
|
+
expect(['closed', 'disconnected', 'failed']).toContain(B.pc.connectionState);
|
|
136
|
+
expect(bpc.signalingState).toBe('closed');
|
|
82
137
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
expect(await aReceived).toBe("blue");
|
|
89
|
-
expect(await bReceived).toBe("red");
|
|
90
|
-
}, 10e3);
|
|
91
|
-
});
|
|
92
|
-
});
|
|
138
|
+
promises.push(promise);
|
|
139
|
+
}
|
|
140
|
+
await Promise.all(promises);
|
|
141
|
+
console.log('end teardown', Date.now() - start);
|
|
142
|
+
}, Math.max(30e3, 1e3 * nPairs));
|
|
93
143
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
});
|
|
102
|
-
test(WebRTC, async (a, b) => {
|
|
103
|
-
a.dataChannelPromise = a.connection.ensureDataChannel(channelName);
|
|
104
|
-
await a.connection.signalsReady; // await for offer
|
|
105
|
-
b.dataChannelPromise = b.connection.ensureDataChannel(channelName, {}, a.connection.signals);
|
|
106
|
-
await b.connection.signalsReady; // await answer
|
|
107
|
-
await a.connection.connectVia(signals => b.connection.respond(signals)); // Transmits signals back and forth.
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe("connection to server", function () {
|
|
112
|
-
describe("using NG ICE", function () {
|
|
113
|
-
let connection, dataChannel;
|
|
114
|
-
beforeAll(async function () {
|
|
115
|
-
connection = WebRTC.ensure({serviceLabel: 'Client'});
|
|
116
|
-
const ready = connection.signalsReady;
|
|
117
|
-
const dataChannelPromise = connection.ensureDataChannel('echo');
|
|
118
|
-
await ready;
|
|
119
|
-
connection.connectVia(signals => fetchSignals("http://localhost:3000/test/data/echo/foo", signals));
|
|
120
|
-
dataChannel = await dataChannelPromise;
|
|
121
|
-
});
|
|
122
|
-
afterAll(function () {
|
|
123
|
-
connection.close();
|
|
144
|
+
describe("one side opens", function () {
|
|
145
|
+
describe('non-negotiated', function () {
|
|
146
|
+
standardBehavior(async function ({index}) {
|
|
147
|
+
const {A, B, bothOpen} = makePair({index});
|
|
148
|
+
A.createChannel('data', {negotiated: false});
|
|
149
|
+
await bothOpen;
|
|
150
|
+
}, {includeConflictCheck: false, includeSecondChannel: false});
|
|
124
151
|
});
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
152
|
+
describe("negotiated on first signal", function () {
|
|
153
|
+
standardBehavior(async function ({index}) {
|
|
154
|
+
const {A, B, bothOpen} = makePair({index});
|
|
155
|
+
// There isn't really a direct, automated way to have one side open another with negotiated:true,
|
|
156
|
+
// because the receiving RTCPeerConnection does not fire 'datachannel' when the sender was negotiated:true.
|
|
157
|
+
// However, what the app can do is wake up and create an explicit createChannel when it first receives an
|
|
158
|
+
// unsolicited offer.
|
|
159
|
+
let awake = false;
|
|
160
|
+
A.signal = async msg => {
|
|
161
|
+
if (!awake) {
|
|
162
|
+
awake = true;
|
|
163
|
+
B.createChannel("data", {negotiated: true});
|
|
164
|
+
}
|
|
165
|
+
B.onSignal(msg);
|
|
166
|
+
};
|
|
167
|
+
A.createChannel('data', {negotiated: true});
|
|
168
|
+
await bothOpen;
|
|
169
|
+
}, {includeConflictCheck: false});
|
|
129
170
|
});
|
|
130
171
|
});
|
|
131
|
-
});
|
|
132
172
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
173
|
+
describe("simultaneous two-sided", function () {
|
|
174
|
+
describe("negotiated single full-duplex-channel", function () {
|
|
175
|
+
describe("impolite first", function () {
|
|
176
|
+
standardBehavior(async function ({index}) {
|
|
177
|
+
const {A, B, bothOpen} = makePair({index});
|
|
178
|
+
A.createChannel("data", {negotiated: true});
|
|
179
|
+
B.createChannel("data", {negotiated: true});
|
|
180
|
+
await bothOpen;
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
describe("polite first", function () {
|
|
184
|
+
standardBehavior(async function ({index}) {
|
|
185
|
+
const {A, B, bothOpen} = makePair({index});
|
|
186
|
+
B.createChannel("data", {negotiated: true});
|
|
187
|
+
A.createChannel("data", {negotiated: true});
|
|
188
|
+
await bothOpen;
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
describe("non-negotiated dual half-duplex channels", function () {
|
|
193
|
+
const delay = 10;
|
|
194
|
+
describe("impolite first", function () {
|
|
195
|
+
standardBehavior(async function ({index}) {
|
|
196
|
+
const {A, B, bothOpen} = makePair({delay, index});
|
|
197
|
+
A.createChannel("data", {negotiated: false});
|
|
198
|
+
B.createChannel("data", {negotiated: false});
|
|
199
|
+
await bothOpen;
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
describe("polite first", function () {
|
|
203
|
+
standardBehavior(async function ({index}) {
|
|
204
|
+
const {A, B, bothOpen} = makePair({delay, index});
|
|
205
|
+
B.createChannel("data", {negotiated: false});
|
|
206
|
+
A.createChannel("data", {negotiated: false});
|
|
207
|
+
await bothOpen;
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
|
156
211
|
});
|
|
157
212
|
});
|
|
158
213
|
});
|
package/lib/datachannels.js
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { WebRTCPeerEvents } from './peerevents.js';
|
|
2
|
-
|
|
3
|
-
// Data Channels:
|
|
4
|
-
// -
|
|
5
|
-
// - Send/receive, including the break-up of long messages and re-assembly.
|
|
6
|
-
|
|
7
|
-
export class WebRTCDataChannels extends WebRTCPeerEvents {
|
|
8
|
-
|
|
9
|
-
// Negotiated channels use specific integers on both sides, starting with this number.
|
|
10
|
-
// We do not start at zero because the non-negotiated channels (as used on server relays) generate their
|
|
11
|
-
// own ids starting with 0, and we don't want to conflict.
|
|
12
|
-
// The spec says these can go to 65,534, but I find that starting greater than the value here gives errors.
|
|
13
|
-
// As of 7/6/25, current evergreen browsers work with 1000 base, but Firefox fails in our case (10 negotatiated channels)
|
|
14
|
-
// if any ids are 256 or higher.
|
|
15
|
-
static BASE_CHANNEL_ID = 125;
|
|
16
|
-
channelId = this.constructor.BASE_CHANNEL_ID;
|
|
17
|
-
get hasStartedConnecting() {
|
|
18
|
-
return this.channelId > this.constructor.BASE_CHANNEL_ID;
|
|
19
|
-
}
|
|
20
|
-
async ensureDataChannel(channelName, channelOptions = {}, signals = null) { // Return a promise for an open data channel on this connection.
|
|
21
|
-
// Much of this machinery is for ealing with the first data channel on either side in the case that the connection as a whole
|
|
22
|
-
// has not yet been made.
|
|
23
|
-
|
|
24
|
-
const hasStartedConnecting = this.hasStartedConnecting; // True for first channel only. Must ask before incrementing id.
|
|
25
|
-
const id = this.channelId++; // This and everything leading up to it must be synchronous on this instance, so that id assignment is deterministic.
|
|
26
|
-
const negotiated = this.multiplex && hasStartedConnecting;
|
|
27
|
-
const allowOtherSideToCreate = !hasStartedConnecting /*!negotiated*/ && !!signals; // Only the 0th with signals waits passively.
|
|
28
|
-
// signals can be nullish, in which case the real signals will have to be assigned later. This allows the data channel to be started (and to consume
|
|
29
|
-
// a channelId) synchronously, but the promise won't resolve until the real signals are supplied later. This is
|
|
30
|
-
// useful in multiplexing an ordered series of data channels on an ANSWER connection, where the data channels must
|
|
31
|
-
// match up with an OFFER connection on a peer. This works because of the wonderful happenstance that answer connections
|
|
32
|
-
// getDataChannelPromise (which doesn't require the connection to yet be open) rather than createDataChannel (which would
|
|
33
|
-
// require the connection to already be open).
|
|
34
|
-
const useSignals = !hasStartedConnecting && signals?.length;
|
|
35
|
-
const options = negotiated ? {id, negotiated, ...channelOptions} : channelOptions;
|
|
36
|
-
this.log({label: this.label, channelName, multiplex: this.multiplex, hasStartedConnecting, id, negotiated, allowOtherSideToCreate, useSignals, options});
|
|
37
|
-
if (hasStartedConnecting) {
|
|
38
|
-
await this.connected; // Before creating promise.
|
|
39
|
-
// I sometimes encounter a bug in Safari in which ONE of the channels created soon after connection gets stuck in
|
|
40
|
-
// the connecting readyState and never opens. Experimentally, this seems to be robust.
|
|
41
|
-
//
|
|
42
|
-
// Note to self: If it should turn out that we still have problems, try serializing the calls to peer.createDataChannel
|
|
43
|
-
// so that there isn't more than one channel opening at a time.
|
|
44
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
45
|
-
} else if (useSignals) {
|
|
46
|
-
this.signals = signals;
|
|
47
|
-
}
|
|
48
|
-
const promise = allowOtherSideToCreate ?
|
|
49
|
-
this.getDataChannelPromise(channelName) :
|
|
50
|
-
this.createDataChannel(channelName, options);
|
|
51
|
-
return await promise;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
createDataChannel(label = "data", channelOptions = {}) { // Promise resolves when the channel is open (which will be after any needed negotiation).
|
|
55
|
-
return new Promise(resolve => {
|
|
56
|
-
this.log('create data-channel', label, channelOptions);
|
|
57
|
-
let channel = this.peer.createDataChannel(label, channelOptions);
|
|
58
|
-
this.noteChannel(channel, 'explicit'); // Noted even before opened.
|
|
59
|
-
// The channel may have already been opened on the other side. In this case, all browsers fire the open event anyway,
|
|
60
|
-
// but wrtc (i.e., on nodeJS) does not. So we have to explicitly check.
|
|
61
|
-
switch (channel.readyState) {
|
|
62
|
-
case 'open':
|
|
63
|
-
setTimeout(() => resolve(channel), 10);
|
|
64
|
-
break;
|
|
65
|
-
case 'connecting':
|
|
66
|
-
channel.onopen = _ => resolve(channel);
|
|
67
|
-
break;
|
|
68
|
-
default:
|
|
69
|
-
throw new Error(`Unexpected readyState ${channel.readyState} for data channel ${label}.`);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
waitingChannels = {}; // channelName => resolvers
|
|
74
|
-
getDataChannelPromise(label = "data") { // Resolves to an open data channel.
|
|
75
|
-
return new Promise(resolve => {
|
|
76
|
-
this.log('promise data-channel', label);
|
|
77
|
-
this.waitingChannels[label] = resolve;
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
resetPeer() { // Reset a 'connected' property that promised to resolve when opened, and track incoming datachannels.
|
|
81
|
-
super.resetPeer();
|
|
82
|
-
this.connected = new Promise(resolve => { // this.connected is a promise that resolves when we are.
|
|
83
|
-
this.peer.addEventListener('connectionstatechange', event => {
|
|
84
|
-
if (this.peer.connectionState === 'connected') {
|
|
85
|
-
resolve(true);
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
this.peer.addEventListener('datachannel', event => { // Resolve promise made with getDataChannelPromise().
|
|
90
|
-
const channel = event.channel;
|
|
91
|
-
const label = channel.label;
|
|
92
|
-
const waiting = this.waitingChannels[label];
|
|
93
|
-
this.noteChannel(channel, 'datachannel event', waiting); // Regardless of whether we are waiting.
|
|
94
|
-
if (!waiting) return; // Might not be explicitly waiting. E.g., routers.
|
|
95
|
-
delete this.waitingChannels[label];
|
|
96
|
-
waiting(channel);
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// TODO: (Optionaly?) Set up generic jsonrpc on each channel, along with serialization and fragmentation/assembly for long messages. (See collections/serializer)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// We need to know if there are open data channels. There is a proposal and even an accepted PR for RTCPeerConnection.getDataChannels(),
|
|
104
|
-
// https://github.com/w3c/webrtc-extensions/issues/110
|
|
105
|
-
// but it hasn't been deployed everywhere yet. So we'll need to keep our own count.
|
|
106
|
-
// Alas, a count isn't enough, because we can open stuff, and the other side can open stuff, but if it happens to be
|
|
107
|
-
// the same "negotiated" id, it isn't really a different channel. (https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/datachannel_event
|
|
108
|
-
dataChannels = new Map(); // channelName => Open channel objects.
|
|
109
|
-
reportChannels() { // Return a report string useful for debugging.
|
|
110
|
-
const entries = Array.from(this.dataChannels.entries());
|
|
111
|
-
const kv = entries.map(([k, v]) => `${k}:${v.id}`);
|
|
112
|
-
return `${this.dataChannels.size}/${kv.join(', ')}`;
|
|
113
|
-
}
|
|
114
|
-
noteChannel(channel, source, waiting) { // Bookkeep open channel and return it.
|
|
115
|
-
// Emperically, with multiplex false: // 18 occurrences, with id=null|0|1 as for eventchannel or createDataChannel
|
|
116
|
-
// Apparently, without negotiation, id is initially null (regardless of options.id), and then assigned to a free value during opening
|
|
117
|
-
const key = channel.label; //fixme channel.id === null ? 1 : channel.id;
|
|
118
|
-
const existing = this.dataChannels.get(key);
|
|
119
|
-
this.log('got data-channel', source, key, channel.readyState, 'existing:', existing, 'waiting:', waiting);
|
|
120
|
-
this.dataChannels.set(key, channel);
|
|
121
|
-
channel.addEventListener('close', event => { // Close whole connection when no more data channels or streams.
|
|
122
|
-
this.dataChannels.delete(key);
|
|
123
|
-
// If there's nothing open, close the connection.
|
|
124
|
-
if (this.dataChannels.size) return;
|
|
125
|
-
if (this.peer.getSenders().length) return;
|
|
126
|
-
this.close();
|
|
127
|
-
});
|
|
128
|
-
return channel;
|
|
129
|
-
}
|
|
130
|
-
close(removeConnection = true) {
|
|
131
|
-
this.channelId = this.constructor.BASE_CHANNEL_ID;
|
|
132
|
-
super.close();
|
|
133
|
-
if (removeConnection) this.constructor.connections.delete(this.serviceLabel);
|
|
134
|
-
// If the webrtc implementation closes the data channels before the peer itself, then this.dataChannels will be empty.
|
|
135
|
-
// But if not (e.g., status 'failed' or 'disconnected' on Safari), then let us explicitly close them so that Synchronizers know to clean up.
|
|
136
|
-
for (const channel of this.dataChannels.values()) {
|
|
137
|
-
if (channel.readyState !== 'open') continue;
|
|
138
|
-
// It appears that in Safari (18.5) for a call to channel.close() with the connection already internally closed, Safari
|
|
139
|
-
// will set channel.readyState to 'closing', but NOT fire the closed or closing event. So we have to dispatch it ourselves.
|
|
140
|
-
//channel.close();
|
|
141
|
-
channel.dispatchEvent(new Event('close'));
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|