196 lines
6.9 KiB
JavaScript
196 lines
6.9 KiB
JavaScript
import { describe, expect, test } from "@odoo/hoot";
|
|
import { advanceTime } from "@odoo/hoot-mock";
|
|
import { browser } from "@web/core/browser/browser";
|
|
import { onRpc, mountWebClient } from "@web/../tests/web_test_helpers";
|
|
import { defineMailModels, mockGetMedia } from "@mail/../tests/mail_test_helpers";
|
|
import { PeerToPeer, STREAM_TYPE, UPDATE_EVENT } from "@mail/discuss/call/common/peer_to_peer";
|
|
|
|
describe.current.tags("desktop");
|
|
defineMailModels();
|
|
|
|
class Network {
|
|
_peerToPeerInstances = new Map();
|
|
_notificationRoute;
|
|
constructor(route) {
|
|
this._notificationRoute = route || "/any/mock/notification";
|
|
onRpc(this._notificationRoute, async (req) => {
|
|
const {
|
|
params: { peer_notifications },
|
|
} = await req.json();
|
|
for (const notification of peer_notifications) {
|
|
const [sender_session_id, target_session_ids, content] = notification;
|
|
for (const id of target_session_ids) {
|
|
const p2p = this._peerToPeerInstances.get(id);
|
|
p2p.handleNotification(sender_session_id, content);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* @param id
|
|
* @return {{id, p2p: PeerToPeer}}
|
|
*/
|
|
register(id) {
|
|
const p2p = new PeerToPeer({ notificationRoute: this._notificationRoute });
|
|
this._peerToPeerInstances.set(id, p2p);
|
|
return { id, p2p };
|
|
}
|
|
close() {
|
|
for (const p2p of this._peerToPeerInstances.values()) {
|
|
p2p.disconnect();
|
|
}
|
|
}
|
|
}
|
|
|
|
test("basic peer to peer connection", async () => {
|
|
await mountWebClient();
|
|
const channelId = 1;
|
|
const network = new Network();
|
|
const user1 = network.register(1);
|
|
const user2 = network.register(2);
|
|
user2.remoteStates = new Map();
|
|
user2.p2p.addEventListener("update", ({ detail: { name, payload } }) => {
|
|
if (name === UPDATE_EVENT.CONNECTION_CHANGE) {
|
|
user2.remoteStates.set(payload.id, payload.state);
|
|
}
|
|
});
|
|
|
|
user2.p2p.connect(user2.id, channelId);
|
|
user1.p2p.connect(user1.id, channelId);
|
|
await user1.p2p.addPeer(user2.id);
|
|
expect(user2.remoteStates.get(user1.id)).toBe("connected");
|
|
network.close();
|
|
});
|
|
|
|
test("mesh peer to peer connections", async () => {
|
|
await mountWebClient();
|
|
const channelId = 2;
|
|
const network = new Network();
|
|
const userCount = 10;
|
|
const users = Array.from({ length: userCount }, (_, i) => network.register(i));
|
|
const promises = [];
|
|
for (const user of users) {
|
|
user.p2p.connect(user.id, channelId);
|
|
for (let i = 0; i < user.id; i++) {
|
|
promises.push(user.p2p.addPeer(i));
|
|
}
|
|
}
|
|
await Promise.all(promises);
|
|
|
|
let connectionsCount = 0;
|
|
for (const user of users) {
|
|
connectionsCount += user.p2p.peers.size;
|
|
}
|
|
expect(connectionsCount).toBe(userCount * (userCount - 1));
|
|
connectionsCount = 0;
|
|
network.close();
|
|
for (const user of users) {
|
|
connectionsCount += user.p2p.peers.size;
|
|
}
|
|
expect(connectionsCount).toBe(0);
|
|
});
|
|
|
|
test("connection recovery", async () => {
|
|
await mountWebClient();
|
|
const channelId = 1;
|
|
const network = new Network();
|
|
const user1 = network.register(1);
|
|
const user2 = network.register(2);
|
|
user2.remoteStates = new Map();
|
|
user2.p2p.addEventListener("update", ({ detail: { name, payload } }) => {
|
|
if (name === UPDATE_EVENT.CONNECTION_CHANGE) {
|
|
user2.remoteStates.set(payload.id, payload.state);
|
|
}
|
|
});
|
|
|
|
user1.p2p.connect(user1.id, channelId);
|
|
user1.p2p.addPeer(user2.id);
|
|
// only connecting user2 after user1 has called addPeer so that user2 ignores notifications
|
|
// from user1, which simulates a connection drop that should be recovered.
|
|
user2.p2p.connect(user2.id, channelId);
|
|
expect(user2.remoteStates.get(user1.id)).toBe(undefined);
|
|
const openPromise = new Promise((resolve) => {
|
|
user1.p2p.peers.get(2).dataChannel.onopen = resolve;
|
|
});
|
|
advanceTime(5_000); // recovery timeout
|
|
await openPromise;
|
|
expect(user2.remoteStates.get(user1.id)).toBe("connected");
|
|
network.close();
|
|
});
|
|
|
|
test("can broadcast a stream and control download", async () => {
|
|
mockGetMedia();
|
|
await mountWebClient();
|
|
const channelId = 3;
|
|
const network = new Network();
|
|
const user1 = network.register(1);
|
|
const user2 = network.register(2);
|
|
user2.remoteMedia = new Map();
|
|
const trackPromise = new Promise((resolve) => {
|
|
user2.p2p.addEventListener("update", ({ detail: { name, payload } }) => {
|
|
if (name === UPDATE_EVENT.TRACK) {
|
|
user2.remoteMedia.set(payload.sessionId, {
|
|
[payload.type]: {
|
|
track: payload.track,
|
|
active: payload.active,
|
|
},
|
|
});
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
|
|
user2.p2p.connect(user2.id, channelId);
|
|
user1.p2p.connect(user1.id, channelId);
|
|
await user1.p2p.addPeer(user2.id);
|
|
const videoStream = await browser.navigator.mediaDevices.getUserMedia({
|
|
video: true,
|
|
});
|
|
const videoTrack = videoStream.getVideoTracks()[0];
|
|
await user1.p2p.updateUpload(STREAM_TYPE.CAMERA, videoTrack);
|
|
await trackPromise;
|
|
const user2RemoteMedia = user2.remoteMedia.get(user1.id);
|
|
const user2CameraTransceiver = user2.p2p.peers.get(user1.id).getTransceiver(STREAM_TYPE.CAMERA);
|
|
expect(user2CameraTransceiver.direction).toBe("recvonly");
|
|
expect(user2RemoteMedia[STREAM_TYPE.CAMERA].track.kind).toBe("video");
|
|
expect(user2RemoteMedia[STREAM_TYPE.CAMERA].active).toBe(true);
|
|
user2.p2p.updateDownload(user1.id, { camera: false });
|
|
expect(user2CameraTransceiver.direction).toBe("inactive");
|
|
network.close();
|
|
});
|
|
|
|
test("can broadcast arbitrary messages (dataChannel)", async () => {
|
|
await mountWebClient();
|
|
const channelId = 4;
|
|
const network = new Network();
|
|
const user1 = network.register(1);
|
|
const user2 = network.register(2);
|
|
user1.inbox = [];
|
|
const pongPromise = new Promise((resolve) => {
|
|
user1.p2p.addEventListener("update", ({ detail: { name, payload } }) => {
|
|
if (name === UPDATE_EVENT.BROADCAST) {
|
|
user1.inbox.push(payload);
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
user2.inbox = [];
|
|
user2.p2p.addEventListener("update", ({ detail: { name, payload } }) => {
|
|
if (name === UPDATE_EVENT.BROADCAST) {
|
|
user2.inbox.push(payload);
|
|
user2.p2p.broadcast("pong");
|
|
}
|
|
});
|
|
|
|
user2.p2p.connect(user2.id, channelId);
|
|
user1.p2p.connect(user1.id, channelId);
|
|
await user1.p2p.addPeer(user2.id);
|
|
user1.p2p.broadcast("ping");
|
|
await pongPromise;
|
|
expect(user2.inbox[0].senderId).toBe(user1.id);
|
|
expect(user2.inbox[0].message).toBe("ping");
|
|
expect(user1.inbox[0].senderId).toBe(user2.id);
|
|
expect(user1.inbox[0].message).toBe("pong");
|
|
network.close();
|
|
});
|