This commit is contained in:
zhangzheng
2026-02-27 18:35:40 +08:00
parent adef8b4cce
commit 1bb1fee5cc
265 changed files with 104076 additions and 92 deletions

View File

@@ -0,0 +1,16 @@
// mock class
export class DOMRect {
constructor(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
get left() {
return this.x;
}
get top() {
return this.y;
}
}

View File

@@ -0,0 +1,11 @@
// mock class
export class DOMHTMLVideoElement {
constructor(rect) {
this.rect = rect;
}
getBoundingClientRect() {
return this.rect;
}
}

View File

@@ -0,0 +1,172 @@
import {
FourCC,
Mouse,
Keyboard,
Touchscreen,
Gamepad,
KeyboardState,
MouseState,
TouchscreenState,
GamepadState,
StateEvent,
InputEvent,
TextEvent
} from "../src/inputdevice.js";
describe(`FourCC`, () => {
test('toInt32', () => {
const number = new FourCC('A', 'A', 'A', 'A').toInt32();
expect(number).toBe(0x41414141);
});
});
describe(`MouseState`, () => {
describe(`with MouseEvent`, () => {
let event;
beforeEach(() => {
event = new MouseEvent('click', { buttons:1, clientX:0, clientY:0});
});
test('format', () => {
const format = new MouseState(event).format;
expect(format).toBe(0x4d4f5553);
});
test('buffer', () => {
const state = new MouseState(event);
expect(state.buffer.byteLength).toBeGreaterThan(0);
});
});
describe(`with WheelEvent`, () => {
let event;
beforeEach(() => {
event = new WheelEvent('wheel', { deltaX:0, deltaY:0 });
});
test('format', () => {
const format = new MouseState(event).format;
expect(format).toBe(0x4d4f5553);
});
test('buffer', () => {
const state = new MouseState(event);
expect(state.buffer.byteLength).toBeGreaterThan(0);
});
});
});
describe(`KeyboardState`, () => {
let event;
beforeEach(() => {
event = new KeyboardEvent('keydown', { code: 'KeyA' });
});
test('format', () => {
const format = new KeyboardState(event).format;
expect(format).toBe(0x4b455953);
});
test('buffer', () => {
const state = new KeyboardState(event);
expect(state.buffer.byteLength).toBeGreaterThan(0);
});
});
describe(`TouchscreenState`, () => {
let event;
beforeEach(() => {
event = new TouchEvent("touchstart", {
changedTouches: [{ // InputInit
identifier: 0,
target: null,
clientX: 0,
clientY: 0,
screenX: 0,
screenY: 0,
pageX: 0,
pageY: 0,
radiusX: 0,
radiusY: 0,
rotationAngle: 0,
force: 0,
altitudeAngle: 0,
azimuthAngle:0,
touchType: "direct"
}]
});
});
test('format', () => {
const format = new TouchscreenState(event, null, Date.now()).format;
expect(format).toBe(0x54534352);
});
test('buffer', () => {
const state = new TouchscreenState(event, null, Date.now());
expect(state.buffer.byteLength).toBeGreaterThan(0);
});
});
describe(`GamepadState`, () => {
let event;
beforeEach(() => {
event = {
type: 'gamepadupdated',
gamepad : {
id: 1,
buttons: Array(16).fill({ pressed: false, value: 1 }),
axes:[0, 0, 0, 0]
}};
});
test('format', () => {
const format = new GamepadState(event).format;
expect(format).toBe(0x47504144);
});
test('buffer', () => {
const state = new GamepadState(event);
expect(state.buffer.byteLength).toBeGreaterThan(0);
});
});
describe(`StateEvent`, () => {
let state;
beforeEach(() => {
const event = new KeyboardEvent('keydown', { code: 'KeyA' });
state = new KeyboardState(event);
});
test('buffer', () => {
const stateEvent = StateEvent.fromState(state, 0, Date.now());
expect(new Int32Array(stateEvent.buffer.slice(0, 4))[0]).toBe(StateEvent.format);
});
});
describe(`TextEvent`, () => {
test('buffer', () => {
const event = new KeyboardEvent('keydown', { code: 'KeyA', key: "a"});
const textEvent = TextEvent.create(0, event, Date.now());
expect(new Int32Array(textEvent.buffer.slice(0, 4))[0]).toBe(TextEvent.format);
const offset = InputEvent.size;
// 'a' is 97
expect(new Uint32Array(textEvent.buffer.slice(offset, offset+4))[0]).toBe(97);
});
});
describe(`Mouse`, () => {
test('alignedSizeInBytes', () => {
let device = new Mouse("Mouse", "Mouse", 1, null, null);
expect(device).toBeInstanceOf(Mouse);
});
});
describe(`Keyboard`, () => {
test('alignedSizeInBytes', () => {
let device = new Keyboard("Keyboard", "Keyboard", 1, null, null);
expect(device).toBeInstanceOf(Keyboard);
});
});
describe(`Touchscreen`, () => {
test('alignedSizeInBytes', () => {
let device = new Touchscreen("Touchscreen", "Touchscreen", 1, null, null);
expect(device).toBeInstanceOf(Touchscreen);
});
});
describe(`Gamepad`, () => {
test('alignedSizeInBytes', () => {
let device = new Gamepad("Gamepad", "Gamepad", 1, null, null);
expect(device).toBeInstanceOf(Gamepad);
});
});

View File

@@ -0,0 +1,132 @@
import {
InputDevice,
MouseState,
KeyboardState,
TouchscreenState,
GamepadState
} from "../src/inputdevice.js";
import {
MessageType,
NewDeviceMsg,
NewEventsMsg,
RemoveDeviceMsg,
InputRemoting,
} from "../src/inputremoting.js";
import {
Sender,
Observer
} from "../src/sender.js";
import {DOMRect} from "./domrect.js";
describe(`InputRemoting`, () => {
let sender = null;
let inputRemoting = null;
let observer = null;
beforeEach(async () => {
document.getBoundingClientRect = function(){ return new DOMRect(0,0,0,0); };
sender = new Sender(document);
inputRemoting = new InputRemoting(sender);
let dc = null;
observer = new Observer(dc);
});
test('startSending', () => {
expect.assertions(0);
inputRemoting.startSending();
});
test('stopSending', () => {
expect.assertions(0);
inputRemoting.startSending();
inputRemoting.stopSending();
});
test('subscribe', () => {
expect.assertions(0);
inputRemoting.subscribe(observer);
});
});
test('create NewDeviceMsg', () => {
const device = new InputDevice("Keyboard", "Keyboard", 0, null, null);
const msg = NewDeviceMsg.create(device);
expect(msg.participant_id).toBe(0);
expect(msg.type).toBe(MessageType.NewDevice);
expect(msg.data).toBeInstanceOf(ArrayBuffer);
expect(msg.data.byteLength).toBeGreaterThan(0);
});
describe('create NewEventMsg', () => {
test('using MouseState', () => {
const event = new MouseEvent('click', { buttons:0, clientX:0, clientY:0} );
const state = new MouseState(event);
const msg = NewEventsMsg.create(state);
expect(msg.participant_id).toBe(0);
expect(msg.type).toBe(MessageType.NewEvents);
expect(msg.data).toBeInstanceOf(ArrayBuffer);
expect(msg.data.byteLength).toBeGreaterThan(0);
});
test('using KeyboardState', () => {
const event = new KeyboardEvent("keydown", { code: 'KeyA' });
const state = new KeyboardState(event);
const msg = NewEventsMsg.create(state);
expect(msg.participant_id).toBe(0);
expect(msg.type).toBe(MessageType.NewEvents);
expect(msg.data).toBeInstanceOf(ArrayBuffer);
expect(msg.data.byteLength).toBeGreaterThan(0);
});
test('using TouchscreenState', () => {
const event = new TouchEvent("touchstart", {
changedTouches: [{ // InputInit
identifier: 0,
target: null,
clientX: 0,
clientY: 0,
screenX: 0,
screenY: 0,
pageX: 0,
pageY: 0,
radiusX: 0,
radiusY: 0,
rotationAngle: 0,
force: 0,
altitudeAngle: 0,
azimuthAngle:0,
touchType: "direct"
}]
});
const state = new TouchscreenState(event, null, Date.now());
expect(state.touchData).not.toBeNull();
expect(state.touchData).toHaveLength(1);
const msg = NewEventsMsg.create(state.touchData[0]);
expect(msg.participant_id).toBe(0);
expect(msg.type).toBe(MessageType.NewEvents);
expect(msg.data).toBeInstanceOf(ArrayBuffer);
expect(msg.data.byteLength).toBeGreaterThan(0);
});
test('using GamepadState', () => {
const event = {
type: 'gamepadupdated',
gamepad : {
id: 1,
buttons: Array(16).fill({ pressed: false, value: 1 }),
axes:[1, 1, 1, 1]
}};
const state = new GamepadState(event);
const msg = NewEventsMsg.create(state);
expect(msg.participant_id).toBe(0);
expect(msg.type).toBe(MessageType.NewEvents);
expect(msg.data).toBeInstanceOf(ArrayBuffer);
expect(msg.data.byteLength).toBeGreaterThan(0);
});
});
test('create RemoveDeviceMsg', () => {
const device = new InputDevice("Keyboard", "Keyboard", 0, null, null);
const msg = RemoveDeviceMsg.create(device);
expect(msg.participant_id).toBe(0);
expect(msg.type).toBe(MessageType.RemoveDevice);
expect(msg.data).toBeInstanceOf(ArrayBuffer);
expect(msg.data.byteLength).toBeGreaterThan(0);
});

View File

@@ -0,0 +1,67 @@
import {
MemoryHelper
} from "../src/memoryhelper.js";
describe(`MemoryHelper.writeSingleBit`, () => {
test('turn on with offset 0', () => {
let bytes = new ArrayBuffer(3);
MemoryHelper.writeSingleBit(bytes, 0, false);
// check 00 00 00
const view = new Uint8Array(bytes);
expect(view[0]).toBe(0);
expect(view[1]).toBe(0);
expect(view[2]).toBe(0);
});
test('turn off with offset 0', () => {
let bytes = new ArrayBuffer(3);
MemoryHelper.writeSingleBit(bytes, 0, true);
// check 00 00 01
const view = new Uint8Array(bytes);
expect(view[0]).toBe(1);
expect(view[1]).toBe(0);
expect(view[2]).toBe(0);
MemoryHelper.writeSingleBit(bytes, 0, false);
// check 00 00 00
expect(view[0]).toBe(0);
expect(view[1]).toBe(0);
expect(view[2]).toBe(0);
});
test('turn on with offset 32', () => {
let bytes = new ArrayBuffer(3);
MemoryHelper.writeSingleBit(bytes, 8, true);
// check 00 01 00
const view = new Uint8Array(bytes);
expect(view[0]).toBe(0);
expect(view[1]).toBe(1);
expect(view[2]).toBe(0);
MemoryHelper.writeSingleBit(bytes, 0, true);
// check 00 01 01
expect(view[0]).toBe(1);
expect(view[1]).toBe(1);
expect(view[2]).toBe(0);
});
test('turn on with offset 15', () => {
let bytes = new ArrayBuffer(3);
MemoryHelper.writeSingleBit(bytes, 15, true);
// check 00 80 00
const view = new Uint8Array(bytes);
expect(view[0]).toBe(0);
expect(view[1]).toBe(128);
expect(view[2]).toBe(0);
MemoryHelper.writeSingleBit(bytes, 15, false);
// check 00 00 00
expect(view[0]).toBe(0);
expect(view[1]).toBe(0);
expect(view[2]).toBe(0);
});
});

View File

@@ -0,0 +1,224 @@
import { sleep } from "./testutils";
/** @type {MockPrivateSignalingManager | MockPublicSignalingManager} */
let manager;
export function reset(isPrivate) {
manager = isPrivate ? new MockPrivateSignalingManager() : new MockPublicSignalingManager();
}
export class MockSignaling extends EventTarget {
constructor(interval = 1000) {
super();
this.interval = interval;
}
async start() {
await manager.add(this);
}
async stop() {
await manager.remove(this);
}
async createConnection(connectionId) {
await manager.openConnection(this, connectionId);
}
async deleteConnection(connectionId) {
await manager.closeConnection(this, connectionId);
}
async sendOffer(connectionId, sdp) {
const data = { 'sdp': sdp, 'connectionId': connectionId };
await manager.offer(this, data);
}
async sendAnswer(connectionId, sdp) {
const data = { 'sdp': sdp, 'connectionId': connectionId };
await manager.answer(this, data);
}
async sendCandidate(connectionId, candidate, sdpMLineIndex, sdpMid) {
const data = {
'candidate': candidate,
'sdpMLineIndex': sdpMLineIndex,
'sdpMid': sdpMid,
'connectionId': connectionId
};
await manager.candidate(this, data);
}
}
class MockPublicSignalingManager {
constructor() {
this.list = new Set();
this.delay = async () => await sleep(10);
}
async add(signaling) {
await this.delay();
this.list.add(signaling);
signaling.dispatchEvent(new Event("start"));
}
async remove(signaling) {
await this.delay();
this.list.delete(signaling);
signaling.dispatchEvent(new Event("end"));
}
async openConnection(signaling, connectionId) {
await this.delay();
const data = { connectionId: connectionId, polite: true };
signaling.dispatchEvent(new CustomEvent("connect", { detail: data }));
}
async closeConnection(signaling, connectionId) {
await this.delay();
const data = { connectionId: connectionId };
for (const element of this.list) {
element.dispatchEvent(new CustomEvent("disconnect", { detail: data }));
}
}
async offer(owner, data) {
await this.delay();
data.polite = false;
for (const signaling of this.list) {
if (signaling != owner) {
signaling.dispatchEvent(new CustomEvent("offer", { detail: data }));
}
}
}
async answer(owner, data) {
await this.delay();
for (const signaling of this.list) {
if (signaling != owner) {
signaling.dispatchEvent(new CustomEvent("answer", { detail: data }));
}
}
}
async candidate(owner, data) {
await this.delay();
for (const signaling of this.list) {
if (signaling != owner) {
signaling.dispatchEvent(new CustomEvent("candidate", { detail: data }));
}
}
}
}
class MockPrivateSignalingManager {
constructor() {
// structure Map<string:connectionId, Set<MockSignaling>> connectionIds
this.connectionIds = new Map();
this.delay = async () => await sleep(10);
}
async add(signaling) {
await this.delay();
signaling.dispatchEvent(new Event("start"));
}
async remove(signaling) {
await this.delay();
signaling.dispatchEvent(new Event("end"));
}
async openConnection(signaling, connectionId) {
await this.delay();
const peerExists = this.connectionIds.has(connectionId);
if (!peerExists) {
this.connectionIds.set(connectionId, new Set());
}
const list = this.connectionIds.get(connectionId);
list.add(signaling);
const data = { connectionId: connectionId, polite: peerExists };
signaling.dispatchEvent(new CustomEvent("connect", { detail: data }));
}
async closeConnection(signaling, connectionId) {
await this.delay();
const peerExists = this.connectionIds.has(connectionId);
const list = this.connectionIds.get(connectionId);
if (!peerExists || !list.has(signaling)) {
console.error(`${connectionId} This connection id is not used.`);
}
const data = { connectionId: connectionId };
for (const element of list) {
element.dispatchEvent(new CustomEvent("disconnect", { detail: data }));
}
list.delete(signaling);
if (list.size == 0) {
this.connectionIds.delete(connectionId);
}
}
findList(owner, connectionId) {
if (!this.connectionIds.has(connectionId)) {
return null;
}
const list = new Set(this.connectionIds.get(connectionId));
list.delete(owner);
if (list.Count == 0) {
return null;
}
return list;
}
async offer(owner, data) {
await this.delay();
const list = this.findList(owner, data.connectionId);
if (list == null) {
console.warn(`${data.connectionId} This connection id is not ready other session.`);
return;
}
data.polite = true;
for (const signaling of list) {
if (signaling != owner) {
signaling.dispatchEvent(new CustomEvent("offer", { detail: data }));
}
}
}
async answer(owner, data) {
await this.delay();
const list = this.findList(owner, data.connectionId);
if (list == null) {
console.warn(`${data.connectionId} This connection id is not ready other session.`);
return;
}
for (const signaling of list) {
if (signaling != owner) {
signaling.dispatchEvent(new CustomEvent("answer", { detail: data }));
}
}
}
async candidate(owner, data) {
await this.delay();
const list = this.findList(owner, data.connectionId);
if (list == null) {
console.warn(`${data.connectionId} This connection id is not ready other session.`);
return;
}
for (const signaling of list) {
if (signaling != owner) {
signaling.dispatchEvent(new CustomEvent("candidate", { detail: data }));
}
}
}
}

View File

@@ -0,0 +1,250 @@
import Peer from "../src/peer.js";
import { waitFor, sleep, getUniqueId, getRTCConfiguration } from "./testutils.js";
describe(`peer connection test`, () => {
const connectionId = "12345";
test(`constructor`, () => {
const peer = new Peer(connectionId, true);
expect(peer).not.toBeNull();
const rtcPeer = peer.pc;
expect(rtcPeer).not.toBeNull();
expect(rtcPeer.ontrack).not.toBeNull();
expect(rtcPeer.onicecandidate).not.toBeNull();
expect(rtcPeer.onnegotiationneeded).not.toBeNull();
expect(rtcPeer.onsignalingstatechange).not.toBeNull();
expect(rtcPeer.oniceconnectionstatechange).not.toBeNull();
expect(rtcPeer.onicegatheringstatechange).not.toBeNull();
});
test(`close peer`, async () => {
const config = getRTCConfiguration();
const peer = new Peer(connectionId, true, config);
expect(peer).not.toBeNull();
peer.close();
expect(peer.connectionId).toBeNull();
expect(peer.pc).toBeNull();
});
test(`transceiver direction is sendrecv if using addtrack`, () => {
const config = getRTCConfiguration();
const peer = new Peer(connectionId, true, config);
expect(peer).not.toBeNull();
const track = { id: getUniqueId(), kind: "audio" };
const sender = peer.addTrack(connectionId, track);
const transceiver = peer.getTransceivers(connectionId).find(t => t.sender == sender);
expect(transceiver.direction).toBe("sendrecv");
});
test(`fire trackevent when addtrack`, async () => {
let trackEvent;
const config = getRTCConfiguration();
const peer = new Peer(connectionId, true, config);
expect(peer).not.toBeNull();
peer.addEventListener('trackevent', (e) => trackEvent = e.detail);
const track = { id: getUniqueId(), kind: "audio" };
peer.addTrack(connectionId, track);
await waitFor(() => trackEvent != null);
expect(trackEvent.track).toBe(track);
});
test(`fire trackevent when on got offer description include track`, async () => {
let trackEvent;
const config = getRTCConfiguration();
const peer = new Peer(connectionId, true, config);
expect(peer).not.toBeNull();
peer.addEventListener('trackevent', (e) => trackEvent = e.detail);
const testDesc = { type: "offer", sdp: "newtracksdp" };
peer.onGotDescription(connectionId, testDesc);
await waitFor(() => trackEvent != null);
expect(trackEvent.track).not.toBeNull();
});
test(`fire sendoffer when addtrack`, async () => {
let offer;
const config = getRTCConfiguration();
const peer = new Peer(connectionId, true, config);
expect(peer).not.toBeNull();
peer.addEventListener('sendoffer', (e) => offer = e.detail);
const track = { id: getUniqueId(), kind: "audio" };
peer.addTrack(connectionId, track);
await waitFor(() => offer != null);
expect(offer.connectionId).toBe(connectionId);
});
test(`fire sendoffer when addTransceiver`, async () => {
let offer;
const config = getRTCConfiguration();
const peer = new Peer(connectionId, true, config);
expect(peer).not.toBeNull();
peer.addEventListener('sendoffer', (e) => offer = e.detail);
peer.addTransceiver(connectionId, "video");
await waitFor(() => offer != null);
expect(offer.connectionId).toBe(connectionId);
});
test(`fire sendoffer when createDataChannel`, async () => {
let offer;
const config = getRTCConfiguration();
const peer = new Peer(connectionId, true, config);
expect(peer).not.toBeNull();
peer.addEventListener('sendoffer', (e) => offer = e.detail);
peer.createDataChannel(connectionId, "testChannel");
await waitFor(() => offer != null);
expect(offer.connectionId).toBe(connectionId);
});
test(`re-fire sendoffer if get answer not yet`, async () => {
let sendOfferCount = 0;
let offer;
const config = getRTCConfiguration();
const peer = new Peer(connectionId, true, config, 100);
expect(peer).not.toBeNull();
peer.addEventListener('sendoffer', (e) => {
offer = e.detail;
sendOfferCount++;
});
const track = { id: getUniqueId(), kind: "audio" };
peer.addTrack(connectionId, track);
await waitFor(() => sendOfferCount > 2);
expect(offer.connectionId).toBe(connectionId);
expect(sendOfferCount).toBeGreaterThan(2);
});
test(`fire sendanswer when on got offer description in polite`, async () => {
let answer;
const config = getRTCConfiguration();
const peer = new Peer(connectionId, true, config);
expect(peer).not.toBeNull();
peer.addEventListener('sendanswer', (e) => answer = e.detail);
const testDesc = { type: "offer", sdp: "newtracksdp" };
peer.onGotDescription(connectionId, testDesc);
await waitFor(() => answer != null);
expect(answer.connectionId).toBe(connectionId);
});
test(`fire sendanswer when on got offer description in polite that have offer`, async () => {
let offer;
let answer;
const config = getRTCConfiguration();
const peer = new Peer(connectionId, true, config);
expect(peer).not.toBeNull();
peer.addEventListener('sendoffer', (e) => offer = e.detail);
peer.addEventListener('sendanswer', (e) => answer = e.detail);
const track = { id: getUniqueId(), kind: "audio" };
peer.addTrack(connectionId, track);
await waitFor(() => offer != null);
expect(offer.connectionId).toBe(connectionId);
const testDesc = { type: "offer", sdp: "newtracksdp" };
peer.onGotDescription(connectionId, testDesc);
await waitFor(() => answer != null);
expect(answer.connectionId).toBe(connectionId);
});
test(`fire sendanswer when on got offer description in impolite that don't have offer`, async () => {
let answer;
const config = getRTCConfiguration();
const peer = new Peer(connectionId, false, config);
expect(peer).not.toBeNull();
peer.addEventListener('sendanswer', (e) => answer = e.detail);
const testDesc = { type: "offer", sdp: "newtracksdp" };
peer.onGotDescription(connectionId, testDesc);
await waitFor(() => answer != null);
expect(answer.connectionId).toBe(connectionId);
});
test(`don't fire sendanswer when on got offer description in impolite that have offer`, async () => {
let offer;
let answer;
const config = getRTCConfiguration();
const peer = new Peer(connectionId, false, config);
expect(peer).not.toBeNull();
peer.addEventListener('sendoffer', (e) => offer = e.detail);
peer.addEventListener('sendanswer', (e) => answer = e.detail);
const track = { id: getUniqueId(), kind: "audio" };
peer.addTrack(connectionId, track);
await waitFor(() => offer != null);
expect(offer.connectionId).toBe(connectionId);
const testDesc = { type: "offer", sdp: "newtracksdp" };
peer.onGotDescription(connectionId, testDesc);
await sleep(100);
expect(answer).toBeUndefined();
});
test(`fire nagotiated when on got answer description that have offer`, async () => {
let offer;
let negotiated = false;
const config = getRTCConfiguration();
const peer = new Peer(connectionId, true, config);
expect(peer).not.toBeNull();
peer.addEventListener('sendoffer', (e) => offer = e.detail);
peer.pc.addEventListener('negotiated', () => negotiated = true);
const track = { id: getUniqueId(), kind: "audio" };
peer.addTrack(connectionId, track);
await waitFor(() => offer != null);
expect(offer.connectionId).toBe(connectionId);
const answerDesc = { type: "answer", sdp: "newtracksdp" };
peer.onGotDescription(connectionId, answerDesc);
await waitFor(() => negotiated);
expect(negotiated).toBeTruthy();
});
test(`fire sendcandidate when on addTransceiver`, async () => {
let candidate;
const config = getRTCConfiguration();
const peer = new Peer(connectionId, true, config);
expect(peer).not.toBeNull();
peer.addEventListener('sendcandidate', (e) => candidate = e.detail);
peer.addTransceiver(connectionId, { id: getUniqueId(), kind: "video" });
await waitFor(() => candidate != null);
expect(candidate.connectionId).toBe(connectionId);
});
test(`accept candidate when on got candidate that have remote description`, async () => {
let answer;
const config = getRTCConfiguration();
const peer = new Peer(connectionId, false, config);
expect(peer).not.toBeNull();
peer.addEventListener('sendanswer', (e) => answer = e.detail);
const testDesc = { type: "offer", sdp: "newtracksdp" };
peer.onGotDescription(connectionId, testDesc);
await waitFor(() => answer != null);
expect(answer.connectionId).toBe(connectionId);
const testCandidate = { candidate: getUniqueId(), sdpMLineIndex: 0, sdpMid: 0 };
peer.onGotCandidate(connectionId, testCandidate);
await waitFor(() => peer.pc.candidates.length > 0);
expect(peer.pc.candidates.length).toBeGreaterThan(0);
});
test(`don't accept candidate when on got candidate that don't have remote description`, async () => {
const config = getRTCConfiguration();
const peer = new Peer(connectionId, false, config);
expect(peer).not.toBeNull();
const testCandidate = { candidate: getUniqueId(), sdpMLineIndex: 0, sdpMid: 0 };
peer.onGotCandidate(connectionId, testCandidate);
await sleep(100);
expect(peer.pc.candidates.length).toBe(0);
});
});

View File

@@ -0,0 +1,316 @@
import { sleep, getUniqueId } from './testutils';
export class PeerConnectionMock extends EventTarget {
constructor(config) {
super();
this.delay = async () => await sleep(10);
this.config = config;
this.ontrack = undefined;
this.ondatachannel = undefined;
this.onicecandidate = undefined;
this.onnegotiationneeded = undefined;
this.onsignalingstatechange = undefined;
this.oniceconnectionstatechange = undefined;
this.onicegatheringstatechange = undefined;
this.pendingLocalDescription = null;
this.currentLocalDescription = null;
this.pendingRemoteDescription = null;
this.currentRemoteDescription = null;
this.candidates = [];
this.signalingState = "stable";
this.iceConnectionState = "new";
this.iceGatheringState = "new";
this.audioTracks = new Map();
this.videoTracks = new Map();
this.channels = new Map();
this.transceiverCount = 0;
this.transceivers = new Map();
}
get localDescription() {
if (this.pendingLocalDescription) {
return this.pendingLocalDescription;
}
return this.currentLocalDescription;
}
get remoteDescription() {
if (this.pendingRemoteDescription) {
return this.pendingRemoteDescription;
}
return this.currentRemoteDescription;
}
close() {
this.ontrack = undefined;
this.ondatachannel = undefined;
this.onicecandidate = undefined;
this.onnegotiationneeded = undefined;
this.onsignalingstatechange = undefined;
this.oniceconnectionstatechange = undefined;
this.onicegatheringstatechange = undefined;
this.pendingLocalDescription = null;
this.currentLocalDescription = null;
this.pendingRemoteDescription = null;
this.currentRemoteDescription = null;
this.candidates = [];
this.signalingState = "close";
this.iceConnectionState = "closed";
this.audioTracks.clear();
this.videoTracks.clear();
this.channels.clear();
this.transceiverCount = 0;
this.transceivers.clear();
}
fireOnNegotiationNeeded() {
if (this.onnegotiationneeded) {
this.onnegotiationneeded();
}
}
getTransceivers() {
return Array.from(this.transceivers.values());
}
addTrack(track) {
if (track.kind == "audio") {
this.audioTracks.set(track.id, track);
} else {
this.videoTracks.set(track.id, track);
}
const transceiver = { direction: "sendrecv", sender: { track: track }, receiver: null, setCodecPreferences: (codecs) => { console.log(codecs); } };
this.transceivers.set(this.transceiverCount++, transceiver);
this.fireOnNegotiationNeeded();
return transceiver.sender;
}
addTransceiver(trackOrKind) {
if (typeof trackOrKind == "string") {
const track = { id: getUniqueId(), kind: trackOrKind };
if (track.kind == "audio") {
this.audioTracks.set(track.id, track);
} else {
this.videoTracks.set(track.id, track);
}
const transceiver = { direction: "sendrecv", sender: { track: track }, receiver: null, setCodecPreferences: (codecs) => { console.log(codecs); } };
this.transceivers.set(this.transceiverCount++, transceiver);
this.fireOnNegotiationNeeded();
return transceiver;
}
if (trackOrKind.kind == "audio") {
this.audioTracks.set(trackOrKind.id, trackOrKind);
} else {
this.videoTracks.set(trackOrKind.id, trackOrKind);
}
const transceiver = { direction: "sendrecv", sender: { track: trackOrKind }, receiver: null, setCodecPreferences: (codecs) => { console.log(codecs); } };
this.transceivers.set(this.transceiverCount++, transceiver);
this.fireOnNegotiationNeeded();
return transceiver;
}
createDataChannel(label) {
const channel = { id: getUniqueId(), label: label };
this.channels.set(channel.id, channel);
this.fireOnNegotiationNeeded();
return channel;
}
async setLocalDescription(description = null) {
if (description == null) {
description = this._createSessionDescription();
}
await this.delay();
this._setSessionDescription(description, false);
}
async setRemoteDescription(description) {
await this.delay();
if (description.type == "offer" && this.signalingState == "have-local-offer") {
this._setSessionDescription({ type: "rollback", sdp: "" }, true);
}
this._setSessionDescription(description, true);
}
_createSessionDescription() {
let dummySdp = "testsdp";
if (this.videoTracks.size > 0) {
dummySdp += "videotrack";
}
if (this.audioTracks.size > 0) {
dummySdp += "audiotrack";
}
if (this.channels.size > 0) {
dummySdp += "datachannel";
}
if (this.signalingState == "stable" || this.signalingState == "have-local-offer" || this.signalingState == "have-remote-pranswer") {
return { type: "offer", sdp: dummySdp };
}
return { type: "answer", sdp: dummySdp };
}
_setSessionDescription(description, remote) {
if (description.type == "rollback"
&& (this.signalingState == "stable" || this.signalingState == "have-local-pranswer" || this.signalingState == "have-remote-pranswer")) {
throw "InvalidStateError";
}
if (description.type != "rollback") {
if (remote) {
if (description.type == "offer") {
this.pendingRemoteDescription = description;
this.signalingState = "have-remote-offer";
this.onsignalingstatechange(this.signalingState);
// if sdp contains track string, create dummy track
if (description.sdp.includes("track")) {
const isVideo = description.sdp.includes("video");
const kind = isVideo ? "video" : "audio";
this._createTrackAndTransceiver(kind);
}
if (description.sdp.includes("datachannel")) {
const channel = { id: getUniqueId(), label: "dummychannel" };
this.channels.set(channel.id, channel);
}
}
if (description.type == "answer") {
this.currentRemoteDescription = description;
this.currentLocalDescription = this.pendingLocalDescription;
this.pendingLocalDescription = null;
this.pendingRemoteDescription = null;
this.signalingState = "stable";
this.onsignalingstatechange(this.signalingState);
}
if (description.type == "pranswer") {
this.pendingRemoteDescription = description;
this.signalingState = "have-remote-pranswer";
this.onsignalingstatechange(this.signalingState);
}
} else {
if (description.type == "offer") {
this.pendingLocalDescription = description;
this.signalingState = "have-local-offer";
this.onsignalingstatechange(this.signalingState);
}
if (description.type == "answer") {
this.currentLocalDescription = description;
this.currentRemoteDescription = this.pendingRemoteDescription;
this.pendingLocalDescription = null;
this.pendingRemoteDescription = null;
this.signalingState = "stable";
this.onsignalingstatechange(this.signalingState);
// if sdp contains track string, create dummy track
if (description.sdp.includes("track")) {
const isVideo = description.sdp.includes("video");
const kind = isVideo ? "video" : "audio";
this._createTrackAndTransceiver(kind);
}
if (description.sdp.includes("datachannel")) {
const channel = { id: getUniqueId(), label: "dummychannel" };
this.channels.set(channel.id, channel);
}
}
if (description.type == "pranswer") {
this.pendingLocalDescription = description;
this.signalingState = "have-local-pranswer";
this.onsignalingstatechange(this.signalingState);
}
}
} else {
this.pendingLocalDescription = null;
this.pendingRemoteDescription = null;
this.signalingState = "stable";
this.onsignalingstatechange(this.signalingState);
}
if (this.videoTracks.size != 0 || this.audioTracks.size != 0) {
this._mockGatheringIceCandidate(this.videoTracks.size + this.audioTracks.size);
}
//fire ontrack with new tracks, after using tracks clear.
if (this.ontrack) {
for (const track of this.videoTracks.values()) {
this.ontrack({ track: track });
}
this.videoTracks.clear();
for (const track of this.audioTracks.values()) {
this.ontrack({ track: track });
}
this.audioTracks.clear();
}
if (this.ondatachannel) {
for (const channel of this.channels.values()) {
this.ondatachannel({ channel: channel });
}
this.channels.clear();
}
}
async _mockGatheringIceCandidate(count) {
this.iceGatheringState = "gathering";
if (this.onicegatheringstatechange) {
this.onicegatheringstatechange(this.iceGatheringState);
}
for (let index = 0; index < count; index++) {
await this.delay();
const newCandidate = { candidate: getUniqueId(), sdpMLineIndex: index, sdpMid: index };
if (this.onicecandidate) {
this.onicecandidate(newCandidate);
}
}
this.iceGatheringState = "complete";
if (this.onicegatheringstatechange) {
this.onicegatheringstatechange(this.iceGatheringState);
}
if (this.onicecandidate) {
this.onicecandidate({ candidate: null, sdpMLineIndex: null, sdpMid: null });
}
}
async addIceCandidate(candidate) {
await this.delay();
if (this.remoteDescription == null) {
throw "InvalidStateError";
}
this.candidates.push(candidate);
}
_createTrackAndTransceiver(kind) {
const track = { id: getUniqueId(), kind: kind };
if (kind == "video") {
this.videoTracks.set(track.id, track);
} else {
this.audioTracks.set(track.id, track);
}
const transceiver = { direction: "sendrecv", sender: { track: track }, receiver: null, setCodecPreferences: (codecs) => { console.log(codecs); } };
this.transceivers.set(this.transceiverCount++, transceiver);
}
}
export class SessionDescriptionMock {
constructor(object) {
this.sdp = object.sdp;
this.type = object.type;
}
sdp;
type;
}
export class IceCandidateMock {
constructor(object) {
this.candidate = object.candidate;
this.sdpMLineIndex = object.sdpMLineIndex;
this.sdpMid = object.sdpMid;
}
candidate;
sdpMLineIndex;
sdpMid;
}

View File

@@ -0,0 +1,45 @@
import {
LetterBoxType,
PointerCorrector
} from "../src/pointercorrect.js";
import {DOMRect} from "./domrect.js";
import {DOMHTMLVideoElement} from "./domvideoelement.js";
describe(`PointerCorrector.map`, () => {
test('letterboxType', () => {
const rect = new DOMRect(10, 10, 200, 200);
const element = new DOMHTMLVideoElement(rect);
let corrector = new PointerCorrector(50, 100, element);
expect(corrector.letterBoxType).toBe(LetterBoxType.Vertical);
corrector.reset(100, 50, element);
expect(corrector.letterBoxType).toBe(LetterBoxType.Horizontal);
});
test('letterboxSize', () => {
const rect = new DOMRect(0, 0, 100, 100);
const element = new DOMHTMLVideoElement(rect);
let corrector = new PointerCorrector(50, 100, element);
expect(corrector.letterBoxSize).toBe(25);
});
test('contentRect', () => {
const rect = new DOMRect(0, 0, 100, 100);
const element = new DOMHTMLVideoElement(rect);
let corrector = new PointerCorrector(50, 100, element);
expect(corrector.contentRect.x).toBe(25);
expect(corrector.contentRect.y).toBe(0);
expect(corrector.contentRect.width).toBe(50);
expect(corrector.contentRect.height).toBe(100);
});
test('mapping', () => {
const rect = new DOMRect(10, 10, 200, 200);
const element = new DOMHTMLVideoElement(rect);
const videoWidth = 100;
const videoHeight = 100;
let corrector = new PointerCorrector(videoWidth, videoHeight, element);
const position = [10, 10];
const newPosition = corrector.map(position);
expect(newPosition[0]).toBe(0);
expect(newPosition[1]).toBe(100);
});
});

View File

@@ -0,0 +1,187 @@
import { MockSignaling, reset } from "./mocksignaling.js";
import { waitFor, getUniqueId, getRTCConfiguration } from "./testutils.js";
import { RenderStreaming } from "../src/renderstreaming.js";
describe.each([
{ mode: "private" },
{ mode: "public" }
])('renderstreaming test', ({ mode }) => {
const connectionId1 = "12345";
test(`createConnection in ${mode} mode`, async () => {
reset(mode == "private");
const config = getRTCConfiguration();
const renderstreaming = new RenderStreaming(new MockSignaling(), config);
await renderstreaming.start();
let isConnect = false;
renderstreaming.onConnect = () => isConnect = true;
await renderstreaming.createConnection(connectionId1);
await waitFor(() => isConnect);
expect(isConnect).toBe(true);
await renderstreaming.stop();
});
test(`addTrack in ${mode} mode`, async () => {
reset(mode == "private");
const config = getRTCConfiguration();
const renderstreaming = new RenderStreaming(new MockSignaling(), config);
await renderstreaming.start();
let isConnect = false;
renderstreaming.onConnect = () => isConnect = true;
await renderstreaming.createConnection(connectionId1);
await waitFor(() => isConnect);
expect(isConnect).toBe(true);
expect(renderstreaming.getTransceivers(connectionId1).length).toBe(0);
const track = { id: getUniqueId(), kind: "audio" };
renderstreaming.addTrack(track);
expect(renderstreaming.getTransceivers(connectionId1).length).toBe(1);
let isDisconnect = false;
renderstreaming.onDisconnect = () => isDisconnect = true;
await renderstreaming.deleteConnection();
await waitFor(() => isDisconnect);
expect(isDisconnect).toBe(true);
await renderstreaming.stop();
});
test(`createChannel in ${mode} mode`, async () => {
reset(mode == "private");
const config = getRTCConfiguration();
const renderstreaming = new RenderStreaming(new MockSignaling(), config);
await renderstreaming.start();
let isConnect = false;
renderstreaming.onConnect = () => isConnect = true;
await renderstreaming.createConnection(connectionId1);
await waitFor(() => isConnect);
expect(isConnect).toBe(true);
expect(renderstreaming.getTransceivers(connectionId1).length).toBe(0);
const label = "testlabel";
const channel = renderstreaming.createDataChannel(label);
expect(channel.label).toBe(label);
let isDisconnect = false;
renderstreaming.onDisconnect = () => isDisconnect = true;
await renderstreaming.deleteConnection();
await waitFor(() => isDisconnect);
expect(isDisconnect).toBe(true);
await renderstreaming.stop();
});
test(`onTrackEvent in ${mode} mode`, async () => {
reset(mode == "private");
const config = getRTCConfiguration();
const renderstreaming1 = new RenderStreaming(new MockSignaling(), config);
const renderstreaming2 = new RenderStreaming(new MockSignaling(), config);
await renderstreaming1.start();
await renderstreaming2.start();
let isConnect1 = false;
renderstreaming1.onConnect = () => isConnect1 = true;
let isConnect2 = false;
renderstreaming2.onConnect = () => isConnect2 = true;
await renderstreaming1.createConnection(connectionId1);
await renderstreaming2.createConnection(connectionId1);
await waitFor(() => isConnect1 && isConnect2);
expect(isConnect1).toBe(true);
expect(isConnect2).toBe(true);
let isGotOffer1 = false;
let isOnTrack1 = false;
let isGotAnswer2 = false;
renderstreaming1.onGotOffer = () => { isGotOffer1 = true; };
renderstreaming1.onTrackEvent = () => { isOnTrack1 = true; };
renderstreaming2.onGotAnswer = () => { isGotAnswer2 = true; };
expect(renderstreaming1.getTransceivers(connectionId1).length).toBe(0);
const track = { id: getUniqueId(), kind: "audio" };
renderstreaming2.addTrack(track);
expect(renderstreaming2.getTransceivers(connectionId1).length).toBe(1);
await waitFor(() => isGotOffer1);
expect(isGotOffer1).toBe(true);
await waitFor(() => isOnTrack1);
expect(isOnTrack1).toBe(true);
expect(renderstreaming1.getTransceivers(connectionId1).length).toBe(1);
await waitFor(() => isGotAnswer2);
expect(isGotAnswer2).toBe(true);
let isDisconnect1 = false;
renderstreaming1.onDisconnect = () => isDisconnect1 = true;
let isDisconnect2 = false;
renderstreaming2.onDisconnect = () => isDisconnect2 = true;
await renderstreaming1.deleteConnection();
await renderstreaming2.deleteConnection();
await waitFor(() => isDisconnect1 && isDisconnect2);
expect(isDisconnect1).toBe(true);
expect(isDisconnect2).toBe(true);
await renderstreaming1.stop();
await renderstreaming2.stop();
});
test(`onAddDataChannel in ${mode} mode`, async () => {
reset(mode == "private");
const config = getRTCConfiguration();
const renderstreaming1 = new RenderStreaming(new MockSignaling(), config);
const renderstreaming2 = new RenderStreaming(new MockSignaling(), config);
await renderstreaming1.start();
await renderstreaming2.start();
let isConnect1 = false;
renderstreaming1.onConnect = () => isConnect1 = true;
let isConnect2 = false;
renderstreaming2.onConnect = () => isConnect2 = true;
await renderstreaming1.createConnection(connectionId1);
await renderstreaming2.createConnection(connectionId1);
await waitFor(() => isConnect1 && isConnect2);
expect(isConnect1).toBe(true);
expect(isConnect2).toBe(true);
let isGotOffer1 = false;
let isAddChannel1 = false;
let isGotAnswer2 = false;
renderstreaming1.onGotOffer = () => { isGotOffer1 = true; };
renderstreaming1.onAddChannel = () => { isAddChannel1 = true; };
renderstreaming2.onGotAnswer = () => { isGotAnswer2 = true; };
renderstreaming2.createDataChannel("testchannel");
await waitFor(() => isGotOffer1);
expect(isGotOffer1).toBe(true);
await waitFor(() => isAddChannel1);
expect(isAddChannel1).toBe(true);
await waitFor(() => isGotAnswer2);
expect(isGotAnswer2).toBe(true);
let isDisconnect1 = false;
renderstreaming1.onDisconnect = () => isDisconnect1 = true;
let isDisconnect2 = false;
renderstreaming2.onDisconnect = () => isDisconnect2 = true;
await renderstreaming1.deleteConnection();
await renderstreaming2.deleteConnection();
await waitFor(() => isDisconnect1 && isDisconnect2);
expect(isDisconnect1).toBe(true);
expect(isDisconnect2).toBe(true);
await renderstreaming1.stop();
await renderstreaming2.stop();
});
});

View File

@@ -0,0 +1,18 @@
// mock class
/* eslint-disable no-unused-vars */
let instanceResize = null;
/* eslint-disable no-unused-vars */
let callbackResize = null;
export default class ResizeObserverMock {
constructor(callback) {
instanceResize = this;
callbackResize = callback;
}
disconnect() { }
/* eslint-disable no-unused-vars */
observe(target, options) { }
/* eslint-disable no-unused-vars */
unobserve(target) { }
}

View File

@@ -0,0 +1,143 @@
import {
InputRemoting,
} from "../src/inputremoting.js";
import {
Sender,
Observer
} from "../src/sender.js";
import {jest} from '@jest/globals';
import {DOMRect} from "./domrect.js";
// mock
class RTCDataChannel {
get readyState() {
return "open";
}
/* eslint-disable no-unused-vars */
send(message) {
}
}
describe(`Sender`, () => {
let inputRemoting = null;
let sender = null;
let observer = null;
let events = {};
let dc = null;
beforeEach(async () => {
// Empty our events before each test case
events = {};
// Define the addEventListener method with a Jest mock function
document.addEventListener = jest.fn((event, callback) => {
events[event] = callback;
});
document.removeEventListener = jest.fn((event, callback) => {
delete events[event];
});
document.getBoundingClientRect = function(){ return new DOMRect(0,0,0,0); };
sender = new Sender(document);
inputRemoting = new InputRemoting(sender);
dc = new RTCDataChannel();
observer = new Observer(dc);
});
test('devices', () => {
sender.addMouse();
expect(sender.devices.length).toBe(1);
sender.addKeyboard();
expect(sender.devices.length).toBe(2);
});
test('send messages while called startSending', () => {
jest.spyOn(dc, 'send');
sender.addMouse();
sender.addKeyboard();
inputRemoting.subscribe(observer);
inputRemoting.startSending();
expect(dc.send).toHaveBeenCalled();
});
describe('mouse', () => {
test('click', () => {
jest.spyOn(dc, 'send');
sender.addMouse();
inputRemoting.subscribe(observer);
inputRemoting.startSending();
events.click(
new MouseEvent('click', { buttons:1, clientX:0, clientY:0} ));
expect(dc.send).toHaveBeenCalledWith(expect.any(ArrayBuffer));
});
test('mousemove', () => {
jest.spyOn(dc, 'send');
sender.addMouse();
inputRemoting.subscribe(observer);
inputRemoting.startSending();
events.mousemove(
new MouseEvent('mousemove', { buttons:1, deltaX:0, deltaY:0 }));
expect(dc.send).toHaveBeenCalledWith(expect.any(ArrayBuffer));
});
test('wheel', () => {
jest.spyOn(dc, 'send');
sender.addMouse();
inputRemoting.subscribe(observer);
inputRemoting.startSending();
events.wheel(
new WheelEvent('wheel', { wheelDelta:0, deltaX:0, deltaY:0 }));
expect(dc.send).toHaveBeenCalledWith(expect.any(ArrayBuffer));
});
});
describe('keyboard', () => {
test('keydown', () => {
jest.spyOn(dc, 'send');
sender.addKeyboard();
inputRemoting.subscribe(observer);
inputRemoting.startSending();
events.keydown(
new KeyboardEvent('keydown', { code: 'KeyA' }));
expect(dc.send).toHaveBeenCalledWith(expect.any(ArrayBuffer));
});
test('keydown repeat', () => {
jest.spyOn(dc, 'send');
sender.addKeyboard();
inputRemoting.subscribe(observer);
inputRemoting.startSending();
events.keydown(
new KeyboardEvent('keydown', { code: 'KeyA', repeat: true }));
expect(dc.send).toHaveBeenCalledWith(expect.any(ArrayBuffer));
});
});
describe('touchscreen', () => {
test('touchstart', () => {
jest.spyOn(dc, 'send');
sender.addTouchscreen();
inputRemoting.subscribe(observer);
inputRemoting.startSending();
events.touchstart(
new TouchEvent("touchstart", {
changedTouches: [{ // InputInit
identifier: 0,
target: null,
clientX: 0,
clientY: 0,
screenX: 0,
screenY: 0,
pageX: 0,
pageY: 0,
radiusX: 0,
radiusY: 0,
rotationAngle: 0,
force: 0,
altitudeAngle: 0,
azimuthAngle:0,
touchType: "direct"
}]
}));
expect(dc.send).toHaveBeenCalledWith(expect.any(ArrayBuffer));
});
});
describe('gamepad', () => {
//todo
});
});

View File

@@ -0,0 +1,484 @@
import { jest } from '@jest/globals';
import * as Path from 'path';
import { setup, teardown } from 'jest-dev-server';
import { Signaling, WebSocketSignaling } from "../src/signaling.js";
import { MockSignaling, reset } from "./mocksignaling.js";
import { waitFor, sleep, serverExeName } from "./testutils.js";
const portNumber = 8081;
jest.setTimeout(10000);
describe.each([
{ mode: "mock" },
{ mode: "http" },
{ mode: "websocket" },
])('signaling test in public mode', ({ mode }) => {
let signaling1;
let signaling2;
const connectionId1 = "12345";
const connectionId2 = "67890";
const testsdp = "test sdp";
const testcandidate = "test candidate";
beforeAll(async () => {
if (mode == "mock") {
reset(false);
signaling1 = new MockSignaling(1);
signaling2 = new MockSignaling(1);
} else {
const path = Path.resolve(`../bin~/${serverExeName()}`);
let cmd = `${path} -p ${portNumber}`;
if (mode == "http") {
cmd += " -t http";
}
await setup({ command: cmd, port: portNumber, usedPortAction: 'error' });
if (mode == "http") {
signaling1 = new Signaling(1);
signaling2 = new Signaling(1);
}
if (mode == "websocket") {
signaling1 = new WebSocketSignaling(1);
signaling2 = new WebSocketSignaling(1);
}
}
await signaling1.start();
await signaling2.start();
});
afterAll(async () => {
await signaling1.stop();
await signaling2.stop();
signaling1 = null;
signaling2 = null;
if (mode == "mock") {
return;
}
await teardown();
// work around for linux, waitng kill server process
await sleep(1000);
});
test(`onConnect using ${mode}`, async () => {
const signaling1Spy = jest.spyOn(signaling1, 'dispatchEvent');
let connectRes;
let disconnectRes;
signaling1.addEventListener('connect', (e) => connectRes = e.detail);
signaling1.addEventListener('disconnect', (e) => disconnectRes = e.detail);
await signaling1.createConnection(connectionId1);
await waitFor(() => connectRes != null);
expect(connectRes.connectionId).toBe(connectionId1);
expect(connectRes.polite).toBe(true);
await signaling1.deleteConnection(connectionId1);
await waitFor(() => disconnectRes != null);
expect(disconnectRes.connectionId).toBe(connectionId1);
const disconnectCalledCount = signaling1Spy.mock.calls.map(x => x[0].type).filter(x => x == "disconnect").length;
expect(disconnectCalledCount).toBe(1);
signaling1Spy.mockRestore();
});
test(`onOffer using ${mode}`, async () => {
let connectRes1;
let disconnectRes1;
signaling1.addEventListener('connect', (e) => connectRes1 = e.detail);
signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail);
let connectRes2;
let disconnectRes2;
let offerRes2;
signaling2.addEventListener('connect', (e) => connectRes2 = e.detail);
signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail);
signaling2.addEventListener('offer', (e) => offerRes2 = e.detail);
await signaling1.createConnection(connectionId1);
await signaling2.createConnection(connectionId2);
await waitFor(() => connectRes1 != null && connectRes2 != null);
expect(connectRes1.connectionId).toBe(connectionId1);
expect(connectRes2.connectionId).toBe(connectionId2);
await signaling1.sendOffer(connectionId1, testsdp);
await waitFor(() => offerRes2 != null);
expect(offerRes2.connectionId).toBe(connectionId1);
expect(offerRes2.polite).toBe(false);
await signaling1.deleteConnection(connectionId1);
await waitFor(() => disconnectRes1 != null);
expect(disconnectRes1.connectionId).toBe(connectionId1);
await signaling2.deleteConnection(connectionId2);
await waitFor(() => disconnectRes2 != null);
expect(disconnectRes2.connectionId).toBe(connectionId2);
});
test(`onAnswer using ${mode}`, async () => {
let connectRes1;
let disconnectRes1;
let answerRes1;
signaling1.addEventListener('connect', (e) => connectRes1 = e.detail);
signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail);
signaling1.addEventListener('answer', (e) => answerRes1 = e.detail);
let connectRes2;
let disconnectRes2;
let offerRes2;
signaling2.addEventListener('connect', (e) => connectRes2 = e.detail);
signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail);
signaling2.addEventListener('offer', (e) => offerRes2 = e.detail);
await signaling1.createConnection(connectionId1);
await signaling2.createConnection(connectionId2);
await waitFor(() => connectRes1 != null && connectRes2 != null);
await signaling1.sendOffer(connectionId1, testsdp);
await waitFor(() => offerRes2 != null);
expect(offerRes2.connectionId).toBe(connectionId1);
expect(offerRes2.sdp).toBe(testsdp);
signaling2.sendAnswer(connectionId1, testsdp);
await waitFor(() => answerRes1 != null);
expect(answerRes1.connectionId).toBe(connectionId1);
expect(answerRes1.sdp).toBe(testsdp);
await signaling1.deleteConnection(connectionId1);
await waitFor(() => disconnectRes1 != null);
await signaling2.deleteConnection(connectionId2);
await waitFor(() => disconnectRes2 != null);
});
test(`onCandidate using ${mode}`, async () => {
let connectRes1;
let disconnectRes1;
let answerRes1;
let candidateRes1;
signaling1.addEventListener('connect', (e) => connectRes1 = e.detail);
signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail);
signaling1.addEventListener('answer', (e) => answerRes1 = e.detail);
signaling1.addEventListener('candidate', (e) => candidateRes1 = e.detail);
let connectRes2;
let disconnectRes2;
let offerRes2;
let candidateRes2;
signaling2.addEventListener('connect', (e) => connectRes2 = e.detail);
signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail);
signaling2.addEventListener('offer', (e) => offerRes2 = e.detail);
signaling2.addEventListener('candidate', (e) => candidateRes2 = e.detail);
await signaling1.createConnection(connectionId1);
await signaling2.createConnection(connectionId2);
await waitFor(() => connectRes1 != null && connectRes2 != null);
await signaling1.sendOffer(connectionId1, testsdp);
await waitFor(() => offerRes2 != null);
expect(offerRes2.connectionId).toBe(connectionId1);
expect(offerRes2.sdp).toBe(testsdp);
signaling2.sendAnswer(connectionId1, testsdp);
await waitFor(() => answerRes1 != null);
expect(answerRes1.connectionId).toBe(connectionId1);
expect(answerRes1.sdp).toBe(testsdp);
await signaling2.sendCandidate(connectionId1, testcandidate, 1, 1);
await waitFor(() => candidateRes1 != null);
expect(candidateRes1.connectionId).toBe(connectionId1);
expect(candidateRes1.candidate).toBe(testcandidate);
expect(candidateRes1.sdpMid).toBe(1);
expect(candidateRes1.sdpMLineIndex).toBe(1);
await signaling1.sendCandidate(connectionId1, testcandidate, 1, 1);
await waitFor(() => candidateRes2 != null);
expect(candidateRes2.connectionId).toBe(connectionId1);
expect(candidateRes2.candidate).toBe(testcandidate);
expect(candidateRes2.sdpMid).toBe(1);
expect(candidateRes2.sdpMLineIndex).toBe(1);
await signaling1.deleteConnection(connectionId1);
await waitFor(() => disconnectRes1 != null);
await signaling2.deleteConnection(connectionId2);
await waitFor(() => disconnectRes2 != null);
});
});
describe.each([
{ mode: "mock" },
{ mode: "http" },
{ mode: "websocket" },
])('signaling test in private mode', ({ mode }) => {
let signaling1;
let signaling2;
const connectionId = "12345";
const testsdp = "test sdp";
const testcandidate = "test candidate";
beforeAll(async () => {
if (mode == "mock") {
reset(true);
signaling1 = new MockSignaling(1);
signaling2 = new MockSignaling(1);
return;
}
const path = Path.resolve(`../bin~/${serverExeName()}`);
let cmd = `${path} -p ${portNumber} -m private`;
if (mode == "http") {
cmd += " -t http";
}
await setup({ command: cmd, port: portNumber, usedPortAction: 'error' });
if (mode == "http") {
signaling1 = new Signaling(1);
signaling2 = new Signaling(1);
}
if (mode == "websocket") {
signaling1 = new WebSocketSignaling(1);
signaling2 = new WebSocketSignaling(1);
}
await signaling1.start();
await signaling2.start();
});
afterAll(async () => {
await signaling1.stop();
await signaling2.stop();
signaling1 = null;
signaling2 = null;
if (mode == "mock") {
return;
}
await teardown();
// work around for linux, waitng kill server process
await sleep(1000);
});
test(`onConnect using ${mode}`, async () => {
let connectRes1;
let disconnectRes1;
signaling1.addEventListener('connect', (e) => connectRes1 = e.detail);
signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail);
let connectRes2;
let disconnectRes2;
signaling2.addEventListener('connect', (e) => connectRes2 = e.detail);
signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail);
await signaling1.createConnection(connectionId);
await waitFor(() => connectRes1 != null);
expect(connectRes1.connectionId).toBe(connectionId);
expect(connectRes1.polite).toBe(false);
await signaling2.createConnection(connectionId);
await waitFor(() => connectRes2 != null);
expect(connectRes2.connectionId).toBe(connectionId);
expect(connectRes2.polite).toBe(true);
await sleep(signaling1.interval * 2);
await signaling1.deleteConnection(connectionId);
await waitFor(() => disconnectRes1 != null && disconnectRes2 != null);
expect(disconnectRes1.connectionId).toBe(connectionId);
expect(disconnectRes2.connectionId).toBe(connectionId);
disconnectRes2 = null;
await signaling2.deleteConnection(connectionId);
await waitFor(() => disconnectRes2 != null);
});
test(`onOffer using ${mode}`, async () => {
let connectRes1;
let disconnectRes1;
signaling1.addEventListener('connect', (e) => connectRes1 = e.detail);
signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail);
let connectRes2;
let disconnectRes2;
let offerRes2;
signaling2.addEventListener('connect', (e) => connectRes2 = e.detail);
signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail);
signaling2.addEventListener('offer', (e) => offerRes2 = e.detail);
await signaling1.createConnection(connectionId);
await waitFor(() => connectRes1 != null);
expect(connectRes1.connectionId).toBe(connectionId);
signaling1.sendOffer(connectionId, testsdp);
await sleep(signaling1.interval * 2);
// Do not receive offer other signaling if not connected same sendoffer connectionId in private mode
expect(offerRes2).toBeUndefined();
await signaling2.createConnection(connectionId);
await waitFor(() => connectRes2 != null);
expect(connectRes2.connectionId).toBe(connectionId);
await signaling1.sendOffer(connectionId, testsdp);
await waitFor(() => offerRes2 != null);
expect(offerRes2.connectionId).toBe(connectionId);
expect(offerRes2.polite).toBe(true);
await signaling1.deleteConnection(connectionId);
await waitFor(() => disconnectRes1 != null && disconnectRes2 != null);
expect(disconnectRes1.connectionId).toBe(connectionId);
expect(disconnectRes2.connectionId).toBe(connectionId);
disconnectRes2 = null;
await signaling2.deleteConnection(connectionId);
await waitFor(() => disconnectRes2 != null);
});
test(`onAnswer using ${mode}`, async () => {
let connectRes1;
let disconnectRes1;
let answerRes1;
signaling1.addEventListener('connect', (e) => connectRes1 = e.detail);
signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail);
signaling1.addEventListener('answer', (e) => answerRes1 = e.detail);
let connectRes2;
let disconnectRes2;
let offerRes2;
signaling2.addEventListener('connect', (e) => connectRes2 = e.detail);
signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail);
signaling2.addEventListener('offer', (e) => offerRes2 = e.detail);
await signaling1.createConnection(connectionId);
await signaling2.createConnection(connectionId);
await waitFor(() => connectRes1 != null && connectRes2 != null);
await signaling1.sendOffer(connectionId, testsdp);
await waitFor(() => offerRes2 != null);
expect(offerRes2.connectionId).toBe(connectionId);
expect(offerRes2.sdp).toBe(testsdp);
await signaling2.sendAnswer(connectionId, testsdp);
await waitFor(() => answerRes1 != null);
expect(answerRes1.connectionId).toBe(connectionId);
expect(answerRes1.sdp).toBe(testsdp);
await signaling1.deleteConnection(connectionId);
await waitFor(() => disconnectRes1 != null && disconnectRes2 != null);
expect(disconnectRes1.connectionId).toBe(connectionId);
expect(disconnectRes2.connectionId).toBe(connectionId);
disconnectRes2 = null;
await signaling2.deleteConnection(connectionId);
await waitFor(() => disconnectRes2 != null);
});
test(`onCandidate using ${mode}`, async () => {
let connectRes1;
let disconnectRes1;
let answerRes1;
let candidateRes1;
signaling1.addEventListener('connect', (e) => connectRes1 = e.detail);
signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail);
signaling1.addEventListener('answer', (e) => answerRes1 = e.detail);
signaling1.addEventListener('candidate', (e) => candidateRes1 = e.detail);
let connectRes2;
let disconnectRes2;
let offerRes2;
let candidateRes2;
signaling2.addEventListener('connect', (e) => connectRes2 = e.detail);
signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail);
signaling2.addEventListener('offer', (e) => offerRes2 = e.detail);
signaling2.addEventListener('candidate', (e) => candidateRes2 = e.detail);
await signaling1.createConnection(connectionId);
await signaling2.createConnection(connectionId);
await waitFor(() => connectRes1 != null && connectRes2 != null);
await signaling1.sendOffer(connectionId, testsdp);
await waitFor(() => offerRes2 != null);
expect(offerRes2.connectionId).toBe(connectionId);
expect(offerRes2.sdp).toBe(testsdp);
await signaling2.sendAnswer(connectionId, testsdp);
await waitFor(() => answerRes1 != null);
expect(answerRes1.connectionId).toBe(connectionId);
expect(answerRes1.sdp).toBe(testsdp);
await signaling2.sendCandidate(connectionId, testcandidate, 1, 1);
await waitFor(() => candidateRes1 != null);
expect(candidateRes1.connectionId).toBe(connectionId);
expect(candidateRes1.candidate).toBe(testcandidate);
expect(candidateRes1.sdpMLineIndex).toBe(1);
expect(candidateRes1.sdpMid).toBe(1);
await signaling1.sendCandidate(connectionId, testcandidate, 1, 1);
await waitFor(() => candidateRes2 != null);
expect(candidateRes2.connectionId).toBe(connectionId);
expect(candidateRes2.candidate).toBe(testcandidate);
expect(candidateRes2.sdpMLineIndex).toBe(1);
expect(candidateRes2.sdpMid).toBe(1);
await signaling1.deleteConnection(connectionId);
await waitFor(() => disconnectRes1 != null && disconnectRes2 != null);
expect(disconnectRes1.connectionId).toBe(connectionId);
expect(disconnectRes2.connectionId).toBe(connectionId);
disconnectRes2 = null;
await signaling2.deleteConnection(connectionId);
await waitFor(() => disconnectRes2 != null);
});
test(`notReceiveOwnOfferAnswer using ${mode}`, async () => {
let connectRes1;
let disconnectRes1;
let offerRes1;
let answerRes1;
signaling1.addEventListener('connect', (e) => connectRes1 = e.detail);
signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail);
let connectRes2;
let disconnectRes2;
let offerRes2;
let answerRes2;
signaling2.addEventListener('connect', (e) => connectRes2 = e.detail);
signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail);
await signaling1.createConnection(connectionId);
await signaling2.createConnection(connectionId);
await waitFor(() => connectRes1 != null && connectRes2 != null);
signaling1.addEventListener('offer', (e) => offerRes1 = e.detail);
signaling2.addEventListener('offer', (e) => offerRes2 = e.detail);
await signaling1.sendOffer(connectionId, testsdp);
await waitFor(() => offerRes2 != null);
await sleep(signaling1.interval * 2);
expect(offerRes1).toBeUndefined();
expect(offerRes2).not.toBeUndefined();
expect(offerRes2.connectionId).toBe(connectionId);
expect(offerRes2.sdp).toBe(testsdp);
signaling1.addEventListener('answer', (e) => answerRes1 = e.detail);
signaling2.addEventListener('answer', (e) => answerRes2 = e.detail);
await signaling2.sendAnswer(connectionId, testsdp);
await waitFor(() => answerRes1 != null);
await sleep(signaling2.interval * 2);
expect(answerRes1).not.toBeUndefined();
expect(answerRes1.connectionId).toBe(connectionId);
expect(answerRes1.sdp).toBe(testsdp);
expect(answerRes2).toBeUndefined();
await signaling1.deleteConnection(connectionId);
await waitFor(() => disconnectRes1 != null && disconnectRes2 != null);
expect(disconnectRes1.connectionId).toBe(connectionId);
expect(disconnectRes2.connectionId).toBe(connectionId);
disconnectRes2 = null;
await signaling2.deleteConnection(connectionId);
await waitFor(() => disconnectRes2 != null);
});
});

View File

@@ -0,0 +1,39 @@
import process from "process";
export function waitFor(conditionFunction) {
const poll = resolve => {
if (conditionFunction()) resolve();
else setTimeout(() => poll(resolve), 100);
};
return new Promise(poll);
}
export async function sleep(milisecond) {
return new Promise(resolve => setTimeout(resolve, milisecond));
}
export function serverExeName() {
switch (process.platform) {
case 'win32':
return 'webserver.exe';
case 'darwin':
return 'webserver_mac';
case 'linux':
return 'webserver';
default:
return null;
}
}
export function getUniqueId() {
return new Date().getTime().toString(16) + Math.floor(1000 * Math.random()).toString(16);
}
export function getRTCConfiguration() {
let config = {};
config.sdpSemantics = 'unified-plan';
config.iceServers = [{ urls: ['stun:stun.l.google.com:19302'] }];
return config;
}